<!-- title: 双钩护栏：拦截与保存即格式化 -->

## 概述

本教程带你掌握 Claude Code 钩子系统中最核心的两个工具事件——`PreToolUse` 与 `PostToolUse`，构建一条确定性的代码质量护栏：前者在工具执行**前**拦截危险命令、保护敏感文件、改写入参；后者在工具执行**后**自动格式化、自动 Lint、自动跑测试。读完即可在一个项目里跑通"保存即格式化 + 危险命令拦截"的完整闭环。适合已用 Claude Code 跑过至少一个项目、想把"靠 LLM 自觉"升级为"铁律执行"的开发者与工程负责人。

## 前置条件

- 已安装 Claude Code（v2.1.85 及以上可解锁 `if` 字段；v2.0.10 及以上可解锁 `updatedInput` 入参改写）
- 一个真实代码仓库（Node/TS、Python、Go 均可）
- 系统装有 `jq`（`brew install jq` / `apt-get install jq`），用于在钩子脚本里解析 stdin 的 JSON
- 若要做自动格式化，项目里装好对应的格式化器（Prettier / Black / `gofmt` 等）

## 一、为什么需要钩子：从"建议"到"铁律"

Claude Code 会遵循 `CLAUDE.md` 里的约定——但只是"大概率遵循"。当规则是"**永远不要**删 `.env`""**每次写完文件都要**跑 Prettier"时，"大概率"就不够了。

钩子（Hooks）是用户定义的事件处理器，在工具调用的特定生命周期点**确定性地**执行。关键区别在于：

| 机制 | 性质 | 是否每次执行 |
| :--- | :--- | :--- |
| `CLAUDE.md` | 建议（suggestion） | 否，LLM 偶尔跳过 |
| **Hooks** | 强制（enforcement） | **是，无例外** |
| Skills | 上下文相关专业知识 | 仅在匹配时激活 |

一句话判断标准：**违反会造成破坏 → 用钩子；违反只是次优 → 用 `CLAUDE.md`**。

## 二、读懂双钩生命周期

一次工具调用的完整链路如下，`PreToolUse` 和 `PostToolUse` 正好夹住"工具执行"这个不可逆动作：

```mermaid
flowchart LR
    A[用户提示] --> B[Claude 生成工具调用]
    B --> C["PreToolUse 钩子<br/>可拦截/改写/放行"]
    C -->|放行| D[工具执行<br/>产生副作用]
    D --> E["PostToolUse 钩子<br/>可格式化/Lint/测试"]
    E --> F[Claude 继续下一步]
    C -->|拦截 exit 2| G[取消调用<br/>错误反馈给 Claude]
```

- **`PreToolUse`**：在 Claude 决定调用什么工具、传什么参数之后，但**工具真正执行之前**触发。这是唯一能"阻止副作用发生"的同步检查点。可拦截、可放行（跳过权限对话框）、可改写参数。
- **`PostToolUse`**：在工具**成功执行之后**触发。动作已发生，无法撤销，但可以"反应"——格式化刚写的文件、对刚改的代码跑 Lint、触发相关测试。

> 关键时序事实：`PreToolUse` 钩子在**任何权限模式检查之前**触发。也就是说，即便会话以 `bypassPermissions`（跳过所有权限）模式运行，`permissionDecision: "deny"` 依然能拦住调用。这让你能把"谁都无法绕过"的策略焊死在钩子里。

## 三、退出码契约：钩子与 Claude Code 的"对讲机"

钩子脚本通过 stdin（收 JSON）、stdout/stderr、退出码与 Claude Code 通信。其中退出码是核心控制信号：

| 退出码 | 含义 | 适用场景 |
| :--- | :--- | :--- |
| `0` | 无异议，动作正常进行 | `PreToolUse` 下**不等于批准**，正常权限流程仍生效 |
| `2` | **阻断**动作；stderr 会被当作反馈喂给 Claude | 让 Claude 调整策略后重试 |
| 其他非 0 | 动作照常进行，仅记一条 `hook error` 提示 | 非阻断性警告 |

