机制专题 · 05

Claude Code Hooks 机制

Hook 在会话、工具、权限、压缩、子代理、配置变化等节点,把结构化 JSON 交给外部脚本或服务。模型仍负责规划;处理器可以记录、注入上下文、改写输入、给出权限结论或阻断动作。

材料

事件表以 2.1.233-appjs hook-events.txt 为准(31 个)。执行链路按 utils/hooks.tsschemas/hooks.tsservices/tools/toolHooks.ts 讲。官方对照:code.claude.com/docs/en/hooks

1. Hook 在系统里的定位#

Claude Code 的 hook 不是 React hook,而是一套“运行时生命周期拦截器”:在会话、用户提交、工具调用、权限弹窗、压缩、子代理、配置/环境变化等节点,将结构化 JSON 传给外部处理器;处理器可以记录、注入上下文、改写输入、给权限结论、阻断当前动作或让模型继续工作。核心源码入口:

文件作用
schemas/hooks.tssettings.json 中 hook 配置的 Zod schema:command / prompt / agent / http 四类可持久化 hook。
entrypoints/sdk/coreSchemas.ts所有 hook event、输入 payload、JSON 输出 schema 的 SDK 侧定义。
utils/hooks.tshook 运行时主实现:匹配、去重、并发执行、stdin/stdout 协议、退出码、JSON 输出、async registry、权限结果聚合。
services/tools/toolHooks.ts工具执行前后 hook 与权限系统的衔接。
utils/hooks/hooksConfigSnapshot.ts启动时快照、托管策略、禁用/managed-only 规则。
utils/hooks/AsyncHookRegistry.tsasync hook 后台进程注册、完成轮询、输出收集。
utils/hooks/execHttpHook.tsHTTP 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 类型:

类型配置字段执行方式典型用途
commandcommand, shell, timeout, async, asyncRewake, if, statusMessage, oncespawn shell,hook input 写入 stdin,读 stdout/stderr本地脚本、格式化、审计、安全拦截
httpurl, headers, allowedEnvVars, timeout, ifPOST JSON body 到远端,响应必须是 hook JSON中央审计、企业策略服务、Slack/PagerDuty/webhook
promptprompt, model, timeout, if用小模型做一次 JSON schema 判断,返回 {ok, reason}轻量语义验证,Stop/SubagentStop 检查
agentprompt, model, timeout, if启一个带工具的 hook agent,必须用 StructuredOutput 返回 {ok, reason}需要读文件/检索/多步验证的 gate

callbackfunction 不在 settings schema 里,主要是 SDK/内部运行时注入。

matcher 与 if

