musistudio/claude-code-router · warning
[plugin] Stop hook failed: ${formatError(error)}
Error message
[plugin] Stop hook failed: ${formatError(error)} What it means
Warned by GatewayPluginService.stop when an individual plugin stop hook throws while being stopped (reason from stopReasonForPlugin: e.g. 'disabled' or config change). Iteration continues over remaining hooks and resource owners, so one plugin's failure to clean up does not block overall shutdown.
Source
Thrown at packages/core/src/plugins/service.ts:298
try {
await this.loadConfiguredPlugin(pluginConfig);
} catch (error) {
await this.rollbackConfiguredPluginLoad(pluginConfig.id, snapshot);
console.warn(`[plugin:${pluginConfig.id}] Disabled after startup failure: ${formatError(error)}`);
}
}
}
async stop(options: { nextConfig?: AppConfig } = {}): Promise<void> {
const stopHooks = [...this.stopHooks].reverse();
const nextEnabledPluginIds = options.nextConfig ? enabledPluginIds(options.nextConfig) : undefined;
this.stopHooks = [];
for (const stopHook of stopHooks) {
try {
await stopHook.stop({ reason: stopReasonForPlugin(stopHook.pluginId, nextEnabledPluginIds) });
} catch (error) {
console.warn(`[plugin] Stop hook failed: ${formatError(error)}`);
}
}
const resourceOwnerIds = [...this.resourceOwnerIds].reverse();
this.resourceOwnerIds.clear();
for (const ownerId of resourceOwnerIds) {
await backendService.stopOwner(ownerId);
}
this.config = undefined;
this.apps = [];
this.coreGatewayConfig = {};
this.coreProviderPlugins = [];
this.gatewayRequestTransforms = [];
this.gatewayRoutes = [];
this.proxyRoutes = [];
this.providerAccountConnectors.clear();
this.virtualModelProfiles = [];View on GitHub (pinned to 99f24806c6)
Solutions
- Identify the plugin from the error and update/patch its stop hook.
- Report the throw to the plugin author — stop hooks must be defensive and never throw.
- If it blocks practical operation (e.g. lingering handles), restart the process after ensuring critical state is flushed.
Example fix
// plugin stop hook best practice
// before
stop: async () => { await this.server.close(); } // throws if server never started
// after
stop: async () => { if (this.server) await this.server.close().catch(() => {}); } Defensive patterns
Strategy: try-catch
Validate before calling
null
Type guard
null
Try / catch
null
Prevention
- Write stop hooks that never throw and are idempotent
- Close only resources you actually created
- Test plugin start/stop cycles during development
When it happens
Trigger: Calling stop or start (which restarts), or loadConfiguredPlugin/rollbackConfiguredPluginLoad, where a plugin's registered stop callback rejects — unclosed server, hung async cleanup, teardown bug.
Common situations: A third-party plugin whose stop awaits something that never resolves or throws; shutdown racing in-flight requests handled by the plugin; teardown code assuming resources that were never created.
Related errors
- [plugin:${pluginId}] Rollback stop hook failed: ${formatErro
- [backend:${ownerId}] SQLite store close failed: ${formatErro
- [plugin-marketplace] Failed to fetch marketplace from ${url}
- [plugin-marketplace] Failed to load marketplace entry: ${for
- [plugin:${pluginConfig.id}] Disabled after startup failure:
AI-assisted analysis of musistudio/claude-code-router@99f24806c6 (2026-08-27).
Data as JSON: /api/errors/053145d67e635153.
Report an issue: GitHub.