Hook unregistration supported
Why it matters
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.
Severity rationale
Medium because the inability to unregister hook handlers makes plugin disable purely cosmetic — handlers keep running after removal, making clean hot-reload architecturally impossible.
Remediation
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);
}
}
};
}
Detection
-
ID:
unregistration -
Severity:
medium -
What to look for: Check if plugins can cleanly remove their registered hook handlers. Look for:
- An
off(),removeListener(),removeHook(), orunsubscribe()method - Registration returning a disposable/unsubscribe function:
const unsub = hooks.on('event', handler); unsub(); - Automatic cleanup tied to plugin deactivation (host removes all hooks registered by a deactivating plugin)
- VS Code pattern:
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."
- An
-
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); } } }; }
External references
- iso-25010:2011 · reliability.fault-tolerance — Fault tolerance — unregistration prevents resource leaks when plugins are disabled or the host shuts down
- cwe · CWE-404 — Improper Resource Shutdown or Release
Taxons
History
- 2026-04-18·v1.0.0·Initial import from plugin-extension-architecture·automated