matcher 是 matcher group 级别的粗过滤。源码规则:空/缺省/* 全匹配;只含字母数字、_| 时精确匹配或 | 分隔的多精确匹配(如 Edit|Write);含其它字符按 JS 正则(如 mcp__memory__.*);工具名会做 legacy tool name normalize。

if 是单个 hook 级别的细过滤,使用权限规则语法(如 Bash(git *)Edit(*.ts))。源码只为 PreToolUsePostToolUsePostToolUseFailurePermissionRequest 准备 if 条件匹配器;对其它事件写 if 会被跳过。注意当前源码没有把 PermissionDenied 纳入 if 匹配器。

3. 2.1.233 的 HookEvent#

2.1.233 制品里 31 个事件。相对恢复快照多了 UserPromptExpansionPostToolBatchDirectoryAddedMessageDisplay

事件触发时机matcher 字段关键能力
PreToolUse工具执行前tool_name可阻断、可给权限结论、可改写 tool_input、可加上下文。
PermissionRequest权限弹窗即将出现tool_name可程序化 allow/deny;allow 可带 updatedInput / updatedPermissions
PermissionDeniedauto mode classifier 拒绝后tool_name可返回 retry: true 提示模型可重试。
PostToolUse工具成功后tool_name可加上下文;MCP 工具可改写 updatedMCPToolOutput;不能撤销副作用。
PostToolUseFailure工具失败后tool_name可把错误诊断作为额外上下文反馈给模型。
UserPromptSubmit用户提交后、模型处理前可阻断 prompt、注入上下文、做 prompt 审计/改写辅助。
UserPromptExpansion展开 slash / prompt 命令时可阻断、可 preventContinuation、可加 additionalContext
PostToolBatch一批工具跑完后可加 additionalContext
SessionStartsession startup/resume/clear/compactsource可加载初始上下文、写 CLAUDE_ENV_FILE、返回 initialUserMessage、注册 watchPaths。
Setup--init-only--init/--maintenance 的 headless setuptrigger一次性准备,支持上下文注入和 env 文件。
StopClaude 准备结束当前回复时可用 exit 2 或 JSON 阻止结束,让模型继续;payload 有 stop_hook_activelast_assistant_message
StopFailure当前 turn 因 API 错误结束errorfire-and-forget;输出和退出码忽略。
SubagentStartAgent tool 启动子代理agent_type给子代理注入上下文。
SubagentStop子代理准备结束agent_type类似 Stop,但反馈给子代理;payload 有 agent_transcript_path
PreCompactcompact 前triggerstdout 可作为额外 compact instruction;可阻断 compact。
PostCompactcompact 后trigger可展示 compact 后提示/审计。
SessionEnd会话结束reason清理、审计;默认超时更短(源码默认 1500ms,可由 env 覆盖)。
NotificationClaude Code 发通知notification_type外部通知、TTS、日志。
TeammateIdleteam teammate 即将 idle可阻止 idle,让 teammate 继续工作。
TaskCreatedTaskCreate 创建任务时可阻断任务创建。TaskCreate 受任务工具门控,见 Todo 门控
TaskCompletedTask 标记完成时可阻断完成。
ElicitationMCP server 请求用户输入mcp_server_name可自动 accept/decline/cancel,跳过 UI。
ElicitationResult用户响应 MCP elicitation 后mcp_server_name可观察或覆盖发回 MCP server 的响应。
ConfigChangesettings/skills 等配置变更source审计或阻断配置热更新;policy settings 变更不能被 hook 阻断。
InstructionsLoadedCLAUDE.md / rules 加载进上下文load_reason观测/审计;不支持阻断。
WorktreeCreate创建隔离 worktree可替换默认 git worktree 创建逻辑,输出绝对路径。
WorktreeRemove删除 worktree自定义清理。
CwdChangedcwd 改变后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 解析;否则作为普通成功输出。
2blocking。生成 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 会设置 preventContinuationdecision: "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()

  1. 检查全局禁用:disableAllHooksCLAUDE_CODE_SIMPLE
  2. interactive 模式下检查 workspace trust;未 trust 不运行任何 hook。
  3. 从 snapshot、registered hooks、session hooks 合并配置。
  4. 按 event 的 matcher 字段做粗匹配。
  5. 对 command/prompt/agent/http 做去重:command key 含 shell + command + if;http key 含 url + if;plugin/skill 用 root namespace 防止跨插件误去重。
  6. 对有 if 的 hook 做细匹配。
  7. 发 progress message。
  8. 同批匹配到的 hook 并发运行,每个 hook 有自己的 timeout/abort signal。
  9. 聚合结果:阻断、systemMessage、additionalContext、updatedInput、permissionBehavior、watchPaths、elicitation response 等。
  10. 记录 analytics / OTel / stats。

这意味着:多个 hook 默认并行,不能假设 settings 数组里的前一个 hook 已经修改了下一个 hook 的输入。若需要顺序依赖,应写成一个 dispatcher 脚本。

7. Tool hook 与权限系统的细节#

7.1 PreToolUse

executePreToolHooks() 在工具真正执行前构造 input。services/tools/toolHooks.ts 将结果接入权限流:

妙用:自动给危险命令加 --dry-run 或替换成安全 wrapper;拦截 .env、生产数据库、rm -rfgit 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#

StopSubagentStop 是最适合做“完成前验收”的点。payload 包含:stop_hook_active(是否已在 stop hook 递归中)、last_assistant_message(不用读 transcript 就能检查最后回复);SubagentStop 额外有 agent_transcript_pathagent_idagent_type。用法:

源码里 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

适合 async 的场景:日志、备份、通知、远端审计。不要把安全阻断、权限 allow/deny 这类必须同步生效的逻辑做 async。

10. 环境注入:CLAUDE_ENV_FILE、CwdChanged、FileChanged#

SessionStartSetupCwdChangedFileChanged 的 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 中 FileChangedmatcher 是当前 cwd 下要 watch 的文件名,可用 | 分隔(如 .envrc|.env);hook 输出可返回 watchPaths 动态扩展 watch list;文件变化后执行 executeFileChangedHooks(file_path, event) 并可更新 env。这是官方文档提到的 direnv 类玩法在源码中的实现。

11. HTTP hook 安全边界#

execHttpHook.ts 的安全设计比较完整:

这使 HTTP hook 更适合企业集中策略,但仍需要把 URL allowlist 和 secret env allowlist 配好。

12. 策略与信任#

源码里的防护点:

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 安全拦截

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 将 PreToolUsePermissionRequestConfigChangeInstructionsLoaded 发到中央服务,用 allowlist 与 env allowlist 控制出网和 secret 暴露。

14.7 reactive environment

CwdChanged + FileChanged 模拟 direnv:目录变化或 .envrc 改动后更新 CLAUDE_ENV_FILE。适合 monorepo 每个 package 自动切 node/python toolchain。

15. 实践建议#

  1. 能用 matcher/if 缩小范围就不要写大而全 hook。糟糕 scope 会让 agent 明显变慢。
  2. 阻断类逻辑保持同步;日志/通知/备份用 async。
  3. 多 hook 并发执行,不要依赖数组顺序;有顺序依赖写 dispatcher。
  4. 对 PreToolUse 的 allow 不要误解为最高权限;源码仍让 settings deny/ask 规则覆盖。
  5. HTTP hook 必配 URL allowlist 和 env allowlist,避免项目 hook 外发敏感信息。
  6. Stop hook 要利用 stop_hook_active 防止无限“继续工作”。
  7. PostToolUse 已经在副作用之后,适合反馈和补救,不适合防护灾难动作;防护应放 PreToolUse/PermissionRequest。
  8. 对昂贵检查优先用 if(如 Bash(npm run deploy*)),而不是在每个 Bash 调用里启动脚本后再判断。
  9. 写 JSON 输出时一定包含正确的 hookSpecificOutput.hookEventName,源码会做事件名校验。
  10. command hook stdout 若以 { 开头但不是合法 hook JSON,会被当作 JSON validation error;普通文本输出不要以 { 开头。

16. 与官方/社区资料对照#

本次 web search 参考:

事件列表以 2.1.233-appjs 的 hook-events.txt 为准。recovered 快照少了部分后来加上的事件。