musistudio/claude-code-router · warning

[plugin] Stop hook failed

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.

Solutions

  1. Identify the plugin from the error and update/patch its stop hook.
  2. Report the throw to the plugin author — stop hooks must be defensive and never throw.
  3. 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

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


AI-assisted analysis of musistudio/claude-code-router@99f24806c6 (2026-08-27). Data as JSON: /api/errors/053145d67e635153. Report an issue: GitHub.

Appendix: 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)