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
This commit is contained in:
JacobZyy
2026-05-19 14:03:13 +08:00
parent 3dd414226b
commit 4d105d0559
6 changed files with 138 additions and 17 deletions
+15 -1
View File
@@ -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,
}
}
+87 -16
View File
@@ -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<string, unknown> }> = []
export function setPluginHooksConfigs(configs: Array<{ hooks?: Record<string, unknown> }>): 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<string, unknown>
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<string, unknown> }>
): 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<ClaudeHooksConfig | null> {
@@ -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,
+14
View File
@@ -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[]
}
+3
View File
@@ -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,
@@ -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)
}
}
+1
View File
@@ -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";