From 4d105d055970a7033ca7cccf03b2d19921c26f89 Mon Sep 17 00:00:00 2001 From: JacobZyy Date: Tue, 19 May 2026 14:03:13 +0800 Subject: [PATCH] fix(hooks): merge marketplace plugin hooksConfigs into claude-code-hooks at config time Previously, loadPluginHooksConfigs() loaded plugin hooks from marketplace plugins (hookify, superpowers, zzcommon, zzfe, etc.) into pluginComponents.hooksConfigs, but config-handler.ts never consumed them. This meant plugin hooks were discovered but never merged into the runtime hooks dispatch system. Changes: - Extend ClaudeHookEvent and ClaudeHooksConfig to support all 12 event types (PostToolUseFailure, PermissionRequest, Notification, SubagentStart, SubagentStop, SessionStart, SessionEnd) in addition to the existing 5 - Add ALL_HOOK_EVENT_TYPES constant as single source of truth for event type iteration - Add mergePluginHooksConfigs() to unwrap plugin HooksConfig (with hooks wrapper) into flat ClaudeHooksConfig, filtering out unsupported prompt/agent hook types - Add setPluginHooksConfigs() to store pending plugin configs and invalidate the config cache - Create applyHookConfig() handler following existing applyXxxConfig pattern, wired into config-handler after loadPluginComponents() - Extend DisabledHooksConfig and mergeDisabledHooks for all 12 events Closes #4179 --- src/hooks/claude-code-hooks/config-loader.ts | 16 ++- src/hooks/claude-code-hooks/config.ts | 103 ++++++++++++++++--- src/hooks/claude-code-hooks/types.ts | 14 +++ src/plugin-handlers/config-handler.ts | 3 + src/plugin-handlers/hook-config-handler.ts | 18 ++++ src/plugin-handlers/index.ts | 1 + 6 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 src/plugin-handlers/hook-config-handler.ts diff --git a/src/hooks/claude-code-hooks/config-loader.ts b/src/hooks/claude-code-hooks/config-loader.ts index ea494ffdb..2b079fbfc 100644 --- a/src/hooks/claude-code-hooks/config-loader.ts +++ b/src/hooks/claude-code-hooks/config-loader.ts @@ -11,7 +11,14 @@ export interface DisabledHooksConfig { Stop?: string[] PreToolUse?: string[] PostToolUse?: string[] + PostToolUseFailure?: string[] + PermissionRequest?: string[] UserPromptSubmit?: string[] + Notification?: string[] + SubagentStart?: string[] + SubagentStop?: string[] + SessionStart?: string[] + SessionEnd?: string[] PreCompact?: string[] } @@ -78,10 +85,17 @@ function mergeDisabledHooks( if (!base) return override return { - Stop: override.Stop ?? base.Stop, PreToolUse: override.PreToolUse ?? base.PreToolUse, PostToolUse: override.PostToolUse ?? base.PostToolUse, + PostToolUseFailure: override.PostToolUseFailure ?? base.PostToolUseFailure, + PermissionRequest: override.PermissionRequest ?? base.PermissionRequest, UserPromptSubmit: override.UserPromptSubmit ?? base.UserPromptSubmit, + Notification: override.Notification ?? base.Notification, + Stop: override.Stop ?? base.Stop, + SubagentStart: override.SubagentStart ?? base.SubagentStart, + SubagentStop: override.SubagentStop ?? base.SubagentStop, + SessionStart: override.SessionStart ?? base.SessionStart, + SessionEnd: override.SessionEnd ?? base.SessionEnd, PreCompact: override.PreCompact ?? base.PreCompact, } } diff --git a/src/hooks/claude-code-hooks/config.ts b/src/hooks/claude-code-hooks/config.ts index 7d6341cfe..5960703f1 100644 --- a/src/hooks/claude-code-hooks/config.ts +++ b/src/hooks/claude-code-hooks/config.ts @@ -22,11 +22,33 @@ interface RawHookMatcher { interface RawClaudeHooksConfig { PreToolUse?: RawHookMatcher[] PostToolUse?: RawHookMatcher[] + PostToolUseFailure?: RawHookMatcher[] + PermissionRequest?: RawHookMatcher[] UserPromptSubmit?: RawHookMatcher[] + Notification?: RawHookMatcher[] Stop?: RawHookMatcher[] + SubagentStart?: RawHookMatcher[] + SubagentStop?: RawHookMatcher[] + SessionStart?: RawHookMatcher[] + SessionEnd?: RawHookMatcher[] PreCompact?: RawHookMatcher[] } +const ALL_HOOK_EVENT_TYPES: (keyof ClaudeHooksConfig)[] = [ + "PreToolUse", + "PostToolUse", + "PostToolUseFailure", + "PermissionRequest", + "UserPromptSubmit", + "Notification", + "Stop", + "SubagentStart", + "SubagentStop", + "SessionStart", + "SessionEnd", + "PreCompact", +] + function normalizeHookMatcher(raw: RawHookMatcher): HookMatcher { return { matcher: raw.matcher ?? raw.pattern ?? "*", @@ -36,15 +58,8 @@ function normalizeHookMatcher(raw: RawHookMatcher): HookMatcher { function normalizeHooksConfig(raw: RawClaudeHooksConfig): ClaudeHooksConfig { const result: ClaudeHooksConfig = {} - const eventTypes: (keyof RawClaudeHooksConfig)[] = [ - "PreToolUse", - "PostToolUse", - "UserPromptSubmit", - "Stop", - "PreCompact", - ] - for (const eventType of eventTypes) { + for (const eventType of ALL_HOOK_EVENT_TYPES) { if (raw[eventType]) { result[eventType] = raw[eventType].map(normalizeHookMatcher) } @@ -97,14 +112,7 @@ function mergeHooksConfig( override: ClaudeHooksConfig ): ClaudeHooksConfig { const result: ClaudeHooksConfig = { ...base } - const eventTypes: (keyof ClaudeHooksConfig)[] = [ - "PreToolUse", - "PostToolUse", - "UserPromptSubmit", - "Stop", - "PreCompact", - ] - for (const eventType of eventTypes) { + for (const eventType of ALL_HOOK_EVENT_TYPES) { if (override[eventType]) { result[eventType] = [...(base[eventType] || []), ...override[eventType]] } @@ -112,6 +120,64 @@ function mergeHooksConfig( return result } +let pendingPluginHooksConfigs: Array<{ hooks?: Record }> = [] + +export function setPluginHooksConfigs(configs: Array<{ hooks?: Record }>): void { + pendingPluginHooksConfigs = configs + configCache.clear() +} + +function isHookAction(h: unknown): h is HookAction { + if (typeof h !== "object" || h === null) return false + const obj = h as Record + if (obj.type === "command" && typeof obj.command === "string") return true + if (obj.type === "http" && typeof obj.url === "string") return true + return false +} + +interface PluginHookMatcher { + matcher?: string + pattern?: string + hooks?: unknown[] +} + +function isPluginHookMatcher(m: unknown): m is PluginHookMatcher { + return typeof m === "object" && m !== null && Array.isArray((m as PluginHookMatcher).hooks) +} + +export function mergePluginHooksConfigs( + base: ClaudeHooksConfig, + pluginHooksConfigs: Array<{ hooks?: Record }> +): ClaudeHooksConfig { + let result = { ...base } + + for (const pluginConfig of pluginHooksConfigs) { + if (!pluginConfig.hooks) continue + + const pluginOverrides: ClaudeHooksConfig = {} + for (const eventType of ALL_HOOK_EVENT_TYPES) { + const pluginMatchers = pluginConfig.hooks[eventType] + if (!Array.isArray(pluginMatchers)) continue + + const converted: HookMatcher[] = pluginMatchers + .filter(isPluginHookMatcher) + .map((m) => ({ + matcher: m.matcher ?? m.pattern ?? "*", + hooks: (m.hooks ?? []).filter(isHookAction), + })) + .filter((m) => m.hooks.length > 0) + + if (converted.length > 0) { + pluginOverrides[eventType] = converted + } + } + + result = mergeHooksConfig(result, pluginOverrides) + } + + return result +} + export async function loadClaudeHooksConfig( customSettingsPath?: string ): Promise { @@ -139,6 +205,11 @@ export async function loadClaudeHooksConfig( } } + // Merge plugin hooks configs + if (pendingPluginHooksConfigs.length > 0) { + mergedConfig = mergePluginHooksConfigs(mergedConfig, pendingPluginHooksConfigs) + } + const resolvedConfig = Object.keys(mergedConfig).length > 0 ? mergedConfig : null configCache.set(cacheKey, { value: resolvedConfig, diff --git a/src/hooks/claude-code-hooks/types.ts b/src/hooks/claude-code-hooks/types.ts index 28924de10..84ccde210 100644 --- a/src/hooks/claude-code-hooks/types.ts +++ b/src/hooks/claude-code-hooks/types.ts @@ -6,8 +6,15 @@ export type ClaudeHookEvent = | "PreToolUse" | "PostToolUse" + | "PostToolUseFailure" + | "PermissionRequest" | "UserPromptSubmit" + | "Notification" | "Stop" + | "SubagentStart" + | "SubagentStop" + | "SessionStart" + | "SessionEnd" | "PreCompact" export interface HookMatcher { @@ -33,8 +40,15 @@ export type HookAction = HookCommand | HookHttp export interface ClaudeHooksConfig { PreToolUse?: HookMatcher[] PostToolUse?: HookMatcher[] + PostToolUseFailure?: HookMatcher[] + PermissionRequest?: HookMatcher[] UserPromptSubmit?: HookMatcher[] + Notification?: HookMatcher[] Stop?: HookMatcher[] + SubagentStart?: HookMatcher[] + SubagentStop?: HookMatcher[] + SessionStart?: HookMatcher[] + SessionEnd?: HookMatcher[] PreCompact?: HookMatcher[] } diff --git a/src/plugin-handlers/config-handler.ts b/src/plugin-handlers/config-handler.ts index d77920c44..ddd9b8fe0 100644 --- a/src/plugin-handlers/config-handler.ts +++ b/src/plugin-handlers/config-handler.ts @@ -4,6 +4,7 @@ import type { ModelCacheState } from "../plugin-state"; import { log } from "../shared"; import { applyAgentConfig } from "./agent-config-handler"; import { applyCommandConfig } from "./command-config-handler"; +import { applyHookConfig } from "./hook-config-handler"; import { applyMcpConfig } from "./mcp-config-handler"; import { applyProviderConfig } from "./provider-config-handler"; import { loadPluginComponents } from "./plugin-components-loader"; @@ -30,6 +31,8 @@ export function createConfigHandler(deps: ConfigHandlerDeps) { const pluginComponents = await loadPluginComponents({ pluginConfig }); + applyHookConfig({ pluginComponents }); + const agentResult = await applyAgentConfig({ config, pluginConfig, diff --git a/src/plugin-handlers/hook-config-handler.ts b/src/plugin-handlers/hook-config-handler.ts new file mode 100644 index 000000000..8c84e20c2 --- /dev/null +++ b/src/plugin-handlers/hook-config-handler.ts @@ -0,0 +1,18 @@ +import type { PluginComponents } from "./plugin-components-loader" +import { setPluginHooksConfigs } from "../hooks/claude-code-hooks/config" +import { log } from "../shared" + +export function applyHookConfig(params: { + pluginComponents: PluginComponents; +}): void { + const { pluginComponents } = params + + if (pluginComponents.hooksConfigs.length > 0) { + log("[hook-config-handler] Merging plugin hooks configs", { + count: pluginComponents.hooksConfigs.length, + plugins: pluginComponents.plugins.map(p => p.name), + }) + + setPluginHooksConfigs(pluginComponents.hooksConfigs) + } +} diff --git a/src/plugin-handlers/index.ts b/src/plugin-handlers/index.ts index fa9bde977..9fe4d58b2 100644 --- a/src/plugin-handlers/index.ts +++ b/src/plugin-handlers/index.ts @@ -3,6 +3,7 @@ export * from "./provider-config-handler"; export * from "./agent-config-handler"; export * from "./tool-config-handler"; export * from "./mcp-config-handler"; +export * from "./hook-config-handler"; export * from "./command-config-handler"; export * from "./plugin-components-loader"; export * from "./category-config-resolver";