* 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
})
| 3138 | * @returns Array of HookOutsideReplResult objects containing command, succeeded, and output |
| 3139 | */ |
| 3140 | async function executeHooksOutsideREPL({ |
| 3141 | getAppState, |
| 3142 | hookInput, |
| 3143 | matchQuery, |
| 3144 | signal, |
| 3145 | timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS, |
| 3146 | }: { |
| 3147 | getAppState?: () => AppState |
| 3148 | hookInput: HookInput |
| 3149 | matchQuery?: string |
| 3150 | signal?: AbortSignal |
| 3151 | timeoutMs: number |
| 3152 | }): Promise<HookOutsideReplResult[]> { |
| 3153 | if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { |
| 3154 | return [] |
| 3155 | } |
| 3156 | |
| 3157 | const hookEvent = hookInput.hook_event_name |
| 3158 | const hookName = matchQuery ? `${hookEvent}:${matchQuery}` : hookEvent |
| 3159 | if (shouldDisableAllHooksIncludingManaged()) { |
| 3160 | logForDebugging( |
| 3161 | `Skipping hooks for ${hookName} due to 'disableAllHooks' managed setting`, |
| 3162 | ) |
| 3163 | return [] |
| 3164 | } |
| 3165 | |
| 3166 | // SECURITY: ALL hooks require workspace trust in interactive mode |
| 3167 | // This centralized check prevents RCE vulnerabilities for all current and future hooks |
| 3168 | if (shouldSkipHookDueToTrust()) { |
| 3169 | logForDebugging( |
| 3170 | `Skipping ${hookName} hook execution - workspace trust not accepted`, |
| 3171 | ) |
| 3172 | return [] |
| 3173 | } |
| 3174 | |
| 3175 | const appState = getAppState ? getAppState() : undefined |
| 3176 | // Use main session ID for outside-REPL hooks |
| 3177 | const sessionId = getSessionId() |
| 3178 | const matchingHooks = await getMatchingHooks( |
| 3179 | appState, |
| 3180 | sessionId, |
| 3181 | hookEvent, |
| 3182 | hookInput, |
| 3183 | ) |
| 3184 | if (matchingHooks.length === 0) { |
| 3185 | return [] |
| 3186 | } |
| 3187 | |
| 3188 | if (signal?.aborted) { |
| 3189 | return [] |
| 3190 | } |
| 3191 | |
| 3192 | const userHooks = matchingHooks.filter(h => !isInternalHook(h)) |
| 3193 | if (userHooks.length > 0) { |
| 3194 | const pluginHookCounts = getPluginHookCounts(userHooks) |
| 3195 | const hookTypeCounts = getHookTypeCounts(userHooks) |
| 3196 | logEvent(`tengu_run_hook`, { |
| 3197 | hookName: |
no test coverage detected