* Common logic for executing hooks * @param hookInput The structured hook input that will be validated and converted to JSON * @param toolUseID The ID for tracking this hook execution * @param matchQuery The query to match against hook matchers * @param signal Optional AbortSignal to cancel hook
({
hookInput,
toolUseID,
matchQuery,
signal,
timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
toolUseContext,
messages,
forceSyncExecution,
requestPrompt,
toolInputSummary,
}: {
hookInput: HookInput
toolUseID: string
matchQuery?: string
signal?: AbortSignal
timeoutMs?: number
toolUseContext?: ToolUseContext
messages?: Message[]
forceSyncExecution?: boolean
requestPrompt?: (
sourceName: string,
toolInputSummary?: string | null,
) => (request: PromptRequest) => Promise<PromptResponse>
toolInputSummary?: string | null
})
| 2086 | * @returns Async generator that yields progress messages and hook results |
| 2087 | */ |
| 2088 | async function* executeHooks({ |
| 2089 | hookInput, |
| 2090 | toolUseID, |
| 2091 | matchQuery, |
| 2092 | signal, |
| 2093 | timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS, |
| 2094 | toolUseContext, |
| 2095 | messages, |
| 2096 | forceSyncExecution, |
| 2097 | requestPrompt, |
| 2098 | toolInputSummary, |
| 2099 | }: { |
| 2100 | hookInput: HookInput |
| 2101 | toolUseID: string |
| 2102 | matchQuery?: string |
| 2103 | signal?: AbortSignal |
| 2104 | timeoutMs?: number |
| 2105 | toolUseContext?: ToolUseContext |
| 2106 | messages?: Message[] |
| 2107 | forceSyncExecution?: boolean |
| 2108 | requestPrompt?: ( |
| 2109 | sourceName: string, |
| 2110 | toolInputSummary?: string | null, |
| 2111 | ) => (request: PromptRequest) => Promise<PromptResponse> |
| 2112 | toolInputSummary?: string | null |
| 2113 | }): AsyncGenerator<AggregatedHookResult> { |
| 2114 | if (shouldDisableAllHooksIncludingManaged()) { |
| 2115 | return |
| 2116 | } |
| 2117 | |
| 2118 | if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { |
| 2119 | return |
| 2120 | } |
| 2121 | |
| 2122 | const hookEvent = hookInput.hook_event_name |
| 2123 | const hookName = matchQuery ? `${hookEvent}:${matchQuery}` : hookEvent |
| 2124 | |
| 2125 | // Bind the prompt callback to this hook's name and tool input summary so the UI can display context |
| 2126 | const boundRequestPrompt = requestPrompt?.(hookName, toolInputSummary) |
| 2127 | |
| 2128 | // SECURITY: ALL hooks require workspace trust in interactive mode |
| 2129 | // This centralized check prevents RCE vulnerabilities for all current and future hooks |
| 2130 | if (shouldSkipHookDueToTrust()) { |
| 2131 | logForDebugging( |
| 2132 | `Skipping ${hookName} hook execution - workspace trust not accepted`, |
| 2133 | ) |
| 2134 | return |
| 2135 | } |
| 2136 | |
| 2137 | const appState = toolUseContext ? toolUseContext.getAppState() : undefined |
| 2138 | // Use the agent's session ID if available, otherwise fall back to main session |
| 2139 | const sessionId = toolUseContext?.agentId ?? getSessionId() |
| 2140 | const matchingHooks = await getMatchingHooks( |
| 2141 | appState, |
| 2142 | sessionId, |
| 2143 | hookEvent, |
| 2144 | hookInput, |
| 2145 | toolUseContext?.options?.tools, |
no test coverage detected