AI 编程教程中文版
从原理到实战

10 · 怎么让操作 100% 执行

Hooks 不是提醒 Claude Code 记得做事,而是在工具调用、提示提交、停止前后设置确定性检查点。能容忍遗漏写 CLAUDE.md,不能容忍就用 Hook。

翔宇有一条工程习惯:凡是“忘一次就会出事”的规则,不要只写进提示词里。提示词让 AI 尽量记住,Hooks(钩子)让系统在关键节点强制检查。前者是提醒,后者是门禁。——翔宇

这一篇用 14 分钟换什么:第 9 篇讲 MCP 让 Claude Code 够到外部系统。够得到以后,风险也变大了。这一篇讲 Hooks:怎么在 Claude Code 准备调用工具、调用之后、提交提示、准备停止等时间点插入自动检查,让“必须发生的事”不再只靠 Claude 记得。

1. 先从一个危险的小动作开始

你在 CLAUDE.md 里写了一条规则:

永远不要修改 `.env` 文件。

这条规则有用吗?有用。Claude Code 会读到,也大概率遵守。

但“大概率”不是“保证”。

想象某一次任务里,上下文很长、文件很多、你又让它“直接修好本地配置”。Claude 准备写 .env。如果只靠 CLAUDE.md,它可能会想起规则,也可能在复杂上下文里漏掉。

.env 不是普通文件。改坏一次,可能就是数据库连接、生产 token、第三方密钥出问题。

这时你不应该再问“怎么让提示词写得更严厉”。你应该换一个机制。

第一性原理:Hooks 解决的不是“Claude 不理解规则”,而是“某些规则不能靠模型记忆来执行”。

Claude Code 官方 Hooks guide 对 Hooks 的定位很清楚:Hooks 会在 Claude Code 生命周期的特定点自动运行,用来 enforce project rules、automate repetitive tasks、integrate with existing tools。也就是说,它是确定性自动化,不是又一段劝 Claude 听话的提示词。

2. CLAUDE.md、Permissions、Hooks 各管什么

先把三个概念摆清楚。

机制中文理解强度适合什么
CLAUDE.md工作说明书建议 / 习惯风格、流程、偏好、项目背景
Permissions权限闸门允许 / 询问 / 拒绝工具级访问控制
Hooks自动检查点到点必触发格式化、审计、阻止危险动作、补上下文

举一个 TypeScript 项目:

  • “优先用现有 service 层,不要新造抽象”适合写 CLAUDE.md
  • “Bash 里运行 rm -rf 要询问”适合权限规则。
  • “每次改 .ts 文件后跑 formatter”适合 PostToolUse Hook。
  • “任何写 .env 的尝试都拦下来”适合 PreToolUse Hook。

这三者不是互相替代。

CLAUDE.md 给方向,Permissions 管门槛,Hooks 做检查点。

一句话理解:能容忍偶尔遗漏的,写进 CLAUDE.md;需要工具级边界的,用 Permissions;到某个事件点必须自动发生的,用 Hooks。

3. Hook 其实是生命周期检查点

Hook 的本质是:Claude Code 运行到某个时间点,自动把一段 JSON 输入交给你配置的 handler(处理器)。handler 可以是命令、HTTP 端点、MCP 工具、prompt 或 agent。

先看一个简化时间线:

flowchart LR
    Prompt["用户提交提示"]
    Think["Claude 思考"]
    Pre["PreToolUse<br/>工具执行前"]
    Tool["工具调用<br/>Edit / Write / Bash / MCP"]
    Post["PostToolUse<br/>工具成功后"]
    Stop["Stop<br/>准备停止"]

    Prompt --> Think --> Pre --> Tool --> Post --> Think --> Stop

    style Pre fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
    style Post fill:#dcfce7,stroke:#22c55e,stroke-width:2px
    style Stop fill:#f3e8ff,stroke:#a855f7,stroke-width:2px

