When hook names are untyped strings with no validation and handler signatures are undocumented, plugin authors must read the host's source code to discover what hooks exist and what arguments their handlers receive. This blocks third-party plugin development, makes the API fragile across upgrades, and guarantees that handler signature mismatches appear only at runtime — not at compile time. ISO 25010 maintainability.modularity depends on well-defined, discoverable interfaces between components; an opaque hook API violates this structurally.
Critical because an undiscoverable, untyped hook registration API prevents plugin authors from writing correct handlers without reading host internals, collapsing the boundary between plugin and host.
Replace any untyped string-based registration with a generic method that uses a typed hook map. Compile-time enforcement of hook names and handler signatures eliminates an entire class of runtime errors for plugin authors.
interface PluginHooks {
'before:request': (ctx: RequestContext) => void | Promise<void>;
'after:response': (ctx: ResponseContext) => void | Promise<void>;
'on:error': (error: PluginError, ctx: ErrorContext) => void;
}
class HookRegistry {
on<K extends keyof PluginHooks>(hook: K, handler: PluginHooks[K]): void;
}
ID: plugin-extension-architecture.hook-system.hook-registration
Severity: critical
What to look for: Find the API that plugins use to register handlers for hooks or events. Look for patterns like hooks.on('eventName', handler), hooks.tap('pluginName', handler) (tapable), app.addHook('hookName', handler) (Fastify), add_action('hook_name', callback) (WordPress), emitter.on('event', handler). Check that:
// tapable (webpack) — typed hooks:
compiler.hooks.compilation.tap('MyPlugin', (compilation) => { /* typed */ });
// Fastify — typed hook handlers:
fastify.addHook('onRequest', async (request, reply) => { /* typed */ });
Pass criteria: Enumerate all hook registration mechanisms. There is a clear, documented API for registering hook handlers. Hook names are discoverable (via types, constants, or documentation). Handler signatures are typed or documented so plugin authors know what arguments they receive and what they should return. At least 1 typed registration method must exist.
Fail criteria: No registration API exists (plugins modify host internals directly), OR the registration API accepts any string as a hook name with no validation or documentation, OR handler signatures are untyped and undocumented (plugin authors must read host source code to understand what arguments their handlers receive).
Skip (N/A) when: The plugin system does not use hooks or events — it uses a purely declarative extension model (e.g., plugins provide configuration objects or data, not executable handlers).
Detail on fail: "Plugins register handlers via emitter.on(eventName, handler) but eventName is an untyped string with no validation. There is no list of available hooks, no documentation of handler signatures, and no TypeScript types. Plugin authors must read the host source to discover what hooks exist and what arguments handlers receive."
Remediation: Hook registration should be discoverable, typed, and validated. Plugin authors should never need to read the host's source code to figure out which hooks exist or what their handlers receive.
// Define hooks as a typed map:
interface PluginHooks {
'before:request': (ctx: RequestContext) => void | Promise<void>;
'after:response': (ctx: ResponseContext) => void | Promise<void>;
'on:error': (error: PluginError, ctx: ErrorContext) => void;
}
// Registration validates hook names at compile time:
class HookRegistry {
on<K extends keyof PluginHooks>(hook: K, handler: PluginHooks[K]): void;
}