需要更精细控制时，退出 `0` 并向 stdout 打印结构化 JSON。例如 `PreToolUse` 拒绝并给出理由：

```json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "请用 rg 替代 grep 以提升性能"
  }
}
```

`permissionDecision` 取值：`allow`（跳过交互式权限提示）、`deny`（取消调用并把理由回传 Claude）、`ask`（照常弹权限对话框）、`defer`（仅非交互 `-p` 模式可用，保留调用供 SDK 包装器后续处理）。

## 四、第一步：第一个 PostToolUse——保存即格式化

这是回报最快的一个钩子：Claude 每写/改一个文件，自动跑 Prettier。

把以下内容写入项目根目录的 `.claude/settings.json`：

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true",
            "statusMessage": "Formatting with Prettier..."
          }
        ]
      }
    ]
  }
}
```

**操作说明**：`matcher: "Edit|Write"` 让钩子只对文件编辑类工具触发（不对 `Bash`、`Read` 触发，避免无谓开销）。`jq` 从 stdin 的 JSON 中取出刚改的文件路径，管道给 Prettier。`2>/dev/null || true` 保证对 Prettier 不支持的文件类型（如 `.md`）不报错。

**预期结果**：让 Claude 改一个 `.ts` 文件，文件保存瞬间自动按 Prettier 规范重排；过程中底部状态栏显示自定义文字"Formatting with Prettier..."。

**常见问题**：
- 钩子不触发？在会话里输入 `/hooks`，确认 `PostToolUse` 下出现该钩子；`matcher` 区分大小写，`Edit|Write` 必须精确。
- 看到 `hook error`？通常是 `jq: command not found`——装好 `jq` 即可。

## 五、第二步：PreToolUse 拦截危险命令

`PreToolUse` 是生产安全的头号事件。下面这个钩子拦截 `rm -rf`、`git push --force`、`DROP TABLE` 等。

新建 `.claude/hooks/block-destructive.sh`：

```bash
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if [ -z "$COMMAND" ]; then exit 0; fi

DANGEROUS_PATTERNS=('rm -rf' 'git push --force' 'git push -f' 'DROP TABLE' 'git reset --hard')
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qi "$pattern"; then
    echo "Blocked: command matches dangerous pattern '$pattern'" >&2
    exit 2   # 退出码 2 = 阻断，stderr 反馈给 Claude
  fi
done
exit 0
```

赋予可执行权限，并在 `settings.json` 注册：

```bash
chmod +x .claude/hooks/block-destructive.sh
```

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-destructive.sh", "timeout": 5 }
        ]
      }
    ]
  }
}
```

**预期结果**：让 Claude"清理临时目录"。原本它会执行 `rm -rf /tmp/project-cache`；钩子激活后，命令被拦截，Claude 收到错误反馈，自动改用更安全的 `find /tmp/project-cache -type f -delete` 重试——无需人工干预。

**常见问题**：
- 钩子"坏了"、`.env` 照改不误？**90% 是退出码写错**：写了 `exit 1`（非阻断警告）而不是 `exit 2`（阻断）。这是最高频的踩坑点。
- 用 `"$CLAUDE_PROJECT_DIR"` 或绝对路径引用脚本，避免 `command not found`。要彻底绕开 shell 引号问题，可加 `"args": []` 切到 exec 形式直接 spawn 脚本。

## 六、第三步：保护敏感文件

把 `.env`、lock 文件、CI 配置焊死，任何工具（含 `Bash` 里的 `sed`/`echo >`）都改不动。注意 matcher 用 `Edit|Write|Bash` 三管齐下：

```bash
#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE_PATH" ] && exit 0

for pattern in ".env" "package-lock.json" "pnpm-lock.yaml" ".git/" ".github/workflows/"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: '$FILE_PATH' 是受保护文件" >&2
    exit 2
  fi
done
exit 0
```