Hooks 不是只在“写文件前”能用。官方 Hooks reference 列了很多事件,比如:

  • UserPromptSubmit:用户提示提交时触发,适合注入上下文、拦截危险请求。
  • PreToolUse:工具执行前触发,适合阻止危险命令、改写工具输入。
  • PermissionRequest:即将弹权限请求时触发,适合自动允许/拒绝某类请求。
  • PostToolUse:工具成功后触发,适合格式化、审计、补充反馈。
  • PostToolUseFailure:工具失败后触发,适合记录失败、给 Claude 修复线索。
  • Stop:Claude 准备停下时触发,适合检查任务是否真的完成。
  • SubagentStop:子代理准备结束时触发,适合审查子代理结果。
  • PreCompact / PostCompact:上下文压缩前后触发,适合归档或重新注入关键上下文。
  • Elicitation / ElicitationResult:MCP server 请求用户输入前后触发,适合审计或自动处理信息征询。

新手不需要一口气记完。先记三个最常用的:

  • PreToolUse:动手前拦。
  • PostToolUse:动手后补。
  • Stop:收工前查。

Hook 触发顺序到底是什么样:一次完整工具调用的事件链是 UserPromptSubmit → (思考) → PreToolUse → (Permission 检查) → 工具执行 → PostToolUse / PostToolUseFailure → (思考) → Stop关键事实:① PreToolUse 在 Permission 检查之前触发——Hook 可以提前拦掉危险操作不弹权限窗;② Stop 在准备结束本轮之前触发——可以阻止 Claude 过早收工。所以 Hook 不是"事后审计",是主动嵌入决策链路——这是它跟"日志"的本质区别。

新手坑:不要为了“控制感”给每个事件都挂 Hook。Hook 越多,运行越慢,干扰越多,排查也越难。

4. 一个最小 Hook 长什么样

Hooks 写在 settings JSON 里。结构有三层:

  1. 选事件,比如 PreToolUse
  2. 选 matcher group,比如只匹配 WriteBash
  3. 放一个或多个 hook handler。

比如阻止写 .env

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/protect-env.sh"
          }
        ]
      }
    ]
  }
}

脚本大概这样:

#!/usr/bin/env bash
set -euo pipefail

INPUT="$(cat)"
FILE_PATH="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')"

case "$FILE_PATH" in
  *.env|*/.env|*.env.*)
    echo "Blocked: environment files may contain secrets and must be edited manually." >&2
    exit 2
    ;;
esac

exit 0

Claude Code 会把事件 JSON 通过 stdin 传给 command hook。上面脚本读 tool_input.file_path,如果目标是环境文件,就返回 exit code 2。

关键点:Hook 不需要说服 Claude。它在工具执行前已经拿到了结构化输入,可以直接基于文件路径、命令、权限模式做判断。

5. exit code 2 不只是“报错”

官方 Hooks reference 明确说:exit 0 表示成功;exit 2 表示 blocking error;其他退出码对多数事件来说是 non-blocking error。

这很重要:

  • exit 0:成功,动作继续;stdout 里的 JSON 可能被解析。
  • exit 2:阻止;stderr 会反馈给 Claude。
  • exit 1 或其他:多数事件下只是非阻塞错误,动作继续。

所以不要用传统直觉写:

exit 1

如果你想阻止操作,要写:

echo "Blocked: do not edit .env" >&2
exit 2

并且记住一个细节:exit 2 时,Claude Code 忽略 stdout 和 stdout 里的 JSON,只看 stderr 作为反馈。 如果你要用 JSON 做更细控制,通常是 exit 0 然后在 stdout 输出 JSON。

例如 PreToolUse 可以返回:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database writes are not allowed."
  }
}

实操判断:简单阻止用 stderr + exit 2;需要更细的 allow / deny / ask / 修改输入,用 exit 0 + JSON output

6. 五种 handler:不只有 shell

旧理解里,Hook 常被理解成“自动跑一个 shell 脚本”。现在已经不止这样。

官方 Hooks reference 里的 handler type 有五类:

  • command:执行 shell 命令,适合格式化、文件保护、日志。
  • http:POST 到一个 URL,适合通知、审计、接外部系统。
  • mcp_tool:调已连接 MCP server 的工具,适合复用现有 MCP 能力。
  • prompt:让 Claude 模型做一次判断,适合检查是否真的完成、是否符合复杂规则。
  • agent:启动带工具的 verifier agent,适合需要读文件、搜索后再判断的场景。

