Claude Code 学习站
Mingyu's Library

学习站 / 系统课程 / D8

D8 · Hooks

把「希望 Claude 记得做」变成「一定会发生」:hook 是挂在 agentic loop 固定节点上的你自己的代码,本课讲清事件、配置、退出码和三类典型用法。

前七课里,你影响 Claude 的手段全是「说给它听」:CLAUDE.md、plan、Skills。hook 换了一条路——不跟模型商量,直接在它的执行流程上插入确定性执行的 shell 命令。格式化、拦截敏感文件、完工前强制跑测试,都属于这一课。

为什么这天学这个

这课直接踩在两块已有的地基上。D3(权限模式与沙箱)讲的是静态门禁:一条工具调用「允许还是不允许」。hook 在同一条链路上给你第二种控制:在调用前后运行你的代码,能拦截、能改写、能反馈——而且一个返回 deny 的 PreToolUse hook 连 bypassPermissions 模式都拦得住,这是权限规则做不到的角度。D7(Skills)留下的那条对比线在本课收口:Skill 是「教会模型怎么做」,遵不遵守终究由模型决定;hook 是「不管模型怎么想都会发生」。

往后看,hook 会反复出现:D9(MCP)的工具同样能被 hook 匹配(mcp__服务名__工具名);D13(Headless 与 CI)里 hook 是无人值守时唯一可靠的守门机制;D14(Plugins)则把 hook 打包分发给整个团队。跳过这课,后面这三课都会缺一块。

选型提示「这个需求该用 Skill、hook、MCP 还是 Subagent?」——学完本课如果仍拿不准,去看选型专页 Tip E1:五种扩展机制怎么选

概念讲清楚

官方hook 是用户定义的 shell 命令(也可以是 HTTP 端点或 LLM prompt),Claude Code 在其生命周期的特定节点自动运行它们,给你确定性控制:某些动作一定会发生,而不是寄希望于模型「自觉想起来去做」。官方文档同时给出选型原则:能写成确定性规则的用 command hook;需要判断力的决定,可以用 type: "prompt"(单轮模型评估)或实验性的 type: "agent"(带工具的子代理校验)。

一张表分清:提示、门禁、强制

本站观点把前几课的机制放在一起对照,hook 的位置就清楚了:

机制本质谁来执行模型能忽略吗
CLAUDE.md / Skills(D4、D7)写给模型看的指示模型读了以后照做能——它是提示,不是约束
权限规则(D3)静态的 allow / ask / deny 门禁Claude Code 在调用工具时查表不能,但只能「放行或拦下」,不能附加动作
Hooks(本课)挂在生命周期节点上的你的代码Claude Code 无条件运行不能——模型根本不参与,还能拦截、改写、反馈

hook 插在 agentic loop 的哪里

官方事件按三种节奏触发:每会话一次(SessionStart / SessionEnd)、每轮一次(UserPromptSubmit、Stop)、以及 agentic loop 里每一次工具调用都触发的 PreToolUse / PostToolUse。下图是主干路径:

SessionStart UserPromptSubmit agentic loop(每次工具调用都经过) 模型决定调用工具 PreToolUse 工具执行 PostToolUse exit 2 或 deny: 工具调用被拦截, stderr 反馈给模型 没有新的工具调用,本轮回复完成 Stop Stop hook exit 2:打回去继续干活 会话终止 SessionEnd
实心强调框是 hook 事件(插入点),普通框是循环本体。PreToolUse 拦得住(工具还没跑),PostToolUse 拦不住(已经跑完,只能反馈),Stop 能把「想收工」的 Claude 打回去继续干活。

常用事件清单

官方参考文档共列出 31 个事件,下面是最常用的一批;每个事件可配 matcher 过滤(工具事件按工具名匹配,如 BashEdit|Write、正则 mcp__.*;留空则每次都触发,匹配区分大小写):

事件何时触发典型用途
SessionStart会话开始或恢复时(matcher 可区分 startup/resume/compact 等)注入上下文,压缩后补回关键信息
UserPromptSubmit你提交 prompt 后、Claude 处理前校验或拦截 prompt,附加环境信息
PreToolUse每次工具调用执行前,可拦截保护敏感文件,禁止危险命令
PermissionRequestClaude Code 准备向你弹权限询问时代替你自动批准特定请求
PostToolUse每次工具调用成功后(已执行,不可撤销)自动格式化,记录审计日志
NotificationClaude Code 发通知时(等你输入、等权限)桌面通知,不用盯终端
Stop / SubagentStopClaude(或子代理)结束本轮回复时,可打回完工校验:测试没过不许停
PreCompact / SessionEnd上下文压缩前 / 会话终止时备份转录、清理临时文件

配置写在哪里

官方hook 写在 settings 文件的 hooks 键下,三层嵌套:事件名 → matcher 分组 → 处理器数组。下面是官方的「编辑后自动跑 Prettier」示例,放进项目根的 .claude/settings.json 即生效:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

放在哪个文件决定作用范围:

位置作用范围可共享
~/.claude/settings.json你的所有项目否,仅本机
.claude/settings.json单个项目是,可提交进仓库
.claude/settings.local.json单个项目否,默认被 gitignore
企业托管策略设置整个组织是,管理员控制
插件 hooks/hooks.json插件启用期间是,随插件分发(D14)
Skill / agent 的 frontmatter该组件激活期间是,写在组件文件里

会话内输入 /hooks 可以按事件浏览当前生效的全部 hook——注意这个菜单是只读的,增删改要直接编辑 settings JSON(或让 Claude 替你改)。想一键全关,在 settings 里设 "disableAllHooks": true(托管设置里的 hook 除外)。

