Claude Code Hooks 机制
Hook 在会话、工具、权限、压缩、子代理、配置变化等节点,把结构化 JSON 交给外部脚本或服务。模型仍负责规划;处理器可以记录、注入上下文、改写输入、给出权限结论或阻断动作。
事件表以 2.1.233-appjs hook-events.txt 为准(31 个)。执行链路按 utils/hooks.ts、schemas/hooks.ts、services/tools/toolHooks.ts 讲。官方对照:code.claude.com/docs/en/hooks。
1. Hook 在系统里的定位#
Claude Code 的 hook 不是 React hook,而是一套“运行时生命周期拦截器”:在会话、用户提交、工具调用、权限弹窗、压缩、子代理、配置/环境变化等节点,将结构化 JSON 传给外部处理器;处理器可以记录、注入上下文、改写输入、给权限结论、阻断当前动作或让模型继续工作。核心源码入口:
| 文件 | 作用 |
|---|---|
schemas/hooks.ts | settings.json 中 hook 配置的 Zod schema:command / prompt / agent / http 四类可持久化 hook。 |
entrypoints/sdk/coreSchemas.ts | 所有 hook event、输入 payload、JSON 输出 schema 的 SDK 侧定义。 |
utils/hooks.ts | hook 运行时主实现:匹配、去重、并发执行、stdin/stdout 协议、退出码、JSON 输出、async registry、权限结果聚合。 |
services/tools/toolHooks.ts | 工具执行前后 hook 与权限系统的衔接。 |
utils/hooks/hooksConfigSnapshot.ts | 启动时快照、托管策略、禁用/managed-only 规则。 |
utils/hooks/AsyncHookRegistry.ts | async hook 后台进程注册、完成轮询、输出收集。 |
utils/hooks/execHttpHook.ts | HTTP hook 的 POST、header env 插值、URL allowlist、SSRF guard。 |
components/hooks/* | /hooks TUI 菜单。这个快照里菜单偏只读,提示直接改 settings.json。 |
2. 配置模型#
settings 中的形状:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh",
"timeout": 30,
"statusMessage": "checking shell command"
}
]
}
]
}
}
配置来源按源码可分为:用户 ~/.claude/settings.json、项目 .claude/settings.json、本地项目 .claude/settings.local.json、托管策略 policySettings、plugin hook(hooks/hooks.json)、skill/agent frontmatter(注册为 session hook)、SDK/内部 callback(getRegisteredHooks())、session function hook(内存里的 TS callback)。
源码中的持久化 hook 类型:
| 类型 | 配置字段 | 执行方式 | 典型用途 |
|---|---|---|---|
command | command, shell, timeout, async, asyncRewake, if, statusMessage, once | spawn shell,hook input 写入 stdin,读 stdout/stderr | 本地脚本、格式化、审计、安全拦截 |
http | url, headers, allowedEnvVars, timeout, if | POST JSON body 到远端,响应必须是 hook JSON | 中央审计、企业策略服务、Slack/PagerDuty/webhook |
prompt | prompt, model, timeout, if | 用小模型做一次 JSON schema 判断,返回 {ok, reason} | 轻量语义验证,Stop/SubagentStop 检查 |
agent | prompt, model, timeout, if | 启一个带工具的 hook agent,必须用 StructuredOutput 返回 {ok, reason} | 需要读文件/检索/多步验证的 gate |
callback 与 function 不在 settings schema 里,主要是 SDK/内部运行时注入。
matcher 与 if
matcher 是 matcher group 级别的粗过滤。源码规则:空/缺省/* 全匹配;只含字母数字、_、| 时精确匹配或 | 分隔的多精确匹配(如 Edit|Write);含其它字符按 JS 正则(如 mcp__memory__.*);工具名会做 legacy tool name normalize。
if 是单个 hook 级别的细过滤,使用权限规则语法(如 Bash(git *)、Edit(*.ts))。源码只为 PreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest 准备 if 条件匹配器;对其它事件写 if 会被跳过。注意当前源码没有把 PermissionDenied 纳入 if 匹配器。
3. 2.1.233 的 HookEvent#
2.1.233 制品里 31 个事件。相对恢复快照多了 UserPromptExpansion、PostToolBatch、DirectoryAdded、MessageDisplay。
| 事件 | 触发时机 | matcher 字段 | 关键能力 |
|---|---|---|---|
PreToolUse | 工具执行前 | tool_name | 可阻断、可给权限结论、可改写 tool_input、可加上下文。 |
PermissionRequest | 权限弹窗即将出现 | tool_name | 可程序化 allow/deny;allow 可带 updatedInput / updatedPermissions。 |
PermissionDenied | auto mode classifier 拒绝后 | tool_name | 可返回 retry: true 提示模型可重试。 |
PostToolUse | 工具成功后 | tool_name | 可加上下文;MCP 工具可改写 updatedMCPToolOutput;不能撤销副作用。 |
PostToolUseFailure | 工具失败后 | tool_name | 可把错误诊断作为额外上下文反馈给模型。 |
UserPromptSubmit | 用户提交后、模型处理前 | 无 | 可阻断 prompt、注入上下文、做 prompt 审计/改写辅助。 |
UserPromptExpansion | 展开 slash / prompt 命令时 | 无 | 可阻断、可 preventContinuation、可加 additionalContext。 |
PostToolBatch | 一批工具跑完后 | 无 | 可加 additionalContext。 |
SessionStart | session startup/resume/clear/compact | source | 可加载初始上下文、写 CLAUDE_ENV_FILE、返回 initialUserMessage、注册 watchPaths。 |
Setup | --init-only、--init/--maintenance 的 headless setup | trigger | 一次性准备,支持上下文注入和 env 文件。 |
Stop | Claude 准备结束当前回复时 | 无 | 可用 exit 2 或 JSON 阻止结束,让模型继续;payload 有 stop_hook_active 与 last_assistant_message。 |
StopFailure | 当前 turn 因 API 错误结束 | error | fire-and-forget;输出和退出码忽略。 |
SubagentStart | Agent tool 启动子代理 | agent_type | 给子代理注入上下文。 |
SubagentStop | 子代理准备结束 | agent_type | 类似 Stop,但反馈给子代理;payload 有 agent_transcript_path。 |
PreCompact | compact 前 | trigger | stdout 可作为额外 compact instruction;可阻断 compact。 |
PostCompact | compact 后 | trigger | 可展示 compact 后提示/审计。 |
SessionEnd | 会话结束 | reason | 清理、审计;默认超时更短(源码默认 1500ms,可由 env 覆盖)。 |
Notification | Claude Code 发通知 | notification_type | 外部通知、TTS、日志。 |
TeammateIdle | team teammate 即将 idle | 无 | 可阻止 idle,让 teammate 继续工作。 |
TaskCreated | TaskCreate 创建任务时 | 无 | 可阻断任务创建。TaskCreate 受任务工具门控,见 Todo 门控。 |
TaskCompleted | Task 标记完成时 | 无 | 可阻断完成。 |
Elicitation | MCP server 请求用户输入 | mcp_server_name | 可自动 accept/decline/cancel,跳过 UI。 |
ElicitationResult | 用户响应 MCP elicitation 后 | mcp_server_name | 可观察或覆盖发回 MCP server 的响应。 |
ConfigChange | settings/skills 等配置变更 | source | 审计或阻断配置热更新;policy settings 变更不能被 hook 阻断。 |
InstructionsLoaded | CLAUDE.md / rules 加载进上下文 | load_reason | 观测/审计;不支持阻断。 |
WorktreeCreate | 创建隔离 worktree | 无 | 可替换默认 git worktree 创建逻辑,输出绝对路径。 |
WorktreeRemove | 删除 worktree | 无 | 自定义清理。 |
CwdChanged | cwd 改变后 | 无 | 写 CLAUDE_ENV_FILE,动态更新 FileChanged watchPaths。 |
FileChanged | 被 watch 的文件变化 | basename(file_path) | reactive env,如 .envrc/.env 变化后更新 shell env。 |
DirectoryAdded | /add-dir 或 SDK register_repo_root 注册新工作目录后 | 无 | 新工作目录进会话。 |
MessageDisplay | 展示 assistant 消息时 | 无 | 可转换或隐藏消息文本。 |
4. 输入协议#
所有 hook input 都带基础字段:
{
"session_id": "...",
"transcript_path": "...",
"cwd": "...",
"permission_mode": "default|acceptEdits|bypassPermissions|plan|dontAsk|auto",
"agent_id": "... optional, subagent only",
"agent_type": "... optional"
}
不同事件追加自己的字段,例如:
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm test" },
"tool_use_id": "toolu_...",
"session_id": "...",
"transcript_path": "...",
"cwd": "/repo"
}
命令 hook 通过 stdin 收到这个 JSON,HTTP hook 通过 POST body 收到同样 JSON。源码用 jsonStringify(hookInput) 一次性序列化,所有同批 hook 共享。
5. 输出协议:退出码 + stdout JSON#
5.1 通用退出码
| 退出码 | 源码行为 |
|---|---|
0 | 成功。stdout 如果是 JSON 则按 hook JSON schema 解析;否则作为普通成功输出。 |
2 | blocking。生成 blockingError,不同事件的上层消费者决定“阻断工具 / 给模型反馈 / 阻止停止 / 阻止 compact”。 |
| 其它非零 | non-blocking error。stderr 展示给用户或 debug,主流程通常继续。 |
官方文档也把 exit code 2 称为阻断控制点:PreToolUse 可阻断工具,Stop 可迫使 Claude 继续工作,UserPromptSubmit 可阻断用户 prompt。
5.2 JSON 输出顶层字段
stdout 以 { 开头时,源码尝试解析 hook JSON:
{
"continue": true,
"suppressOutput": false,
"stopReason": "why stop when continue=false",
"decision": "approve|block",
"reason": "human/model visible reason",
"systemMessage": "warning shown to user/model as hook_system_message",
"hookSpecificOutput": { "hookEventName": "..." }
}
要点:continue: false 会设置 preventContinuation;decision: "approve" 映射成 permission allow,"block" 映射成 deny + blockingError;hookSpecificOutput.hookEventName 必须等于当前事件,源码会校验;HTTP hook 响应必须是 JSON,空 body 当作 {},非 2xx / 超时 / 连接错误是 non-blocking error,真正阻断要返回 2xx + JSON decision: "block"。
5.3 事件特定输出
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "...",
"updatedInput": { "command": "npm test -- --runInBand" },
"additionalContext": "Tell Claude why this input was adjusted."
}
}
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Lint failed; ask Claude to fix these files.",
"updatedMCPToolOutput": { "...": "MCP only" }
}
}
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedInput": { "file_path": "/safe/path" },
"updatedPermissions": []
}
}
}
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Repo state summary...",
"initialUserMessage": "Run /doctor first",
"watchPaths": ["/repo/.env"]
}
}
{
"hookSpecificOutput": {
"hookEventName": "Elicitation",
"action": "accept",
"content": { "answer": "..." }
}
}
6. 执行链路源码解读#
主链路在 executeHooks():
- 检查全局禁用:
disableAllHooks、CLAUDE_CODE_SIMPLE。 - interactive 模式下检查 workspace trust;未 trust 不运行任何 hook。
- 从 snapshot、registered hooks、session hooks 合并配置。
- 按 event 的 matcher 字段做粗匹配。
- 对 command/prompt/agent/http 做去重:command key 含 shell + command + if;http key 含 url + if;plugin/skill 用 root namespace 防止跨插件误去重。
- 对有
if的 hook 做细匹配。 - 发 progress message。
- 同批匹配到的 hook 并发运行,每个 hook 有自己的 timeout/abort signal。
- 聚合结果:阻断、systemMessage、additionalContext、updatedInput、permissionBehavior、watchPaths、elicitation response 等。
- 记录 analytics / OTel / stats。
这意味着:多个 hook 默认并行,不能假设 settings 数组里的前一个 hook 已经修改了下一个 hook 的输入。若需要顺序依赖,应写成一个 dispatcher 脚本。
7. Tool hook 与权限系统的细节#
7.1 PreToolUse
executePreToolHooks() 在工具真正执行前构造 input。services/tools/toolHooks.ts 将结果接入权限流:
blockingError→ 最终权限 decision 变成 deny。permissionDecision: "allow"→ 可跳过交互式权限弹窗,但源码仍会再跑 settings deny/ask 规则;hook allow 不能覆盖显式 deny/ask 规则。permissionDecision: "ask"→ 强制进入权限弹窗,并把 hook reason 作为提示。updatedInput:配合 allow/ask 时随权限结果传递;没有 permission decision 时也可 passthrough 改写工具输入,继续正常权限流程。- 多个 hook 的 permission precedence:
deny > ask > allow。
妙用:自动给危险命令加 --dry-run 或替换成安全 wrapper;拦截 .env、生产数据库、rm -rf、git push --force;对 Bash 子命令用 if: "Bash(npm publish*)" 精确触发,避免每个 ls 都 spawn 慢脚本。
7.2 PermissionRequest
这个事件比 PreToolUse 更贴近“用户即将被打断”的时刻。hook 可以自动 allow/deny 并带 updatedPermissions。适合团队内安全策略、CI/headless 自动审批、低风险工具自动放行。
7.3 PostToolUse / PostToolUseFailure
PostToolUse 发生在工具副作用之后,不能撤销已经发生的写文件/命令执行。但它可以:把 lint/test/format 结果通过 additionalContext 反馈给 Claude;对 MCP tool output 做 updatedMCPToolOutput 替换;用 exit 2 或 continue:false 让模型收到反馈并继续修复,但这不是“回滚工具”。PostToolUseFailure 适合错误分类、自动给失败命令补诊断上下文。
8. Stop / SubagentStop:完成条件 gate#
Stop 与 SubagentStop 是最适合做“完成前验收”的点。payload 包含:stop_hook_active(是否已在 stop hook 递归中)、last_assistant_message(不用读 transcript 就能检查最后回复);SubagentStop 额外有 agent_transcript_path、agent_id、agent_type。用法:
- command hook exit 2,把 stderr 作为 “Stop hook feedback” 给模型,让模型继续。
- prompt hook 返回
{"ok": false, "reason": "tests were not run"},源码会转成 blocking +preventContinuation。 - agent hook 启一个小 agent 读取 transcript / grep 文件,做更强验收。
源码里 prompt/agent hook 都强制结构化输出 {ok:boolean, reason?:string}。agent hook 还会过滤掉不适合 agent 内部使用的工具,并限制最大 turn,防止 hook 自己失控。
9. Async hook 与 asyncRewake#
command hook 有两种 async 方式:配置字段 "async": true 或 "asyncRewake": true;脚本第一行 stdout 输出 {"async": true, "asyncTimeout": 15000}。源码要求“第一行”是 async JSON;如果进程一次性输出多行,源码只解析第一行。async 后台进程进入 AsyncHookRegistry:
- 运行时不阻塞主流程。
- 完成后扫描 stdout,寻找第一条非 async JSON 作为最终 sync response。
asyncTimeout在这个快照里按毫秒使用。asyncRewake特殊:不走普通 registry,进程完成时若 exit code 2,会把 blocking feedback 作为task-notification入队,唤醒/打断模型继续处理。
适合 async 的场景:日志、备份、通知、远端审计。不要把安全阻断、权限 allow/deny 这类必须同步生效的逻辑做 async。
10. 环境注入:CLAUDE_ENV_FILE、CwdChanged、FileChanged#
SessionStart、Setup、CwdChanged、FileChanged 的 bash command hook 会收到 CLAUDE_ENV_FILE 环境变量。hook 可把 shell export 写入该文件:
cat >> "$CLAUDE_ENV_FILE" <<'EOF'
export FOO=bar
export PATH="$PWD/node_modules/.bin:$PATH"
EOF
后续 BashTool 执行时,getSessionEnvironmentScript() 会把 session-env 目录下的 hook env 文件按优先级拼起来:
setup -> sessionstart -> cwdchanged -> filechanged
CwdChanged 变化时会清空 cwd/filechanged env 文件,重新运行相关 hook。FileChanged 的 watch 逻辑:settings 中 FileChanged 的 matcher 是当前 cwd 下要 watch 的文件名,可用 | 分隔(如 .envrc|.env);hook 输出可返回 watchPaths 动态扩展 watch list;文件变化后执行 executeFileChangedHooks(file_path, event) 并可更新 env。这是官方文档提到的 direnv 类玩法在源码中的实现。
11. HTTP hook 安全边界#
execHttpHook.ts 的安全设计比较完整:
- POST
Content-Type: application/json。 allowedHttpHookUrls:URL allowlist,undefined表示不限制,空数组表示全部禁止,支持*通配。- header 值支持
$VAR/${VAR}插值,但只有 hook 的allowedEnvVars与全局httpHookAllowedEnvVars交集中的变量会被解析;其它变量变空字符串。 - header value 会移除 CR/LF/NUL,防止 header injection。
- 默认禁用 axios redirect(
maxRedirects: 0)。 - 没有 sandbox/env proxy 时走
ssrfGuardedLookup,阻止私网/link-local SSRF,但允许 loopback 方便本地开发。 - sandbox 开启时通过 sandbox network proxy,由 proxy 强制网络 allowlist。
这使 HTTP hook 更适合企业集中策略,但仍需要把 URL allowlist 和 secret env allowlist 配好。
12. 策略与信任#
源码里的防护点:
- interactive 模式下所有 hook 都要求 workspace trust;未接受 trust dialog 不运行,防止
.claude/settings.jsonRCE。 - non-interactive/SDK 模式 trust 隐含成立。
- policySettings
disableAllHooks: true会禁用所有 hook,包括 managed。 - 非 managed settings 的
disableAllHooks: true不能禁用 managed hooks,而是使运行时进入 managed-only。 - policySettings
allowManagedHooksOnly: true只使用托管 hook;用户/项目/local/plugin/session hook 被跳过。 strictPluginOnlyCustomization会阻止用户/项目/local settings hook,但保留 managed/plugin 合法入口。CLAUDE_CODE_SIMPLE会直接跳过 hook。- plugin hook 执行前检查 pluginRoot 是否存在,避免“插件目录丢失导致脚本 exit 2,被误判为故意阻断”。
13. /hooks 菜单#
recovered/src/components/hooks/* 显示这个快照里的 /hooks 菜单主要是只读浏览器:选择 event、查看 matcher、查看 hook 详情、统计来源(user/project/local/plugin/session/builtin);如果 hooks 被禁用或 managed-only 会显示提示。修改/新增/删除不在菜单里做,UI 文案提示编辑 settings.json 或让 Claude 帮你改。
14. 常见妙用模式#
14.1 安全拦截
PreToolUse+matcher: "Bash"+if: "Bash(rm *)"。- 对
.env/ SSH key / production config 的Read、Edit、Write做 deny。 - 对
git push --force、npm publish、terraform apply强制 ask。
14.2 自动修正输入
PreToolUse 返回 updatedInput,给命令注入 env、修正路径、追加 dry-run、替换危险参数。比阻断后让模型重试更平滑,模型甚至可以不知道输入被修正。
14.3 自动格式化与反馈
PostToolUse 匹配 Edit|Write,运行 prettier/eslint/go fmt。若格式化失败,返回 additionalContext 或 exit 2,让 Claude 继续修复。
14.4 完成前验收
Stop prompt hook 检查“是否修改了测试但没运行测试”;Stop agent hook 读取 transcript 与 git diff 确认 checklist;SubagentStop 对子代理产物做独立验收。
14.5 会话启动上下文
SessionStart 拉取 git status、近期 issue、CI 状态返回 additionalContext;返回 initialUserMessage 可自动引导第一步。
14.6 企业集中审计
HTTP hook 将 PreToolUse、PermissionRequest、ConfigChange、InstructionsLoaded 发到中央服务,用 allowlist 与 env allowlist 控制出网和 secret 暴露。
14.7 reactive environment
CwdChanged + FileChanged 模拟 direnv:目录变化或 .envrc 改动后更新 CLAUDE_ENV_FILE。适合 monorepo 每个 package 自动切 node/python toolchain。
15. 实践建议#
- 能用 matcher/if 缩小范围就不要写大而全 hook。糟糕 scope 会让 agent 明显变慢。
- 阻断类逻辑保持同步;日志/通知/备份用 async。
- 多 hook 并发执行,不要依赖数组顺序;有顺序依赖写 dispatcher。
- 对 PreToolUse 的 allow 不要误解为最高权限;源码仍让 settings deny/ask 规则覆盖。
- HTTP hook 必配 URL allowlist 和 env allowlist,避免项目 hook 外发敏感信息。
- Stop hook 要利用
stop_hook_active防止无限“继续工作”。 - PostToolUse 已经在副作用之后,适合反馈和补救,不适合防护灾难动作;防护应放 PreToolUse/PermissionRequest。
- 对昂贵检查优先用
if(如Bash(npm run deploy*)),而不是在每个 Bash 调用里启动脚本后再判断。 - 写 JSON 输出时一定包含正确的
hookSpecificOutput.hookEventName,源码会做事件名校验。 - command hook stdout 若以
{开头但不是合法 hook JSON,会被当作 JSON validation error;普通文本输出不要以{开头。
16. 与官方/社区资料对照#
本次 web search 参考:
- 官方 Hooks reference:
https://code.claude.com/docs/en/hooks。确认 hook 是 shell/HTTP/LLM prompt,在生命周期事件触发;确认 command hook stdin、HTTP hook POST body、matcher 模式、MCP tool 命名、exit code 2 阻断等机制。事件表以 2.1.233 为准,含UserPromptExpansion、PostToolBatch、DirectoryAdded、MessageDisplay。 - 社区文章
claudefa.st:强调 exit code 2 是主要控制工具;PostToolUse 不能撤销副作用;HTTP hook/async hook 的适用边界。 - 社区文章
claudelog.com:强调 hook scope 很关键;提到 PreToolUse input modification 的用法,如自动加 dry-run、路径修正、参数校验。
事件列表以 2.1.233-appjs 的 hook-events.txt 为准。recovered 快照少了部分后来加上的事件。