这五种放在一条光谱上看:

确定性强 -> 判断力强
command -> http -> mcp_tool -> prompt -> agent

例子:

  • .ts 后跑 Prettier:command
  • 每次权限请求发审计系统:http
  • 复用一个内部 policy MCP server:mcp_tool
  • Stop 前判断“任务是否真的完成”:prompt
  • Stop 前让一个 verifier agent 搜索测试结果、读 diff 再判断:agent

不要反过来用重武器:能用 command 判断的,就不要用 prompt;能用 prompt 判断的,不一定要开 agent。判断越智能,成本和不确定性也越高。

为什么 Anthropic 设计 5 种 handler 而不是只给 command:用 shell command 解决一切是最简单——但有些判断 shell 写不出来。"任务真的完成了吗"需要语义理解,正则做不到——这就是 prompt handler 的位置。"任务完成了但代码风格还有 3 处问题"需要读代码 + 跑命令——这就是 agent handler。5 种递进的设计哲学是:让你按"判断复杂度"选合适的工具,不强迫所有规则都在 shell 里写——避免出现"写 100 行 shell 模拟模型推理"的反模式。

7. Block-at-Write 还是 Block-at-Submit

Hook 很容易被用重。

比如你想保证测试通过。你可以在每次 Edit 后立刻跑全量测试吗?技术上可以。但这会让 Claude 每改一个文件就等很久,任务体验会崩。

更好的方式是区分两类规则:

  • Block-at-Write:动手前或刚动手后立即拦,适合改 .env、删库、写生产配置。
  • Block-at-Submit:收工前统一检查,适合测试、lint、文档完整性、验收清单。

危险动作要早拦,因为一旦发生就可能造成损失。

质量检查可以晚一点,因为 Claude 需要先完成一组相关修改,再统一修 lint、测试和格式。

Stop hook 就适合 Block-at-Submit:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "复盘本轮结果。如果还缺测试或必要的验证步骤,就阻止停止:$ARGUMENTS"
          }
        ]
      }
    ]
  }
}

底层逻辑:早拦保护不可逆风险,晚查保护交付质量。不要把所有质量规则都塞到每一次文件写入前。

8. Hook 配置放在哪里

Hook 遵守 Claude Code settings scope:

  • ~/.claude/settings.json:作用于你所有项目,适合个人通知、个人日志。
  • .claude/settings.json:作用于当前仓库所有协作者,适合团队规范、安全规则。
  • .claude/settings.local.json:只作用于当前仓库你自己,适合本机实验、临时调试。
  • plugin:作用于插件启用处,适合插件随带的 Hook。
  • managed settings:作用于组织级,适合安全和合规强制策略。

官方配置文档里,scope 优先级大致是:managed 最高,其次命令行参数、local、project、user。项目级能覆盖用户级,managed 不能被普通用户覆盖。

你可以用:

/hooks

打开只读 hooks 菜单,检查当前有哪些 hook、来自哪个来源、匹配什么事件。官方说明里 /hooks 是 read-only:想改配置,还是要编辑 settings JSON 或让 Claude 修改文件。

团队规则别放错:团队必须共同遵守的 Hook 放 .claude/settings.json;只对你本机有意义的通知、路径和实验,放 .claude/settings.local.json 或用户级。

9. 幂等性和性能

Hooks 是代码。代码就有工程质量。

第一条是幂等性。

幂等性就是:跑一次和跑多次,结果不应该越来越坏。

好的 Hook:

格式化文件
检查文件路径
追加带唯一 id 的审计日志

危险 Hook:

每触发一次就插一条数据库记录
每触发一次就覆盖一个全局配置
每触发一次就发一条无法去重的外部通知

Claude Code 可能多次编辑同一个文件,PostToolUse 就可能多次触发。如果 Hook 没有幂等性,就会制造重复副作用。

第二条是性能。

PreToolUse 是热路径。你在每次工具调用前跑一个 10 秒脚本,Claude Code 体验会立刻变慢。

