{"record":{"id":"67e848da8f3de5dc","repo":"musistudio/claude-code-router","slug":"plugin-pluginid-rollback-stop-hook-failed","errorCode":null,"errorMessage":"[plugin:${pluginId}] Rollback stop hook failed: ${formatError(error)}","messagePattern":"\\[plugin:(.+?)\\] Rollback stop hook failed: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/core/src/plugins/service.ts","lineNumber":916,"sourceCode":"\n  private async rollbackConfiguredPluginLoad(pluginId: string, snapshot: PluginServiceStateSnapshot): Promise<void> {\n    const newStopHooks = this.stopHooks.slice(snapshot.stopHooks.length).reverse();\n    this.apps = snapshot.apps;\n    this.coreGatewayConfig = snapshot.coreGatewayConfig;\n    this.coreProviderPlugins = snapshot.coreProviderPlugins;\n    this.gatewayRequestTransforms = snapshot.gatewayRequestTransforms;\n    this.gatewayRoutes = snapshot.gatewayRoutes;\n    this.providerAccountConnectors = snapshot.providerAccountConnectors;\n    this.proxyRoutes = snapshot.proxyRoutes;\n    this.resourceOwnerIds = snapshot.resourceOwnerIds;\n    this.stopHooks = snapshot.stopHooks;\n    this.virtualModelProfiles = snapshot.virtualModelProfiles;\n\n    for (const stopHook of newStopHooks) {\n      try {\n        await stopHook.stop({ reason: \"disabled\" });\n      } catch (error) {\n        console.warn(`[plugin:${pluginId}] Rollback stop hook failed: ${formatError(error)}`);\n      }\n    }\n\n    try {\n      await backendService.stopOwner(pluginId);\n    } catch (error) {\n      console.warn(`[plugin:${pluginId}] Rollback resource cleanup failed: ${formatError(error)}`);\n    }\n  }\n}\n\nexport const pluginService = new GatewayPluginService();\n\nfunction pluginPermissionAccess(pluginConfig: Pick<GatewayPluginConfig, \"enabled\" | \"id\" | \"permissions\">): PluginPermissionAccess {\n  const permissions = pluginConfig.permissions\n    ?? knownGatewayPluginDefaultPermissions(pluginConfig.id)\n    ?? (pluginConfig.enabled === true ? undefined : [...GATEWAY_PLUGIN_PERMISSION_IDS]);\n  return {","sourceCodeStart":898,"sourceCodeEnd":934,"githubUrl":"https://github.com/musistudio/claude-code-router/blob/99f24806c6a2c660b16e53e95211c517448a6c90/packages/core/src/plugins/service.ts#L898-L934","documentation":"During rollbackConfiguredPluginLoad, after a plugin's startup failed, each newly registered stop hook for that plugin is invoked with reason 'disabled' to undo partial registration; if one of those stop hooks throws, this warning is logged and rollback continues to the next hook and to backend resource cleanup.","triggerScenarios":"A plugin fails mid-start after registering stop hooks, and one of those hooks itself throws during undo — commonly because the resource it closes was never fully created before the original failure.","commonSituations":"Half-initialized plugin state: start threw after partial setup; teardown assuming fields that are undefined; async cleanup racing the rollback.","solutions":["Fix the underlying startup failure first (error 546 carries the root cause) — rollback warnings then disappear.","Make the plugin's stop hooks idempotent and null-safe so they never throw during undo.","Upgrade the plugin/core pair for lifecycle fixes."],"exampleFix":"// before\nstop: async () => { await this.db.close(); } // throws if start failed before db was created\n// after\nstop: async () => { await this.db?.close().catch(() => {}); }","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"null","tryCatchPattern":"null","preventionTips":["Make stop hooks idempotent so rollback of partial starts succeeds","Fix root startup failures promptly","Initialize resources before registering their stop hooks"],"tags":["plugins","rollback","stop-hook","lifecycle"],"backgroundTag":"plugin-stop-hook-failed","analyzedSha":"99f24806c6a2c660b16e53e95211c517448a6c90","analyzedAt":"2026-08-27T04:11:01.184Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}