退出码与输出如何影响流程

官方事件触发时,Claude Code 把事件数据以 JSON 送进你脚本的 stdin(含 session_idcwdhook_event_name,工具事件还有 tool_nametool_input);你的脚本用退出码回话:

退出码含义效果
0无异议动作正常继续。对 PreToolUse 这不等于批准,正常权限流程照走;UserPromptSubmit 和 SessionStart 的 stdout 会注入 Claude 的上下文
2阻塞拦下动作,stderr 作为原因反馈给 Claude 让它调整。具体效果随事件而异:PreToolUse 拦截工具调用,Stop 阻止收工,PostToolUse 因为工具已执行只能反馈不能撤销
其他非阻塞错误动作照常执行,transcript 里显示一条 hook error 通知

需要比「拦 / 不拦」更细的控制时,exit 0 并向 stdout 打印一个 JSON 对象:PreToolUse 可返回 permissionDecision"allow"(跳过权限询问)、"deny"(拦截并把 permissionDecisionReason 告诉 Claude)或 "ask"(转给用户确认);Stop / PostToolUse 用顶层 {"decision": "block", "reason": "..."}。两种方式二选一:exit 2 时 stdout 里的 JSON 会被忽略

三个高频误区

  • 官方「exit 1 也算失败,应该会拦截吧」——不会。多数事件只有 exit 2 阻塞,exit 1 是非阻塞错误,动作照常执行。要执行策略,必须写 exit 2
  • 官方「hook 放行了就等于有权限」——方向搞反了。hook 的 deny 比权限模式更硬(bypassPermissions 下照样拦),但 hook 的 allow 压不过 settings 里的 deny 规则:hook 只能收紧,不能放宽,D3 的门禁始终在。
  • 本站观点「Stop hook 会不会把 Claude 锁死在干活循环里」——官方留了保险:输入里有 stop_hook_active 字段供脚本自查,且连续阻塞 8 次无进展后 Claude Code 会强制结束本轮。写 Stop hook 第一行就该检查这个字段。

当天能做完的实操

目标:30 分钟配好两个 hook——一个审计日志(PostToolUse),一个敏感文件拦截(PreToolUse)——并亲眼看到拦截发生。示例用 jq 解析 JSON,先确认 jq --version 有输出(macOS 用 brew install jq)。

  1. 在一个测试项目里新建 .claude/settings.json,写入两个 hook:
    {
      "hooks": {
        "PostToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "command",
                "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt"
              }
            ]
          }
        ],
        "PreToolUse": [
          {
            "matcher": "Edit|Write",
            "hooks": [
              {
                "type": "command",
                "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
              }
            ]
          }
        ]
      }
    }
  2. 创建拦截脚本 .claude/hooks/protect-files.sh(内容照抄官方示例),并执行 chmod +x .claude/hooks/protect-files.sh:
    #!/bin/bash
    INPUT=$(cat)
    FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
    
    PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
    
    for pattern in "${PROTECTED_PATTERNS[@]}"; do
      if [[ "$FILE_PATH" == *"$pattern"* ]]; then
        echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
        exit 2
      fi
    done
    
    exit 0
  3. 启动 Claude Code,输入 /hooks。预期:PostToolUse 和 PreToolUse 下各出现 1 个 hook,能看到事件、matcher、来源文件和命令。
  4. 触发日志 hook:让 Claude 随便跑条命令(比如「列出当前目录文件」),然后 cat ~/.claude/command-log.txt。预期:文件里多了刚执行的命令——hook 成功时界面上不显示任何东西,看效果才是验证方式。
  5. 触发拦截 hook:先 touch .env,再让 Claude「给 .env 加一行注释」。预期:编辑在执行前被拦下,Claude 收到脚本 stderr 里的 Blocked 原因并向你解释它换了做法或放弃。
  6. 懒人路线:官方明确说可以让 Claude 替你写 hook。把下面这段直接粘给 Claude Code,让它自己配置、自己演示:
帮我在当前项目配置两个 hook,写进 .claude/settings.json:
1. PostToolUse + matcher "Bash":用 jq 提取 .tool_input.command,追加到 ~/.claude/command-log.txt;
2. PreToolUse + matcher "Edit|Write":调用 .claude/hooks/protect-files.sh,脚本检查文件路径是否命中 .env、package-lock.json、.git/,命中就向 stderr 输出原因并 exit 2 拦截,否则 exit 0。
脚本要 chmod +x。配置完成后:先解释这份 JSON 的三层结构(事件 → matcher → 处理器),然后跑一条测试命令验证日志 hook,最后尝试编辑 .env 演示一次拦截。

验收标准

  • 能不看笔记讲清 hook 与 Skill 的本质区别(确定性执行 vs 模型自觉),以及 hook 与 D3 权限规则的关系(deny 更硬、allow 不能越权)。
  • 能为需求对号入座:自动格式化用哪个事件?保护敏感文件用哪个?「测试没过不许收工」用哪个?(PostToolUse / PreToolUse / Stop)
  • 实操两个 hook 都验证过:命令日志有新行;.env 编辑被拦截且 Claude 收到了原因;/hooks 里能找到它们。
  • 自测题:PreToolUse hook 脚本以 exit 1 退出,工具调用会被拦截吗?exit 0 呢?
自测题答案都不会拦截。只有 exit 2(或 exit 0 + JSON 返回 permissionDecision: "deny")才拦截;exit 1 是非阻塞错误,动作照常执行;exit 0 表示「无异议」,调用继续走正常权限流程——不等于批准。答不上来就回读「退出码与输出」一节。