```json
{
  "matcher": "Edit|Write|Bash",
  "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh" }]
}
```

## 七、第四步：用 `updatedInput` 改写命令（v2.0.10+）

除了"阻断后等 Claude 重试"，`PreToolUse` 还能**直接改写工具参数**再放行。把破坏性的 `rm -rf` 就地改成交互式 `rm -ri`：

```bash
#!/bin/bash
# .claude/hooks/safer-rm.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -q '^rm -rf'; then
  SAFER=$(echo "$COMMAND" | sed 's/^rm -rf/rm -ri/')
  jq -n --arg cmd "$SAFER" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "allow",
      updatedInput: { command: $cmd }
    }
  }'
  exit 0
fi
exit 0
```

`updatedInput` 在执行前替换工具入参，配合 `permissionDecision: "allow"` 跳过权限对话框直接跑改写后的命令。

## 八、多钩组合：最严格者胜

同一事件可挂多个钩子，**所有匹配钩子并行跑完**再合并结果。一个钩子返回 `deny` **不会**阻止兄弟钩子执行——别指望一个钩子的 `deny` 能压制另一个钩子的副作用。

合并规则按**最严格者胜**，优先级为 `deny > defer > ask > allow`。下例在 `Bash` 上挂两个 `PreToolUse`：一个记日志（`exit 0`，无决策），一个拦 `rm -rf`（`exit 2`，拒绝）：

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "jq -r .tool_input.command >> ~/.claude/bash.log" },
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh" }
        ]
      }
    ]
  }
}
```

当 Claude 试图跑 `rm -rf /tmp/build`：日志钩子已写盘（`exit 0`），护栏钩子 `exit 2` 拒绝。拒绝获胜，命令被拦，但日志条目依然保留——因为日志钩子早已跑完。

## 九、用 `if` 字段做精细过滤（v2.1.85+）

`matcher` 只能在工具名层面过滤（如 `Bash`）。`if` 字段用**权限规则语法**把工具名和参数一起过滤，让钩子进程只在真正匹配时才 spawn。下例仅在 Claude 跑 `git` 子命令时触发，而非所有 `Bash` 调用：

```json
{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        { "type": "command", "if": "Bash(git *)", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh" }
      ]
    }
  ]
}
```

`if` 失败时会"放行"（仍跑钩子）：当 Bash 命令无法解析、或 `$()`/反引号/`$VAR` 出现且模式不只含命令名时，过滤器会保守地触发钩子。**因此要用权限系统而非钩子来强制硬性允许/拒绝**，钩子的 `if` 只是性能优化。

## 十、调试钩子三招

钩子默认静默失败。排查时按顺序用这三招：

1. **`/hooks` 浏览**：会话内输入 `/hooks`，确认钩子挂在正确事件下、matcher 准确。
2. **手动喂数据测脚本**：`echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?`——直接看退出码。
3. **debug 日志**：用 `claude --debug-file /tmp/claude.log` 启动，另一终端 `tail -f`；或会话内 `/debug` 开日志、`Ctrl+O` 看转录视图里每个钩子的一行摘要。

高频坑：JSON 解析失败却"脚本是好的"——通常是 Git Bash/`BASH_ENV` 指向的 profile 里有无条件 `echo`，把杂讯拼到了 JSON 前。把 profile 里的 `echo` 包进 `if [[ $- == *i* ]]; then ... fi`（仅交互式 shell 输出）即可。

## 实战案例：一个项目的三钩最小配置

把前面的能力拼成一个可直接落地的 `.claude/settings.json`，覆盖"拦截危险命令 + 保护敏感文件 + 保存即格式化 + 完成通知"四件事：

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-destructive.sh", "timeout": 5 }]
      },
      {
        "matcher": "Edit|Write|Bash",
        "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh", "timeout": 5 }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true",
            "statusMessage": "Formatting..."
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "",
        "hooks": [{ "type": "command", "command": "notify-send 'Claude Code' '需要你的注意'" }]
      }
    ]
  }
}
```

