本章总览
utils/hooks.ts(约 5000 行)实现 Claude Code 的 用户可配置 Shell Hook(PreToolUse、PostToolUse、Stop、SessionStart、PermissionRequest 等),与 src/hooks/ React 目录完全不同。配置来自 settings 与插件;getMatchingHooks 按事件与 matcher 过滤;executeHooks 作为 async generator 统一执行命令、prompt hook、callback,并处理信任对话框、超时、阻塞退出码 2。本章是扩展 Claude Code 行为的主入口文档。
学完本章你应该能
- 列举至少 8 种 hook_event_name 及 matchQuery 来源
- 解释 executeHooks 的安全门(trust、CLAUDE_CODE_SIMPLE、shouldDisableAllHooks)
- 说明 executePreToolUse 与 executePermissionRequestHooks 如何委托 executeHooks
- 理解 blocking 与 exit code 2 的语义
- 区分 executeHooks 与 executeHooksOutsideREPL 的使用场景
核心概念(先读懂这些)
Hook 是用户代码,不是内部回调
settings 中 hooks 数组可定义 matcher 与 command。插件、Skill 也可注入 hookMatchers。isInternalHook 区分内部 callback(如 sessionFileAccessHooks)与用户 shell 命令。内部 hook 走 fast-path,跳过 span/progress,微秒级;用户 hook 可能启动子进程,必须限超时并尊重 AbortSignal。
信任模型:shouldSkipHookDueToTrust
交互模式下 所有 hook 需要用户接受 workspace trust,否则 RCE 风险(历史漏洞:SessionEnd 在未信任时执行)。非交互 SDK 模式隐式信任。executeHooks 开头统一检查,避免每个 export 重复遗漏。
executeHooks 与 OutsideREPL
executeHooks 向 REPL yield AggregatedHookResult,可把 hook 输出变成模型可见消息。executeHooksOutsideREPL 在 SessionEnd、PreCompact 等无 REPL 场景运行,hardcode main sessionId,且 function hook 在 OutsideREPL 路径会抛错(仅 Stop 等允许 function hook)。读源码时先看调用方再决定跟哪条路径。
建议学习步骤
- 阅读 shouldSkipHookDueToTrust 与 createBaseHookInput
- 阅读 getMatchingHooks 的 matchQuery switch
- 阅读 executeHooks 主体(超时、trust、匹配)
- 阅读 executePreToolUseHooks 与 executePermissionRequestHooks
- 对照 settings 示例配置一条 PreToolUse 命令
常见误区
注意
目录 src/hooks/ 与本文件无关;搜 Hook 时限定 utils/hooks.ts
注意
PermissionRequest hook 可改权限,但 policy ConfigChange 不可被 block
注意
PostToolUse 与 PostToolUseFailure 的 matcher 都是 tool_name
命名澄清与配置来源
Claude Code 存在三套「hook」概念:
| 名称 | 位置 | 用途 |
|---|---|---|
| Shell Hook | utils/hooks.ts | 用户 settings / 插件命令 |
| React Hooks | src/hooks/ | Ink UI 状态 |
| Permission hooks | useCanUseTool 调 utils/hooks.ts | PreToolUse / PermissionRequest 等 |
配置由 getHooksConfig(appState, sessionId, hookEvent) 读取,hooksConfigManager.ts 注释要求与 getMatchingHooks 的 matchQuery 规则保持同步。
典型 settings 片段(概念性):
"hooks": {
"PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": ".claude/hooks/lint.sh" }] }]
}
信任检查与 base hook input
shouldSkipHookDueToTrust(约 286 行):
- 非交互会话 → 不跳过
- 交互且未接受 trust dialog → 跳过所有 hook
createBaseHookInput 构造各事件共享字段:session_id、transcript_path、cwd、permission_mode、agent_id/agent_type。transcript_path 使用 getTranscriptPathForSession,与 session-storage 章的 sessionProjectDir 一致性问题直接相关。
安全: executeHooks 在 trust 失败时 logForDebugging 并 return,不抛异常,避免阻断主流程。
源码引用: src/utils/hooks.ts · 第 286–319 行(共 5023 行)
286| export function shouldSkipHookDueToTrust(): boolean {
287| // In non-interactive mode (SDK), trust is implicit - always execute
288| const isInteractive = !getIsNonInteractiveSession()
289| if (!isInteractive) {
290| return false
291| }
292|
293| // In interactive mode, ALL hooks require trust
294| const hasTrust = checkHasTrustDialogAccepted()
295| return !hasTrust
296| }
297|
298| /**
299| * Creates the base hook input that's common to all hook types
300| */
301| export function createBaseHookInput(
302| permissionMode?: string,
303| sessionId?: string,
304| // Typed narrowly (not ToolUseContext) so callers can pass toolUseContext
305| // directly via structural typing without this function depending on Tool.ts.
306| agentInfo?: { agentId?: string; agentType?: string },
307| ): {
308| session_id: string
309| transcript_path: string
310| cwd: string
311| permission_mode?: string
312| agent_id?: string
313| agent_type?: string
314| } {
315| const resolvedSessionId = sessionId ?? getSessionId()
316| // agent_type: subagent's type (from toolUseContext) takes precedence over
317| // the session's --agent flag. Hooks use agent_id presence to distinguish
318| // subagent calls from main-thread calls in a --agent session.
319| const resolvedAgentType = agentInfo?.agentType ?? getMainThreadAgentType()
getMatchingHooks:事件与 matcher
getMatchingHooks(约 1603 行)根据 hook_event_name 计算 matchQuery:
- PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest / PermissionDenied → tool_name
- SessionStart → source
- Notification → notification_type
- SubagentStart / SubagentStop → agent_type
- FileChanged → basename(file_path)
- 等等
然后用 matchesPattern(matchQuery, matcher.matcher) 过滤。插件 hook 附带 pluginRoot/pluginId;Skill hook 带 skillRoot。
verbose 日志帮助排查「配置了但未触发」:先看 matcher 是否过严,再看 sessionId 是否一致。
源码引用: src/utils/hooks.ts · 第 1603–1686 行(共 5023 行)
1603| export async function getMatchingHooks(
1604| appState: AppState | undefined,
1605| sessionId: string,
1606| hookEvent: HookEvent,
1607| hookInput: HookInput,
1608| tools?: Tools,
1609| ): Promise<MatchedHook[]> {
1610| try {
1611| const hookMatchers = getHooksConfig(appState, sessionId, hookEvent)
1612|
1613| // If you change the criteria below, then you must change
1614| // src/utils/hooks/hooksConfigManager.ts as well.
1615| let matchQuery: string | undefined = undefined
1616| switch (hookInput.hook_event_name) {
1617| case 'PreToolUse':
1618| case 'PostToolUse':
1619| case 'PostToolUseFailure':
1620| case 'PermissionRequest':
1621| case 'PermissionDenied':
1622| matchQuery = hookInput.tool_name
1623| break
1624| case 'SessionStart':
1625| matchQuery = hookInput.source
1626| break
1627| case 'Setup':
1628| matchQuery = hookInput.trigger
1629| break
1630| case 'PreCompact':
1631| case 'PostCompact':
1632| matchQuery = hookInput.trigger
1633| break
1634| case 'Notification':
1635| matchQuery = hookInput.notification_type
1636| break
1637| case 'SessionEnd':
1638| matchQuery = hookInput.reason
1639| break
1640| case 'StopFailure':
1641| matchQuery = hookInput.error
1642| break
1643| case 'SubagentStart':
1644| matchQuery = hookInput.agent_type
1645| break
1646| case 'SubagentStop':
1647| matchQuery = hookInput.agent_type
1648| break
1649| case 'TeammateIdle':
1650| case 'TaskCreated':
1651| case 'TaskCompleted':
1652| break
1653| case 'Elicitation':
1654| matchQuery = hookInput.mcp_server_name
1655| break
1656| case 'ElicitationResult':
1657| matchQuery = hookInput.mcp_server_name
1658| break
1659| case 'ConfigChange':
1660| matchQuery = hookInput.source
1661| break
1662| case 'InstructionsLoaded':
1663| matchQuery = hookInput.load_reason
1664| break
1665| case 'FileChanged':
1666| matchQuery = basename(hookInput.file_path)
1667| break
1668| default:
1669| break
1670| }
1671|
1672| logForDebugging(
1673| `Getting matching hook commands for ${hookEvent} with query: ${matchQuery}`,
1674| { level: 'verbose' },
1675| )
1676| logForDebugging(`Found ${hookMatchers.length} hook matchers in settings`, {
1677| level: 'verbose',
1678| })
1679|
1680| // Extract hooks with their plugin context (if any)
1681| const filteredMatchers = matchQuery
1682| ? hookMatchers.filter(
1683| matcher =>
1684| !matcher.matcher || matchesPattern(matchQuery, matcher.matcher),
1685| )
1686| : hookMatchers
executeHooks 核心循环
executeHooks(约 1952 行)是执行引擎心脏:
入口守卫:
- shouldDisableAllHooksIncludingManaged()
- CLAUDE_CODE_SIMPLE 环境变量
- shouldSkipHookDueToTrust()
准备:
- hookName = event 或 event:matchQuery
- boundRequestPrompt 绑定 UI 回调(prompt hook)
- getMatchingHooks → 空则 return
- signal aborted 则 return
用户 hook:
- 记录 tengu_run_hook 分析事件
- 内部 hook 批量 fast-path(callback 类型)
执行:
- 子进程 command、prompt、function 等类型分支(后续行)
- 解析 JSON 输出、阻塞错误 exit 2
- yield AggregatedHookResult 给 REPL
超时默认 TOOL_HOOK_EXECUTION_TIMEOUT_MS,调用方可覆盖。
源码引用: src/utils/hooks.ts · 第 1941–2048 行(共 5023 行)
1941| /**
1942| * Common logic for executing hooks
1943| * @param hookInput The structured hook input that will be validated and converted to JSON
1944| * @param toolUseID The ID for tracking this hook execution
1945| * @param matchQuery The query to match against hook matchers
1946| * @param signal Optional AbortSignal to cancel hook execution
1947| * @param timeoutMs Optional timeout in milliseconds for hook execution
1948| * @param toolUseContext Optional ToolUseContext for prompt-based hooks (required if using prompt hooks)
1949| * @param messages Optional conversation history for prompt/function hooks
1950| * @returns Async generator that yields progress messages and hook results
1951| */
1952| async function* executeHooks({
1953| hookInput,
1954| toolUseID,
1955| matchQuery,
1956| signal,
1957| timeoutMs = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
1958| toolUseContext,
1959| messages,
1960| forceSyncExecution,
1961| requestPrompt,
1962| toolInputSummary,
1963| }: {
1964| hookInput: HookInput
1965| toolUseID: string
1966| matchQuery?: string
1967| signal?: AbortSignal
1968| timeoutMs?: number
1969| toolUseContext?: ToolUseContext
1970| messages?: Message[]
1971| forceSyncExecution?: boolean
1972| requestPrompt?: (
1973| sourceName: string,
1974| toolInputSummary?: string | null,
1975| ) => (request: PromptRequest) => Promise<PromptResponse>
1976| toolInputSummary?: string | null
1977| }): AsyncGenerator<AggregatedHookResult> {
1978| if (shouldDisableAllHooksIncludingManaged()) {
1979| return
1980| }
1981|
1982| if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
1983| return
1984| }
1985|
1986| const hookEvent = hookInput.hook_event_name
1987| const hookName = matchQuery ? `${hookEvent}:${matchQuery}` : hookEvent
1988|
1989| // Bind the prompt callback to this hook's name and tool input summary so the UI can display context
1990| const boundRequestPrompt = requestPrompt?.(hookName, toolInputSummary)
1991|
1992| // SECURITY: ALL hooks require workspace trust in interactive mode
1993| // This centralized check prevents RCE vulnerabilities for all current and future hooks
1994| if (shouldSkipHookDueToTrust()) {
1995| logForDebugging(
1996| `Skipping ${hookName} hook execution - workspace trust not accepted`,
1997| )
1998| return
1999| }
2000|
2001| const appState = toolUseContext ? toolUseContext.getAppState() : undefined
2002| // Use the agent's session ID if available, otherwise fall back to main session
2003| const sessionId = toolUseContext?.agentId ?? getSessionId()
2004| const matchingHooks = await getMatchingHooks(
2005| appState,
2006| sessionId,
2007| hookEvent,
2008| hookInput,
2009| toolUseContext?.options?.tools,
2010| )
2011| if (matchingHooks.length === 0) {
2012| return
2013| }
2014|
2015| if (signal?.aborted) {
2016| return
2017| }
2018|
2019| const userHooks = matchingHooks.filter(h => !isInternalHook(h))
2020| if (userHooks.length > 0) {
2021| const pluginHookCounts = getPluginHookCounts(userHooks)
2022| const hookTypeCounts = getHookTypeCounts(userHooks)
2023| logEvent(`tengu_run_hook`, {
2024| hookName:
2025| hookName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
2026| numCommands: userHooks.length,
2027| hookTypeCounts: jsonStringify(
2028| hookTypeCounts,
2029| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
2030| ...(pluginHookCounts && {
2031| pluginHookCounts: jsonStringify(
2032| pluginHookCounts,
2033| ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
2034| }),
2035| })
2036| } else {
2037| // Fast-path: all hooks are internal callbacks (sessionFileAccessHooks,
2038| // attributionHooks). These return {} and don't use the abort signal, so we
2039| // can skip span/progress/abortSignal/processHookJSONOutput/resultLoop.
2040| // Measured: 6.01µs → ~1.8µs per PostToolUse hit (-70%).
2041| const batchStartTime = Date.now()
2042| const context = toolUseContext
2043| ? {
2044| getAppState: toolUseContext.getAppState,
2045| updateAttributionState: toolUseContext.updateAttributionState,
2046| }
2047| : undefined
2048| for (const [i, { hook }] of matchingHooks.entries()) {
executePreToolUseHooks
在工具执行 之前 调用。若 hasHookForEvent 为 false 直接 return,避免构建 hookInput。
构造 PreToolUseHookInput:spread createBaseHookInput,含 tool_name、tool_input、tool_use_id。
yield* executeHooks({ matchQuery: toolName, toolUseContext, requestPrompt, toolInputSummary })。
阻塞时 getPreToolHookBlockingMessage 格式化错误,query 层应停止 tool 执行。常见用途:lint、禁止写某些路径、录制审计日志。
源码引用: src/utils/hooks.ts · 第 3394–3436 行(共 5023 行)
3394| export async function* executePreToolHooks<ToolInput>(
3395| toolName: string,
3396| toolUseID: string,
3397| toolInput: ToolInput,
3398| toolUseContext: ToolUseContext,
3399| permissionMode?: string,
3400| signal?: AbortSignal,
3401| timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
3402| requestPrompt?: (
3403| sourceName: string,
3404| toolInputSummary?: string | null,
3405| ) => (request: PromptRequest) => Promise<PromptResponse>,
3406| toolInputSummary?: string | null,
3407| ): AsyncGenerator<AggregatedHookResult> {
3408| const appState = toolUseContext.getAppState()
3409| const sessionId = toolUseContext.agentId ?? getSessionId()
3410| if (!hasHookForEvent('PreToolUse', appState, sessionId)) {
3411| return
3412| }
3413|
3414| logForDebugging(`executePreToolHooks called for tool: ${toolName}`, {
3415| level: 'verbose',
3416| })
3417|
3418| const hookInput: PreToolUseHookInput = {
3419| ...createBaseHookInput(permissionMode, undefined, toolUseContext),
3420| hook_event_name: 'PreToolUse',
3421| tool_name: toolName,
3422| tool_input: toolInput,
3423| tool_use_id: toolUseID,
3424| }
3425|
3426| yield* executeHooks({
3427| hookInput,
3428| toolUseID,
3429| matchQuery: toolName,
3430| signal,
3431| timeoutMs,
3432| toolUseContext,
3433| requestPrompt,
3434| toolInputSummary,
3435| })
3436| }
executePostToolUse 与 Failure
PostToolUse:工具成功后,hookInput 含 tool_response,供用户脚本检查产出或发送通知。
PostToolUseFailure:工具抛错或返回错误时触发,matcher 仍为 tool_name,便于针对 Bash 失败运行诊断脚本。
两者都 yield* executeHooks,可能把 hook stdout 作为附加 user 消息反馈给模型(与 OutsideREPL 路径不同)。
源码引用: src/utils/hooks.ts · 第 3450–3525 行(共 5023 行)
3450| export async function* executePostToolHooks<ToolInput, ToolResponse>(
3451| toolName: string,
3452| toolUseID: string,
3453| toolInput: ToolInput,
3454| toolResponse: ToolResponse,
3455| toolUseContext: ToolUseContext,
3456| permissionMode?: string,
3457| signal?: AbortSignal,
3458| timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
3459| ): AsyncGenerator<AggregatedHookResult> {
3460| const hookInput: PostToolUseHookInput = {
3461| ...createBaseHookInput(permissionMode, undefined, toolUseContext),
3462| hook_event_name: 'PostToolUse',
3463| tool_name: toolName,
3464| tool_input: toolInput,
3465| tool_response: toolResponse,
3466| tool_use_id: toolUseID,
3467| }
3468|
3469| yield* executeHooks({
3470| hookInput,
3471| toolUseID,
3472| matchQuery: toolName,
3473| signal,
3474| timeoutMs,
3475| toolUseContext,
3476| })
3477| }
3478|
3479| /**
3480| * Execute post-tool-use-failure hooks if configured
3481| * @param toolName The name of the tool (e.g., 'Write', 'Edit', 'Bash')
3482| * @param toolUseID The ID of the tool use
3483| * @param toolInput The input that was passed to the tool
3484| * @param error The error message from the failed tool call
3485| * @param toolUseContext ToolUseContext for prompt-based hooks
3486| * @param isInterrupt Whether the tool was interrupted by user
3487| * @param permissionMode Optional permission mode from toolPermissionContext
3488| * @param signal Optional AbortSignal to cancel hook execution
3489| * @param timeoutMs Optional timeout in milliseconds for hook execution
3490| * @returns Async generator that yields progress messages and blocking errors
3491| */
3492| export async function* executePostToolUseFailureHooks<ToolInput>(
3493| toolName: string,
3494| toolUseID: string,
3495| toolInput: ToolInput,
3496| error: string,
3497| toolUseContext: ToolUseContext,
3498| isInterrupt?: boolean,
3499| permissionMode?: string,
3500| signal?: AbortSignal,
3501| timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
3502| ): AsyncGenerator<AggregatedHookResult> {
3503| const appState = toolUseContext.getAppState()
3504| const sessionId = toolUseContext.agentId ?? getSessionId()
3505| if (!hasHookForEvent('PostToolUseFailure', appState, sessionId)) {
3506| return
3507| }
3508|
3509| const hookInput: PostToolUseFailureHookInput = {
3510| ...createBaseHookInput(permissionMode, undefined, toolUseContext),
3511| hook_event_name: 'PostToolUseFailure',
3512| tool_name: toolName,
3513| tool_input: toolInput,
3514| tool_use_id: toolUseID,
3515| error,
3516| is_interrupt: isInterrupt,
3517| }
3518|
3519| yield* executeHooks({
3520| hookInput,
3521| toolUseID,
3522| matchQuery: toolName,
3523| signal,
3524| timeoutMs,
3525| toolUseContext,
executePermissionRequestHooks
当权限系统即将向用户弹窗(或 headless 需要 hook 决策)时触发。hookInput 含 permission_suggestions,可把 UI 上显示的规则建议传给脚本。
与 permissions.ts 内嵌的 PermissionRequest 处理互补:引擎侧先尝试 hook,再 fall through 到规则。
yield* executeHooks 参数与 PreToolUse 类似,支持 requestPrompt 供 prompt-based hook 向用户追问。
注意: 这与 hooks/useCanUseTool 的 React 弹窗并行存在——hook 可以 deny/allow,弹窗仍可显示,取决于返回决策与模式。
源码引用: src/utils/hooks.ts · 第 4157–4191 行(共 5023 行)
4157| export async function* executePermissionRequestHooks<ToolInput>(
4158| toolName: string,
4159| toolUseID: string,
4160| toolInput: ToolInput,
4161| toolUseContext: ToolUseContext,
4162| permissionMode?: string,
4163| permissionSuggestions?: PermissionUpdate[],
4164| signal?: AbortSignal,
4165| timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
4166| requestPrompt?: (
4167| sourceName: string,
4168| toolInputSummary?: string | null,
4169| ) => (request: PromptRequest) => Promise<PromptResponse>,
4170| toolInputSummary?: string | null,
4171| ): AsyncGenerator<AggregatedHookResult> {
4172| logForDebugging(`executePermissionRequestHooks called for tool: ${toolName}`)
4173|
4174| const hookInput: PermissionRequestHookInput = {
4175| ...createBaseHookInput(permissionMode, undefined, toolUseContext),
4176| hook_event_name: 'PermissionRequest',
4177| tool_name: toolName,
4178| tool_input: toolInput,
4179| permission_suggestions: permissionSuggestions,
4180| }
4181|
4182| yield* executeHooks({
4183| hookInput,
4184| toolUseID,
4185| matchQuery: toolName,
4186| signal,
4187| timeoutMs,
4188| toolUseContext,
4189| requestPrompt,
4190| toolInputSummary,
4191| })
Stop、SessionEnd 与其它事件
executeStopHooks:回合结束,可阻塞继续(exit 2)。getStopHookMessage 格式化错误。
executeSessionEndHooks / executePreCompactHooks / executePostCompactHooks:走 executeHooksOutsideREPL,无 yield 给 Ink。
executeUserPromptSubmitHooks:用户提交 prompt 前,可完全 block 提交(getUserPromptSubmitHookBlockingMessage)。
executeNotificationHooks:系统通知,match notification_type。
读文件后部 export 列表可得到完整事件表;新增事件必须同时更新 getMatchingHooks switch 与 hooksConfigManager。
源码引用: src/utils/hooks.ts · 第 1894–1939 行(共 5023 行)
1894| export function getStopHookMessage(blockingError: HookBlockingError): string {
1895| return `Stop hook feedback:\n${blockingError.blockingError}`
1896| }
1897|
1898| /**
1899| * Format a blocking error from a TeammateIdle hook.
1900| * @param blockingError The blocking error from the hook
1901| * @returns Formatted message to give feedback to the model
1902| */
1903| export function getTeammateIdleHookMessage(
1904| blockingError: HookBlockingError,
1905| ): string {
1906| return `TeammateIdle hook feedback:\n${blockingError.blockingError}`
1907| }
1908|
1909| /**
1910| * Format a blocking error from a TaskCreated hook.
1911| * @param blockingError The blocking error from the hook
1912| * @returns Formatted message to give feedback to the model
1913| */
1914| export function getTaskCreatedHookMessage(
1915| blockingError: HookBlockingError,
1916| ): string {
1917| return `TaskCreated hook feedback:\n${blockingError.blockingError}`
1918| }
1919|
1920| /**
1921| * Format a blocking error from a TaskCompleted hook.
1922| * @param blockingError The blocking error from the hook
1923| * @returns Formatted message to give feedback to the model
1924| */
1925| export function getTaskCompletedHookMessage(
1926| blockingError: HookBlockingError,
1927| ): string {
1928| return `TaskCompleted hook feedback:\n${blockingError.blockingError}`
1929| }
1930|
1931| /**
1932| * Format a list of blocking errors from a UserPromptSubmit hook's configured commands.
1933| * @param blockingErrors Array of blocking errors from hooks
1934| * @returns Formatted blocking message
1935| */
1936| export function getUserPromptSubmitHookBlockingMessage(
1937| blockingError: HookBlockingError,
1938| ): string {
1939| return `UserPromptSubmit operation blocked by hook:\n${blockingError.blockingError}`
源码引用: src/utils/hooks.ts · 第 3826–3865 行(共 5023 行)
3826| export async function* executeUserPromptSubmitHooks(
3827| prompt: string,
3828| permissionMode: string,
3829| toolUseContext: ToolUseContext,
3830| requestPrompt?: (
3831| sourceName: string,
3832| toolInputSummary?: string | null,
3833| ) => (request: PromptRequest) => Promise<PromptResponse>,
3834| ): AsyncGenerator<AggregatedHookResult> {
3835| const appState = toolUseContext.getAppState()
3836| const sessionId = toolUseContext.agentId ?? getSessionId()
3837| if (!hasHookForEvent('UserPromptSubmit', appState, sessionId)) {
3838| return
3839| }
3840|
3841| const hookInput: UserPromptSubmitHookInput = {
3842| ...createBaseHookInput(permissionMode),
3843| hook_event_name: 'UserPromptSubmit',
3844| prompt,
3845| }
3846|
3847| yield* executeHooks({
3848| hookInput,
3849| toolUseID: randomUUID(),
3850| signal: toolUseContext.abortController.signal,
3851| timeoutMs: TOOL_HOOK_EXECUTION_TIMEOUT_MS,
3852| toolUseContext,
3853| requestPrompt,
3854| })
3855| }
3856|
3857| /**
3858| * Execute session start hooks if configured
3859| * @param source The source of the session start (startup, resume, clear)
3860| * @param sessionId Optional The session id to use as hook input
3861| * @param agentType Optional The agent type (from --agent flag) running this session
3862| * @param model Optional The model being used for this session
3863| * @param signal Optional AbortSignal to cancel hook execution
3864| * @param timeoutMs Optional timeout in milliseconds for hook execution
3865| * @returns Async generator that yields progress messages and hook results
源码目录
子目录 utils/hooks/ 含 hooksConfigManager、类型定义与测试辅助。改 matcher 规则时 必须 双文件对照注释。
动手练习
- 在项目 .claude/settings.json 添加 PreToolUse echo 命令,观察 verbose 日志中 matchQuery
- 未接受 trust 时运行,确认 executeHooks 跳过日志
- 制造 exit 2 阻塞,确认 getPreToolHookBlockingMessage 文案出现在 REPL
- 对比 permissions 章 headless PermissionRequest 与 executePermissionRequestHooks 调用顺序
本章小结与延伸
utils/hooks.ts = 用户自动化与审计的总线。改 matcher 逻辑需同步 hooksConfigManager.ts。 继续学习: