* Execute hooks outside of the REPL (e.g. notifications, session end) * * Unlike executeHooks() which yields messages that are exposed to the model as * system messages, this function only logs errors via logForDebugging (visible * with --debug). Callers that need to surface errors to users shou
({
getAppState,
hookInput,
matchQuery,
signal,
timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
}: {
getAppState?: () => AppState
hookInput: HookInput
matchQuery?: string
signal?: AbortSignal
timeoutMs: number
})
| 3084 | * @returns Array of HookOutsideReplResult objects containing command, succeeded, and output |
| 3085 | */ |
| 3086 | async function executeHooksOutsideREPL({ |
| 3087 | getAppState, |
| 3088 | hookInput, |
| 3089 | matchQuery, |
| 3090 | signal, |
| 3091 | timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS, |
| 3092 | }: { |
| 3093 | getAppState?: () => AppState |
| 3094 | hookInput: HookInput |
| 3095 | matchQuery?: string |
| 3096 | signal?: AbortSignal |
| 3097 | timeoutMs: number |
| 3098 | }): Promise<HookOutsideReplResult[]> { |
| 3099 | if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { |
| 3100 | return [] |
| 3101 | } |
| 3102 | |
| 3103 | const hookEvent = hookInput.hook_event_name |
| 3104 | const hookName = matchQuery ? `${hookEvent}:${matchQuery}` : hookEvent |
| 3105 | if (shouldDisableAllHooksIncludingManaged()) { |
| 3106 | logForDebugging( |
| 3107 | `Skipping hooks for ${hookName} due to 'disableAllHooks' managed setting`, |
| 3108 | ) |
| 3109 | return [] |
| 3110 | } |
| 3111 | |
| 3112 | // SECURITY: ALL hooks require workspace trust in interactive mode |
| 3113 | // This centralized check prevents RCE vulnerabilities for all current and future hooks |
| 3114 | if (shouldSkipHookDueToTrust()) { |
| 3115 | logForDebugging( |
| 3116 | `Skipping ${hookName} hook execution - workspace trust not accepted`, |
| 3117 | ) |
| 3118 | return [] |
| 3119 | } |
| 3120 | |
| 3121 | const appState = getAppState ? getAppState() : undefined |
| 3122 | // Use main session ID for outside-REPL hooks |
| 3123 | const sessionId = getSessionId() |
| 3124 | const matchingHooks = await getMatchingHooks( |
| 3125 | appState, |
| 3126 | sessionId, |
| 3127 | hookEvent, |
| 3128 | hookInput, |
| 3129 | ) |
| 3130 | if (matchingHooks.length === 0) { |
| 3131 | return [] |
| 3132 | } |
| 3133 | |
| 3134 | if (signal?.aborted) { |
| 3135 | return [] |
| 3136 | } |
| 3137 | |
| 3138 | const userHooks = matchingHooks.filter(h => !isInternalHook(h)) |
| 3139 | if (userHooks.length > 0) { |
| 3140 | const pluginHookCounts = getPluginHookCounts(userHooks) |
| 3141 | const hookTypeCounts = getHookTypeCounts(userHooks) |
| 3142 | logEvent(`tengu_run_hook`, { |
| 3143 | hookName: |
no test coverage detected