落地后实测收益（多项目 6 个月数据）：误删命令从每月 2–3 次降到 0；每会话手动跑格式化从 5–10 次降到 0；`.env`/lock 文件误改在源头被拦；单次工具调用钩子开销约 60ms（matcher 越窄越省——空 matcher 约多 80ms/次，一个提示 15–30 次工具调用会累计成 1–2 秒延迟，务必收窄）。

## 进阶：当确定性不够时——prompt 与 agent 钩子

确定性规则覆盖不到、需要"判断"时，换钩子**类型**而非写更复杂的 shell：

- **`type: "prompt"`**：单轮 LLM 评估（默认 Haiku）。适合"Claude 是不是真的做完了"这类判断。返回 `{"ok": true}` 放行，`{"ok": false, "reason": "还差什么"}` 喂回 Claude 让它继续。
- **`type: "agent"`**：多轮子代理，可 `Read`/`Grep`/`Glob`、最多 50 轮工具调用、默认超时 60s。适合"跑测试套件并验证全过"这类需读取代码、执行命令的复杂校验。

`Stop` 钩子用 `prompt`/`agent` 做"完成度校验"时，务必解析 stdin 的 `stop_hook_active` 字段：为 `true` 就 `exit 0` 放行，否则 Claude 会陷入"停下→钩子让它继续→又停下"的循环——连续阻断 8 次后 Claude Code 会强制放行。

## 延伸阅读

- **Claude Code Hooks 官方指南**：`code.claude.com/docs/en/hooks-guide`——全部事件、JSON 契约、`if`/HTTP/MCP 钩子
- **Hooks Reference**：完整事件 schema、异步钩子、MCP 工具钩子字段
- **权限系统**：理解 `allow`/`deny`/`ask` 规则如何与钩子叠加（deny 规则永远压过钩子的 allow）
- **Bash 命令校验器示例**：Anthropic 官方仓库的完整参考实现

## 训练营适用性评估

- **适用类型**：面向有 Claude Code 实战经验的开发者训练营、企业级 AI 编码规范训练营、Agent 可靠性/安全工程训练营。不适合零基础（需先跑通过项目、理解工具调用循环）。
- **教学时长**：建议 1 个半天（3–4 小时）。1 小时讲机制+退出码契约，1.5 小时动手配三钩并跑通闭环，0.5–1 小时做 prompt/agent 钩子进阶与排错演练。
- **考核点**：能否独立写出"拦截 `git push --force` + 保护 `.env` + 保存即格式化"的三钩配置，并用 `/hooks` 与 debug 日志验证生效；能否解释"为什么 `exit 1` 拦不住而 `exit 2` 能"。

## 参考来源

1. [Automate actions with hooks — Claude Code Docs](https://code.claude.com/docs/en/hooks-guide) — Anthropic 官方钩子指南，覆盖全部事件、退出码契约、`if` 字段、prompt/agent/HTTP 钩子
2. [Claude Code Hooks: PreToolUse & PostToolUse Tutorial — Paul's Development Blog](https://paul-schick.com/posts/claude-code-hooks-pretooluse-posttooluse/) — 双钩实战教程，含危险命令拦截、文件保护、`updatedInput` 改写、多钩组合与排错
3. [How to Add a Claude Code PostToolUse Formatter Hook — how2.sh](https://how2.sh/posts/how-to-add-a-claude-code-posttooluse-formatter-hook/) — PostToolUse 自动格式化钩子的逐步配置
4. [Auto Format Hook (auto-format) — Claude Code Learning Hub](https://mygithub05253.github.io/Claude-Code-Study/en/hooks/auto-format) — PostToolUse 钩子运行时机与工作原理讲解
5. [GitHub: claude-hooks](https://github.com/BrianLeishman/claude-hooks) — 开源的自动格式化/Lint/测试钩子集合，含热重载，可直接复用脚本

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
