Hook 系统
什么是钩子?
挂钩允许您在 Claude Code 生命周期中精确定义的点注入副作用 - 在工具运行之前、采样完成之后、会话开始时、文件更改时。 每个钩子都是一个 匹配器+命令 对存储在设置文件中(或会话挂钩的内存中)。 当事件触发时,Claude Code 找到每个匹配的钩子,运行所有钩子,然后解释退出代码和标准输出以决定是继续、阻止还是将输出反馈给模型。
钩子是主要的延伸表面。 它们支持 lint-on-save 行为、策略执行、可观察性管道、结构化验证代理等,所有这些都无需修改 Claude Code 本身。
src/entrypoints/sdk/coreTypes.ts 作为 HOOK_EVENTS 常量数组。 Hook的执行逻辑在 src/utils/hooks.ts 和个人 exec*Hook.ts 文件下 src/utils/hooks/。 配置读取自 src/utils/hooks/hooksConfigSnapshot.ts.
所有 27 个挂钩事件
事件按类别分组。 每个徽章都会显示事件名称 settings.json 以及 TypeScript 源代码中。
生命周期(会话边界)
工具执行
代理及分代理
Compaction
许可与政策
协作/多代理
文件系统和环境
MCP 启发
钩子如何发射
下图展示了单个指令的执行路径 PreToolUse 事件。 其他事件遵循相同的形状:收集事件的钩子→按顺序运行匹配器→聚合结果→决定。
退出代码2/非零分割是中心设计:退出2是模型可见阻塞; 其他非零是用户可见的噪声。
退出代码语义
每个事件定义其自己的退出代码语义。 以下是跨事件摘要。 阅读每个事件的完整描述 hooksConfigManager.ts → getHookEventMetadata().
PreToolUse 退出代码
| 退出代码 | Effect |
|---|---|
| 0 | 未显示标准输出/标准错误; 工具收益 |
| 2 | 显示给模型的 stderr; 工具调用被阻止 |
| other | 仅向用户显示 stderr; 工具收益 |
PostTool使用退出代码
| 退出代码 | Effect |
|---|---|
| 0 | 以转录模式显示的标准输出 (Ctrl+O) |
| 2 | stderr 立即显示给模型 |
| other | stderr 仅显示给用户 |
停止退出代码
| 退出代码 | Effect |
|---|---|
| 0 | 未显示标准输出/标准错误; 会议结束 |
| 2 | 显示给模型的 stderr; 谈话继续 (防止停止) |
| other | 仅向用户显示 stderr; 会议结束 |
当回合因 API 错误(速率限制、身份验证失败)而结束时,StopFailure 会触发,而不是 Stop。 它是一劳永逸的——退出代码和输出被忽略。
用户提示提交退出代码
| 退出代码 | Effect |
|---|---|
| 0 | stdout 注入模型上下文(向 Claude 展示) |
| 2 | 块处理,删除原始提示,向用户显示 stderr |
| other | stderr 仅显示给用户 |
会话启动和设置退出代码
| 退出代码 | Effect |
|---|---|
| 0 | 向 Claude 显示的标准输出(会话的种子上下文) |
| 2 | 两个事件都忽略阻塞错误 |
| other | stderr 仅显示给用户 |
SessionStart 支持 source 具有值的匹配器: startup, resume, clear, compact。 安装程序支持 trigger 具有值的匹配器: init, maintenance.
PreCompact / PostCompact 退出代码
| 退出代码 | 效果(PreCompact) |
|---|---|
| 0 | 标准输出作为自定义紧凑指令附加 |
| 2 | 块压缩 |
| other | 向用户显示 stderr; 压实继续 |
PostCompact:exit 0 向用户显示标准输出; 其他退出代码仅向用户显示 stderr。
CwdChanged 和 FileChanged 退出代码
两项活动均已确定 CLAUDE_ENV_FILE — 编写 bash export 语句到该文件路径,它们将应用于后续的 BashTool 命令。 这两个事件都不支持退出代码 2 阻塞; 非零退出仅向用户显示 stderr。
文件更改还支持 hookSpecificOutput.watchPaths 在 stdout JSON 中向文件观察器动态注册其他路径。 匹配器字段用作管道分隔的文件名 glob(例如 .envrc|.env).
5种钩子命令类型
The type 判别字段选择执行引擎。 所有类型,除了 function 可以坚持到 settings.json; function 仅限会话并在 TypeScript 代码中定义。
command 外壳命令
通过配置的 shell 生成一个子进程(默认:bash / your $SHELL; 也支持 powershell)。 在 stdin 上接收 JSON 格式的钩子输入。 stdout/stderr 解释遵循上面的退出代码。
关键选项: if, timeout, once, async, asyncRewake, statusMessage, shell
prompt 法学硕士提示
向 LLM 发送提示(默认:小型快速模型)。 提示使用 $ARGUMENTS 作为 JSON 挂钩输入的占位符。 模型必须响应 {"ok": true} or {"ok": false, "reason": "..."}。 强制 JSON 模式输出模式确保可解析的响应。
关键选项: if, timeout (默认30秒), model, once, statusMessage
agent 代理验证器
启动完整的多回合子代理(最多 50 回合),可以访问所有工具。 代理读取通过系统提示符注入的路径处的对话记录,然后调用 StructuredOutput 返回工具 {"ok": true/false, "reason": "..."}。 不允许的工具(AgentTool、计划模式)将被过滤掉以防止递归。
关键选项: if, timeout (默认 60 秒), model, once, statusMessage
http HTTP POST
将挂钩输入 JSON 发布到配置的 URL。 响应由调用者解释。 支持标头值中的环境变量插值(仅在 allowedEnvVars 已解决)。 受到 SSRF 防护的保护,该防护会阻止私有/本地链路 IP 范围; 故意允许环回(127.x)。
关键选项: if, timeout (默认 10 分钟), headers, allowedEnvVars, once, statusMessage
function TypeScript 回调
通过编程方式注册的进程内 TypeScript 函数 addFunctionHook()。 返回一个布尔值或 Promise<boolean>。 仅限会话范围 - 无法持久化到 settings.json。 由技能改进系统和结构化输出执行在内部使用。
关键选项: id (以便稍后删除), timeout (默认5秒), errorMessage
prompt hook 查询模型 queryModelWithoutStreaming 并使用 JSON 模式输出模式强制可解析响应。 它不会触发 UserPromptSubmit hooks——这将是无限递归。 同样的模式适用于 agent hooks.
配置源和优先级
Hooks 可以有六个来源。 当同一个事件+匹配器有来自多个源的钩子时,它们会被合并并全部运行; 优先顺序(从 SOURCES in settings/constants.ts) 确定显示顺序 /hooks,但所有钩子都会执行。
| Priority | 来源名称 | 文件路径 | Scope |
|---|---|---|---|
| 1 | userSettings |
~/.claude/settings.json |
该用户的所有项目 |
| 2 | projectSettings |
.claude/settings.json (项目根) |
所有参与此存储库工作的人 |
| 3 | localSettings |
.claude/settings.local.json |
仅您的机器+此项目 |
| 4 | policySettings |
MDM/托管配置(只读) | 企业管理员强制执行 |
| 5 | pluginHook |
~/.claude/plugins/*/hooks/hooks.json |
插件安装的钩子 |
| 6 | sessionHook |
仅限内存中 | 当前会话; 退出时清除 |
allowManagedHooksOnly: true 在托管 (MDM) 设置中设置, only 托管钩子运行——用户、项目、本地和插件钩子都被抑制。 如果 disableAllHooks: true 出现在托管设置中,零挂钩运行(包括托管)。 但如果 disableAllHooks 在非托管源中,托管挂钩仍然运行 - 非托管设置无法禁用托管挂钩。
钩子如何搭配
钩子事件数组中的每个条目都是一个 HookMatcher — 具有可选属性的对象 matcher 字符串和钩子命令数组。 对于支持匹配器的事件(例如 PreToolUse 匹配于 tool_name; Notification 匹配于 notification_type; SessionStart 匹配于 source),用匹配的字符串运行钩子; 没有匹配器的钩子无条件运行。
// ~/.claude/settings.json — minimal example { "hooks": { "PreToolUse": [ { "matcher": "Write", // only fires when tool_name === "Write" "hooks": [ { "type": "command", "command": "./scripts/pre-write.sh" } ] }, { // no matcher → runs for every tool call "hooks": [ { "type": "command", "command": "./scripts/audit-log.sh" } ] } ] } } settings.json
会话挂钩
会话挂钩仅存在于内存中,附加到特定的 sessionId 存储在一个 Map<string, SessionStore>。 它们是通过以下三个函数以编程方式创建的: sessionHooks.ts:
// Adding a command/prompt/agent/http hook for this session addSessionHook(setAppState, sessionId, 'Stop', '', { type: 'command', command: './verify-output.sh' }) // Adding a TypeScript callback hook (function type) const hookId = addFunctionHook( setAppState, sessionId, 'Stop', '', // matcher (empty = all) (messages, signal) => checkCondition(messages), // callback 'Condition not met', // errorMessage if false { timeout: 5000, id: 'my-hook' } // optional ) // Removing a function hook by ID removeFunctionHook(setAppState, sessionId, 'Stop', hookId) sessionHooks.ts API
会话挂钩是 not 反应性的——他们坐在 Map (不是一个普通的对象)这样 Object.is(next, prev) 在状态更新期间返回 true 并且不会触发不必要的重新渲染。 地图发生了原地变异; 仅会话范围的数据发生变化。
来自 frontmatter 的会话挂钩
当加载技能或代理时,其 frontmatter hooks 部分通过以下方式注册为会话挂钩 registerSkillHooks() or registerFrontmatterHooks()。 这些挂钩仅在会话(或代理)处于活动状态时才存在。
细节一:代理触发 SubagentStop, 不是 Stop。 这 registerFrontmatterHooks() 函数自动转换任何 Stop 代理的 frontmatter 中的条目 SubagentStop 所以他们会在正确的地点开火。
"once": true, registerSkillHooks() 附上一个 onHookSuccess 调用的回调 removeSessionHook() 第一次成功执行时。 这是一次性初始化模式的机制。
异步和异步唤醒
命令挂钩支持两个异步标志。 什么时候 async: true,挂钩进程启动,但 Claude 没有等待 - 对话立即继续。 该过程被跟踪在 AsyncHookRegistry 并在主循环中进行轮询。
When asyncRewake: true,挂钩在后台运行,但如果进程以代码 2 退出,则主循环会唤醒模型。这允许后台观察程序以阻塞错误中断 Claude 的下一个响应,即使挂钩本身异步运行。
{ "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "./run-tests-background.sh", "asyncRewake": true // wakes model if tests fail (exit 2) } ] } ] } asyncRewake example
注册表通过以下方式跟踪每个挂起的挂钩 processId。 当主循环通过轮询时 checkForAsyncHookResponses(),完成的钩子被最终确定,并且它们的标准输出被解析为 JSON SyncHookJSONOutput 回复。 如果没有找到,则响应对象默认为 {}.
HTTP 挂钩:安全模型
HTTP 将 POST JSON 挂钩到 URL。 安全模型分为三层:
- 网址白名单: If
allowedHttpHookUrls在合并设置中设置,仅匹配列出的 glob 模式的 URL(使用*作为通配符)是允许的。 空数组会阻止所有 HTTP 挂钩。 - 环境变量白名单: 标头值可以包含
$VAR_NAME插值,但仅限钩子中列出的变量allowedEnvVars数组已解决。 不在列表中的变量将替换为空字符串,以防止通过项目挂钩进行秘密泄露。 - SSRF守卫: DNS解析被拦截
ssrfGuardedLookup(),它会阻止私有 IP 范围(10.x、172.16-31.x、192.168.x、169.254.x、100.64-127.x)和未指定/链接本地 IPv6。 本地开发服务器特意允许环回(127.0.0.1,::1)。 当沙箱代理或 env-var 代理处于活动状态时,防护措施将被绕过(代理执行自己的 DNS;阻止代理自己的 IP 将破坏专用网络上的公司代理)。
带有 auth 标头的 HTTP 挂钩(真实代码模式)
{ "Stop": [ { "hooks": [ { "type": "http", "url": "https://api.example.com/claude-stop-event", "headers": { "Authorization": "Bearer $MY_TOKEN" }, "allowedEnvVars": ["MY_TOKEN"], "timeout": 10 } ] } ] } settings.json — http hook with env var auth
The $MY_TOKEN 标头值中的引用仅被解析,因为 MY_TOKEN 出现在 allowedEnvVars。 任何其他 $VAR 同一标头模板中的内容将默默地替换为空字符串。 标头值也会被清理以去除 CR/LF/NUL 以防止 HTTP 标头注入。
提示和代理挂钩:深入探讨
提示钩子
提示钩子使用以下命令向 LLM 发送消息 queryModelWithoutStreaming。 系统提示告诉模型以 JSON 匹配的方式响应 {"ok": boolean, "reason"?: string}。 输出格式是通过 JSON 模式输出模式强制执行的——模型不能产生任何无法解析的内容。
该钩子解析参数占位符($ARGUMENTS, $0, $ARGUMENTS[0]等)发送前。 使用的模型默认为“小型快速模型”(可通过每个钩子进行配置) model field).
如果模型返回 {"ok": false},钩子结果为 blocking 与一个 preventContinuation: true 旗帜。 如果返回的话 {"ok": true},挂钩成功。 格式错误的 JSON 或架构验证失败会产生非阻塞错误。
代理挂钩
代理钩子通过以下方式运行完整的多轮代理循环(最多 50 轮) query()。 代理获得自定义系统提示,包括对话记录的路径、对按权限过滤的所有工具的访问权限,以及 StructuredOutput 它必须调用工具才能返回结果。 结构化输出强制注册为会话级 Stop 代理循环启动之前的函数钩子,并在之后清理。
如果代理达到 50 回合而没有呼叫 StructuredOutput,结果是 cancelled — 不会向用户显示任何错误。 如果代理完成时根本没有调用该工具,则同样适用。
结构化输出执行模式(真实来源)
// hookHelpers.ts — registerStructuredOutputEnforcement addFunctionHook( setAppState, sessionId, 'Stop', '', // no matcher = all stops messages => hasSuccessfulToolCall(messages, SYNTHETIC_OUTPUT_TOOL_NAME), `You MUST call the ${SYNTHETIC_OUTPUT_TOOL_NAME} tool to complete this request. Call this tool now.`, { timeout: 5000 } ) hookHelpers.ts
该函数钩子在代理停止之前触发,检查是否成功 StructuredOutput 工具调用存在于消息历史记录中,如果不存在,则注入错误消息以强制模型在完成之前调用它。
挂钩事件系统(SDK 遥测)
除了执行挂钩之外,Claude Code 还通过单独的进程内事件总线向 SDK 使用者发出挂钩执行事件 hookEvents.ts。 这与钩子执行系统不同——它纯粹是可观察性/遥测。
三种事件类型
- started — 当钩子开始执行时发出
- progress — 在钩子运行时以轮询间隔(默认 1s)发出,携带当前的 stdout/stderr
- response — 当钩子完成时发出,带有完整的输出、退出代码和结果
始终发出的事件
无论发生什么,总是会发出两个事件 includeHookEvents SDK 选项: SessionStart and Setup。 这些在源代码中被描述为“原始允许列表中的低噪声生命周期事件,并且向后兼容”。 所有其他事件都需要 includeHookEvents: true 在 SDK 选项中或在中运行 CLAUDE_CODE_REMOTE mode.
pendingEvents 如果尚未注册处理程序(例如, SDK 使用者在前几个钩子触发后附加)。 注册处理程序后,缓冲的事件将立即刷新。
The if 过滤字段
每个持久化的钩子命令类型都支持一个可选的 if 场地。 它使用权限规则语法 - 与以下语法相同 allowedTools 模式。 示例: "Bash(git *)", "Read(*.ts)"。 仅当工具调用与模式匹配时才会生成挂钩。
{ "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "./git-safety.sh", "if": "Bash(git push*)" // only fire on git push commands } ] } ] } if field — only fires for git push
The if 字段是钩子标识的一部分:具有相同命令但不同的两个钩子 if 值被视为不同的钩子,并且在会话注册它们时运行。 这 shell 字段也是身份的一部分—— "command": "foo" with "shell": "bash" and "command": "foo" with "shell": "powershell" 是不同的钩子。
现实世界的模式
模式 1:写入时 Lint 保护 (PreToolUse)
{ "PreToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "jq -e '.tool_input.file_path | test(\"test.*\\.ts$\")' <<< \"$CLAUDE_HOOK_INPUT\" && echo 'Must write tests alongside implementation' >&2 && exit 2 || exit 0", "statusMessage": "Checking test coverage policy..." } ] } ] }
如果工具输入路径与测试文件模式不匹配,则 exit 2 会阻止写入,并向 Claude 显示错误,然后 Claude 将重新考虑。 退出 0 允许写入静默进行。
模式2:会话上下文注入(SessionStart)
{ "SessionStart": [ { "matcher": "startup", "hooks": [ { "type": "command", "command": "echo \"Today is $(date). Open PRs: $(gh pr list --json number | jq length)\"" } ] } ] }
退出 0 上的 stdout 向 Claude 显示为会话启动上下文。 该模型在第一个用户提示之前就看到了这一点。 适合注入动态环境信息(日期、PR 状态、分支、部署状态),否则需要用户手动提供上下文。
模式 3:使用代理挂钩停止验证
{ "Stop": [ { "hooks": [ { "type": "agent", "prompt": "Verify that the implementation includes unit tests and that they all pass. Read the transcript at $ARGUMENTS[transcript_path] to understand what was built, then run the tests.", "timeout": 120 } ] } ] }
代理挂钩生成一个完整的子代理,可以访问所有工具。 它可以读取文件、运行命令和检查记录。 如果返回的话 {"ok": false, "reason": "Tests failed: 3 assertions"},会话继续,克劳德必须解决故障。
模式 4:.envrc 在 cwd 更改时自动加载 (CwdChanged)
{ "CwdChanged": [ { "hooks": [ { "type": "command", "command": "[ -f .envrc ] && direnv export bash >> \"$CLAUDE_ENV_FILE\" || true" } ] } ] }
Claude Code 套 CLAUDE_ENV_FILE 到临时文件路径。 写作 export VAR=value 那里的行会导致这些环境变量应用于后续的 BashTool 命令。 此模式模仿 direnv 集成,而不运行完整的 direnv 守护进程。
模式5:基于LLM的策略检查(提示钩子)
{ "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "prompt", "prompt": "The following bash command is about to run: $ARGUMENTS\nReturn ok: true only if the command does not destructively modify production infrastructure. Common destructive patterns: terraform destroy, kubectl delete on prod namespaces, DROP TABLE, rm -rf on critical paths.", "model": "claude-sonnet-4-6" } ] } ] }
提示钩子非常适合难以用正则表达式表达的模糊策略决策。 该模型评估意图,而不仅仅是模式匹配。 使用更强大的模型(十四行诗而不是俳句)可以提高细微差别案例的准确性。
要点
HOOK_EVENTS in src/entrypoints/sdk/coreTypes.ts。 如果添加了新事件,它会首先出现在那里。 元数据(描述、匹配器字段、有效匹配器值)位于 hooksConfigManager.ts → getHookEventMetadata().command (外壳子进程)→ prompt (单一法学硕士课程)→ agent (全程多回合代理)→ http (发布到 URL)→ function (TypeScript 回调,仅限会话)。 更简单的类型具有更低的延迟并且更可预测。allowManagedHooksOnly 压制其他一切; disableAllHooks 在策略中甚至会杀死托管钩子。 非托管设置无法阻止托管挂钩。Object.is(next, prev) 返回 true 并且存储侦听器不会触发。 这在并行代理工作流程中很重要,其中在一次tick中注册了数十个函数挂钩。hookEvents.ts) 与钩子执行是分开的。 这纯粹是可观察性。 仅有的 SessionStart and Setup 事件总是被发出; 所有其他人都需要 includeHookEvents: true 或远程模式。 如果尚未附加 SDK 使用者,则最多可缓冲 100 个事件。测验
git push bash 命令,而不是其他 bash 命令。 哪个字段控制这个?disableAllHooks: true in policySettings。 插件还注册钩子。 什么运行?"asyncRewake": true 到 PostToolUse 命令挂钩。 模型什么时候会被中断?