减少成本的方法:

  • matcher 限定工具名。
  • if 进一步限定命令或文件类型。
  • 小检查用 command,不要动不动 prompt/agent。
  • 长任务考虑 async,但不要用 async 做必须同步拦截的安全规则。

Hook 设计原则:越靠前、越高频的事件,Hook 越要短、准、可预测。

10. 这一篇和前几篇怎么连起来

到这里,Claude Code 的控制层可以串成一张图。

flowchart TB
    Need["你想控制 Claude Code 行为"]
    Know["长期知识和偏好<br/>CLAUDE.md"]
    Reuse["重复任务流程<br/>Skills"]
    Reach["连接外部系统<br/>MCP"]
    People["隔离或协作<br/>SubAgents / Agent Teams"]
    Guard["确定性检查点<br/>Hooks"]
    Perm["工具访问边界<br/>Permissions"]

    Need --> Know
    Need --> Reuse
    Need --> Reach
    Need --> People
    Need --> Guard
    Need --> Perm

    style Guard fill:#fef3c7,stroke:#f59e0b,stroke-width:2px
    style Perm fill:#fee2e2,stroke:#ef4444,stroke-width:2px

每一层解决的问题不同:

  • CLAUDE.md:让 Claude 知道项目规则,关键词是 should。
  • Skills:复用任务流程,关键词是 reusable workflow。
  • SubAgents:隔离旁支任务,关键词是 separate context。
  • Agent Teams:多会话协作,关键词是 shared task list。
  • MCP:够到外部系统,关键词是 external tools。
  • Hooks:到点自动检查,关键词是 deterministic checkpoint。
  • Permissions:哪些工具能用,关键词是 access control。

第 11 篇会专门讲 Permissions。先记住边界:Hook 可以在事件点做检查,Permissions 决定工具访问规则。两者经常一起用,但不是一回事。

一句话理解:Hooks 是“到点必查”的检查点;Permissions 是“能不能用工具”的访问边界;CLAUDE.md 是“希望你怎么做”的说明书。

11. 本章自检

试着用自己的话回答:

  1. 为什么"我已经写进 CLAUDE.md"不能等同于"这件事一定会发生"?对应 §1-§2。
  2. exit 2 和 exit 1 在 Hooks 里有什么关键区别?如果你想阻止写 .env,应该用哪个?对应 §5。
  3. 动手题 ⭐:在你项目根新建 .claude/hooks/protect-secrets.sh,写一个 PreToolUse Hook:当 Claude 想 Edit / Write 任何文件名包含 .env / secret / credentials 的文件时,用 stderr 输出"危险文件不允许编辑"+ exit 2 阻止。然后在 .claude/settings.json 注册。自检:手动让 Claude 试着改 .env,看 Hook 有没有拦下来 + 看 stderr 信息有没有进 Claude 上下文。这一道题做完你会知道:CLAUDE.md 提醒 ≠ 真拦得住,Hook 才是确定性。对应 §1 + §4。

过关标准:能用一句话说清:Hooks 是 Claude Code 生命周期里的确定性检查点,用来自动执行、阻止或反馈那些不能只靠模型记忆完成的规则。

本篇术语速查表
  • Hook:钩子,在 Claude Code 生命周期特定点自动执行的处理器。
  • handler:处理器,Hook 触发后真正运行的 command、http、prompt 等。
  • matcher:匹配器,限定 Hook 对哪些工具或事件生效。
  • PreToolUse:工具执行前,工具调用前触发,可阻止危险动作。
  • PostToolUse:工具执行后,工具成功后触发,常用于格式化和审计。
  • Stop:停止前,Claude 准备结束当前回合时触发。
  • exit code:退出码,脚本结束时返回给 Claude Code 的状态数字。
  • stderr:标准错误输出,exit 2 时反馈给 Claude 的错误信息来源。
  • idempotency:幂等性,多次执行结果仍然安全稳定。
  • Permissions:权限系统,控制 Claude Code 工具访问边界的机制。

官方资料

接下来去哪

如果只记一个判断:凡是“漏一次就会出事”的规则,不要只写成提醒,要放到 Hook 或 Permission 里。

本页目录