When hook handlers cannot be unregistered, disabling a plugin through the admin UI is cosmetic. The plugin's handlers keep firing on every request, every event, every lifecycle call — consuming CPU, accessing data, and potentially interfering with replacement plugins. CWE-404 (Improper Resource Shutdown) applies directly: the resource — an active event handler — is never released. In long-running hosts with hot-reload or dynamic plugin enable/disable, unregistration is the only mechanism that makes plugin state changes observable without a restart.
Medium because the inability to unregister hook handlers makes plugin disable purely cosmetic — handlers keep running after removal, making clean hot-reload architecturally impossible.
Have every registration call return a disposable. When the plugin manager deactivates a plugin, call dispose() on every disposable that plugin registered. This pattern composes cleanly with VS Code's context.subscriptions model.
on(hook: string, handler: Function): Disposable {
this.handlers.get(hook)?.push(handler);
return {
dispose: () => {
const handlers = this.handlers.get(hook);
if (handlers) {
const idx = handlers.indexOf(handler);
if (idx >= 0) handlers.splice(idx, 1);
}
}
};
}
ID: plugin-extension-architecture.hook-system.unregistration
Severity: medium
What to look for: Check if plugins can cleanly remove their registered hook handlers. Look for:
off(), removeListener(), removeHook(), or unsubscribe() methodconst unsub = hooks.on('event', handler); unsub();context.subscriptions.push(disposable) — disposables auto-cleaned on deactivate
Without unregistration, disabling a plugin leaves its handlers attached — they continue to run even after the plugin is "removed."Pass criteria: Count all registration/unregistration pairs. Hook handlers can be unregistered, either manually (via returned disposable or explicit removal API) or automatically (host removes all handlers when a plugin is deactivated/destroyed). The mechanism is documented. 100% of hook registrations must have a corresponding unregistration mechanism.
Fail criteria: No way to remove a registered handler. Disabling or removing a plugin leaves its handlers attached and running. OR unregistration exists but doesn't work correctly (handlers still fire after removal).
Skip (N/A) when: Plugins are loaded once at startup and never removed during the application's lifetime (e.g., build-time-only plugin systems like Babel plugins that run during compilation and then the process exits).
Detail on fail: "hooks.on() returns void. There is no off(), removeListener(), or dispose() method. Once a plugin registers a handler, it cannot be removed. Disabling a plugin via the UI still leaves its handlers firing on every request."
Remediation: Unregistration is essential for hot-reload, plugin disable/enable, and clean shutdown. Without it, the only way to remove a plugin's hooks is to restart the entire application.
// Return a disposable from registration:
on(hook: string, handler: Function): Disposable {
this.handlers.get(hook)?.push(handler);
return {
dispose: () => {
const handlers = this.handlers.get(hook);
if (handlers) {
const idx = handlers.indexOf(handler);
if (idx >= 0) handlers.splice(idx, 1);
}
}
};
}