<!-- title: Hooks九事件编织质量门禁闭环 -->

# 用Hooks织一张全程质量门禁网：Claude Code全生命周期自动化实战

## 概述

本教程聚焦 Claude Code 的 **Hooks（钩子）** 系统——一组在 Agent 生命周期固定节点上"确定性"执行的脚本。与"在 CLAUDE.md 里写一句『记得跑测试』然后祈祷模型照做"不同，Hooks 保证某些动作一定会发生。我们将沿着会话从启动到结束的完整事件链，用 `SessionStart`、`UserPromptSubmit`、`PreToolUse`、`PostToolUse`、`Notification`、`Stop`、`PreCompact`、`SessionEnd` 等九个事件，亲手织出一张"自格式化 + 防破坏 + 质量门禁 + 全程审计 + 上下文抢救"的自动化闭环。适用人群：已能独立使用 Claude Code 完成 CRUD、希望把团队规范"焊死"进工具链的中高级开发者与 DevOps/平台工程师。

## 前置条件

- Claude Code 已安装并能正常对话（`claude --version` 可输出版本）
- 一台 Linux/macOS 机器（Windows 用 Git Bash），已装 `jq`（`jq --version`）
- 一个可作实验场的 Git 仓库（Python 项目最佳，便于演示 `ruff`/`pytest`）
- 了解 JSON 与 shell 脚本基础；不要求写过 Hook

## 第 0 步：建立 Hooks 心智模型

理解一句话：**Hooks 把"希望发生的事"变成"必然发生的事"。** 模型可能忘记、可能偷懒，但一个挂在 `PreToolUse` 上、`exit 2` 的 shell 脚本，无论模型多想跑 `rm -rf /`，都会被硬性拦下。

Claude Code 在 2026 年的 Hooks 系统提供 **五类处理器（handler type）**，都挂在 `settings.json` 的 `hooks` 键下：

| type | 运行方式 | 典型场景 |
| --- | --- | --- |
| `command` | 执行 shell 命令，读 stdin、写 stdout/stderr | 99% 的确定性规则 |
| `http` | POST 事件 JSON 到一个 URL | 团队共享审计服务 |
| `mcp_tool` | 调用已连接 MCP 服务器的工具 | 复用既有 MCP 能力 |
| `prompt` | 单轮 LLM 判定（默认 Haiku，30s 超时） | 需要"判断"而非死规则 |
| `agent` | 多轮子代理验证（可读文件、跑命令，60s/最多50轮） | 要核对代码库真实状态 |

**退出码三态语义**（所有事件通用）：

- `exit 0`：无异议，动作照常进行。对 `UserPromptSubmit`/`SessionStart`，stdout 会被注入到模型上下文。
- `exit 2`：**阻断**。stderr 作为反馈喂回模型。部分事件（`SessionStart`、`Notification` 等）不可阻断，`exit 2` 只是把 stderr 展示给人。
- 其他码：非阻断错误，继续执行，但界面会出现 `<hook name> hook error` 提示。

**预期结果**：理解到——确定性靠 `command`，模糊判断靠 `prompt`，深度核对靠 `agent`。本教程主走 `command`，在第 7 步引入 `agent` 做质量门禁。

## 第 1 步：三层作用域与最小骨架

Hooks 可挂在五个位置，作用域由小到大：

| 位置 | 作用域 | 可共享 |
| --- | --- | --- |
| `~/.claude/settings.json` | 你所有项目 | 否（本机） |
| `.claude/settings.json` | 单个项目 | 是（可提交进仓库） |
| `.claude/settings.local.json` | 单个项目 | 否（自动 gitignore） |
| 企业托管策略 | 全组织 | 是（管理员控制） |
| 插件 `hooks/hooks.json` | 插件启用时 | 是（随插件分发） |

先建项目级骨架。在仓库根创建 `.claude/settings.json`：

```json
{
  "hooks": {
    "SessionStart": [],
    "UserPromptSubmit": [],
    "PreToolUse": [],
    "PostToolUse": [],
    "Notification": [],
    "Stop": [],
    "PreCompact": [],
    "SessionEnd": []
  }
}
```

**操作说明**：保存后，在 Claude Code 内输入 `/hooks`，应能看到按事件分组的空列表。

**常见问题**：`/hooks` 显示为空——多半是 JSON 非法（不允许尾逗号、不允许注释）。用 `jq . .claude/settings.json` 校验。另注意：个别版本（如 v2.1.120 在 VSCode 原生扩展下）会静默丢弃 `~/.claude/settings.json` 里的用户级 Hooks，遇到就改挂项目级。

## 第 2 步：SessionStart——会话自举与上下文续命

`SessionStart` 在会话开始、`/clear`、`resume`、`compact` 时触发，`matcher` 取 `startup|resume|clear|compact`。它**不可阻断**，但 stdout 会被注入上下文。

两个高频用法：**自举环境**与**压缩后续命**。先做自举——把 direnv 注入到 Bash 工具的执行前导（`CLAUDE_ENV_FILE`）：

```json
"SessionStart": [
  {
    "hooks": [
      { "type": "command", "command": "direnv export bash > \"$CLAUDE_ENV_FILE\"" }
    ]
  }
]
```

`CLAUDE_ENV_FILE` 的内容会被 Claude Code 作为脚本前导拼到每条 Bash 命令前，于是 Bash 工具也能"继承"目录级环境变量，省去反复 `export`。

**预期结果**：进入项目后第一条 Bash 命令即可用 `.envrc` 里的变量。

再做"压缩续命"——上下文压缩会丢细节，用 `compact` matcher 把关键约定重新塞回去：

```json
{
  "matcher": "compact",
  "hooks": [
    {
      "type": "command",
      "command": "echo '提醒：用 ruff format 而非 black；提交前必须 pytest 全绿；当前迭代=支付重构。'"
    }
  ]
}
```

**常见问题**：每次 `startup` 都注入大段文本会浪费 token——静态长文优先写进 `CLAUDE.md`，Hook 只放"动态、易变"的少量上下文（如 `git log --oneline -5`）。

## 第 3 步：UserPromptSubmit——提示词级注入与拦截

在你提交一条 prompt、模型处理之前触发，**无 matcher**，默认超时仅 30s。`exit 2` 可阻断处理；更优雅的是返回 `additionalContext` 注入上下文：

```json
"UserPromptSubmit": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "git log --oneline -3 | sed 's/^/最近提交: /'"
      }
    ]
  }
]
```

**预期结果**：每次提问时，模型都能"看到"最近三条提交，减少让它翻历史的需求。

**常见问题**：脚本卡住会拖垮体验（30s 超时）。任何可能阻塞的命令（如联网请求）都别放在这里。

## 第 4 步：PreToolUse——最硬的护栏

在工具执行**之前**触发，可阻断。`matcher` 匹配工具名（`Bash`、`Edit|Write`、或正则 `mcp__memory__.*`）。这是把安全策略"焊死"的核心。

先拦最危险的 `rm -rf`。写脚本 `.claude/hooks/block-rm-rf.sh`：

```bash
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
if echo "$COMMAND" | grep -Eq 'rm[[:space:]]+-[a-zA-Z]*r'; then
  echo "阻断：禁止 rm -rf，请改用逐项删除或 git clean。" >&2
  exit 2
fi
exit 0
```

`chmod +x` 后注册：

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

**进阶——结构化 JSON 输出**比 `exit 2` 更可控，可区分 `allow/deny/ask/defer` 并附带理由：

```bash
#!/bin/bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
if echo "$CMD" | grep -q 'git push'; then
  cat <<'EOF'
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"推送需人工二次确认"}}
EOF
  exit 0
fi
exit 0
```

**关键铁律**：Hook 能"收紧"权限，但**不能放宽**。即便 Hook 返回 `allow`，settings 里的 `deny` 规则依然生效；反过来，`deny` 的 Hook 即便在 `bypassPermissions` 或 `--dangerously-skip-permissions` 模式下也照拦不误。所以 PreToolUse 是"用户无法绕过的最后一道墙"。

**常见问题**：脚本不触发——八成是 matcher 写错（大小写敏感，必须精确 `Bash`），或脚本没 `chmod +x`，或路径用了相对路径（改用 `$CLAUDE_PROJECT_DIR` 或绝对路径）。

## 第 5 步：PostToolUse——自动格式化与审计落盘

工具**成功执行后**触发。注意：它**无法撤销**已发生的动作，只能"善后"或"反馈"。

自动格式化是最经典用法。`Edit|Write` matcher + 提取 `file_path` 喂给 ruff：

```json
"PostToolUse": [
  {
    "matcher": "Edit|Write",
    "hooks": [
      {
        "type": "command",
        "command": "jq -r '.tool_input.file_path' | xargs -r ruff format"
      }
    ]
  }
]
```

再叠一条审计：把每条 Bash 命令记进日志（注意多 Hook 时**最严格者胜**的合并规则）：

```json
{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "command": "jq -c '{ts: now|todate, cmd: .tool_input.command, cwd: .cwd}' >> \"$CLAUDE_PROJECT_DIR/.claude/audit.log\""
    }
  ]
}
```

**多 Hook 合并语义**：同一事件下所有匹配 Hook **并行**跑完再合并。`PreToolUse` 的权限决策按 `deny > defer > ask > allow` 取最严；`additionalContext` 则全部保留。所以一条审计 Hook（`exit 0`）和一条拦截 Hook（`exit 2`）可共存——审计照写，命令照样被拦。

**常见问题**：`PostToolUse` 用 `decision: "block"` 虽能把 `reason` 喂回模型，但文件已改完，无法回滚。要"事前阻断"永远用 PreToolUse。

## 第 6 步：Notification——别再盯着终端

`Notification` 在 Claude 等待输入或权限确认时触发。挂一个桌面通知，你就能切去干别的：

```json
"Notification": [
  {
    "matcher": "",
    "hooks": [
      {
        "type": "command",
        "command": "notify-send 'Claude Code' '需要你的输入'"
      }
    ]
  }
]
```

macOS 用 `osascript -e 'display notification ...'`，Windows 用 PowerShell 的 `MessageBox`。`matcher` 可精细到 `permission_prompt`（只权限确认时响）或 `idle_prompt`（只在任务完成等下一条 prompt 时响）。

**常见问题**：macOS 上 `osascript` 走的是"脚本编辑器"App，若它没有通知权限会**静默失败**。先在终端跑一次 `osascript -e 'display notification "test"'`，再到"系统设置 > 通知 > 脚本编辑器"打开权限。

## 第 7 步：Stop——质量门禁（本文核心）

`Stop` 在 Claude 结束回应时触发，**无 matcher**。返回 `{"decision":"block","reason":"..."}` 或 `exit 2` 可**阻止停止、强制继续**。这正是"质量门禁"的落点：**不让 Claude 收工，除非测试全绿。**

两种实现强度：

**弱门禁（command + Stop）**——直接跑 pytest，失败就 block：

```json
"Stop": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "cd \"$CLAUDE_PROJECT_DIR\" && pytest -q >/tmp/stopgate.log 2>&1 || { echo '测试未通过，详见 /tmp/stopgate.log' >&2; exit 2; }"
      }
    ]
  }
]
```

**强门禁（agent 型 Hook）**——交给一个能读代码、跑命令的子代理去核对：

```json
"Stop": [
  {
    "hooks": [
      {
        "type": "agent",
        "prompt": "运行 ruff check 与 pytest。若有任何失败，返回 {\"ok\": false, \"reason\": \"具体失败摘要\"}；全绿返回 {\"ok\": true}。"
      }
    ]
  }
]
```

`agent` 型 Hook 返回 `ok:false` 时，`reason` 会被当成下一步指令喂回主 Claude，驱动它去修复。

**致命陷阱——死循环与上限**：Stop 一旦 block，模型继续干，干完又触发 Stop，极易死循环。Claude Code 的保护是：**连续 block 超过 8 次且无进展，会强行覆盖 Hook 放行**。正确写法是**读 `stop_hook_active` 字段，若已 true 则放行**：

```bash
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0   # 已经在续跑中，别再拦
fi
cd "$CLAUDE_PROJECT_DIR" && pytest -q || { echo '测试未过，请修复后收尾' >&2; exit 2; }
```

确实需要更多轮，用环境变量 `CLAUDE_CODE_STOP_HOOK_BLOCK_CAP` 调高上限。

**常见问题**：Stop Hook 不触发——它只在"模型主动结束回合"时触发，**用户打断（Ctrl+C）不触发**；API 报错触发的是独立的 `StopFailure` 事件（其 matcher 可按 `rate_limit`/`overloaded`/`billing_error` 等过滤）。

## 第 8 步：PreCompact——上下文抢救

`PreCompact` 在压缩**之前**触发。趁压缩还没把对话摘要化，把"绝不能丢"的关键状态落盘或回灌：

```json
"PreCompact": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "git diff --stat | head -20 | sed 's/^/未提交改动: /'; echo '当前任务=实现支付回调；下一步=补单测'"
      }
    ]
  }
]
```

`PostCompact` 则在压缩后触发，二者配合可做"压缩前快照 + 压缩后恢复"。`matcher` 支持 `manual`/`auto`。

## 第 9 步：SessionEnd——清理与归档

`SessionEnd` 在会话终止时触发，`matcher` 取 `clear|resume|logout|prompt_input_exit|...`。典型用途是清理临时文件与归档审计：

```json
"SessionEnd": [
  {
    "matcher": "clear",
    "hooks": [
      { "type": "command", "command": "rm -f /tmp/claude-scratch-*.txt" }
    ]
  }
]
```

只在你跑 `/clear` 时清理，正常退出不触发。

## 第 10 步：组合、调试与排障

调试三板斧：

1. **`/hooks`**：可视化所有已注册 Hook，按事件分组，最先看这个。
2. **`Ctrl+O` 转录视图**：每个触发的 Hook 一行摘要——成功静默、阻断显示 stderr、非阻断错误显示首行 stderr。
3. **`--debug-file`**：`claude --debug-file /tmp/claude.log` 启动，另开终端 `tail -f`，可看完整匹配/退出码/stdout/stderr。

手动测脚本（强烈建议提交前都测一遍）：

```bash
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./.claude/hooks/block-rm-rf.sh
echo $?   # 应为 0
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/x"}}' | ./.claude/hooks/block-rm-rf.sh
echo $?   # 应为 2
```

**JSON 校验失败**排查：shell-form 命令经 `sh -c` 跑，若你的 `~/.bashrc` 里有无条件 `echo`，输出会拼到 JSON 前面导致解析失败。把 profile 里的 echo 包一层：

```bash
if [[ $- == *i* ]]; then echo "Shell ready"; fi
```

`$-` 含 `i` 表示交互式，Hook 跑在非交互 shell，echo 被跳过。

**关停开关**：`"disableAllHooks": true` 一键全关（托管策略里的 Hook 需要在托管层也设才彻底关）；权限、hooks、`apiKeyHelper` 改完**无需重启**即热加载。

## 实战案例：Python 仓库"质量门禁闭环"

目标：一个支付服务仓库，达到——编辑即格式化、`.env`/`main` 分支不可碰、收尾前 pytest+ruff 必绿、每条命令有审计、压缩不丢任务。完整 `settings.json`：

```json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          { "type": "command",
            "command": "echo '项目=pay-service；规范=ruff format；收尾前 pytest 全绿；当前迭代=回调幂等'" }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/guard.sh\"" }
        ]
      },
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/protect.sh\"" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs -r ruff format" }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command",
            "command": "jq -c '{ts: now|todate, cmd: .tool_input.command}' >> \"$CLAUDE_PROJECT_DIR/.claude/audit.log\"" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/quality-gate.sh\"" }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "idle_prompt",
        "hooks": [
          { "type": "command",
            "command": "notify-send 'Claude Code' '任务完成，等你下一步'" }
        ]
      }
    ]
  }
}
```

`guard.sh`（拦 `rm -rf`、拦 `git push origin main`）：

```bash
#!/bin/bash
INPUT=$(cat); CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
if echo "$CMD" | grep -Eq 'rm[[:space:]]+-[a-zA-Z]*r'; then
  echo "阻断：禁止 rm -rf" >&2; exit 2
fi
if echo "$CMD" | grep -Eq 'git push[[:space:]].*main'; then
  echo "阻断：禁止直推 main，请走 PR" >&2; exit 2
fi
exit 0
```

`protect.sh`（拦 `.env` 改写）：

```bash
#!/bin/bash
INPUT=$(cat); FP=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
case "$FP" in
  *.env|*.env.*) echo "阻断：禁止改写 $FP" >&2; exit 2 ;;
esac
exit 0
```

`quality-gate.sh`（带 `stop_hook_active` 守卫的收尾门禁）：

```bash
#!/bin/bash
INPUT=$(cat)
[ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ] && exit 0
cd "$CLAUDE_PROJECT_DIR" || exit 0
ruff check . >/tmp/qg.log 2>&1 && pytest -q >>/tmp/qg.log 2>&1 || {
  echo "质量门禁未过，摘要：" >&2; tail -5 /tmp/qg.log >&2; exit 2
}
exit 0
```

**运行效果**：你让 Claude"给 `refund.py` 加幂等键"，它编辑完自动 ruff format；想 `git push origin main` 被拦；收尾时 pytest/ruff 没过就被打回继续修，过了才放行；全程 Bash 命令沉淀在 `audit.log`；上下文压缩后任务上下文被重新注入。这就是一张"用户无法绕过、模型无需记忆"的确定性质量网。

```mermaid
flowchart TD
    A[SessionStart<br/>自举环境/压缩续命] --> B[UserPromptSubmit<br/>注入最近提交]
    B --> C[PreToolUse<br/>护栏: rm-rf/.env/main]
    C -->|exit 2 阻断| Z[打回模型重试]
    C -->|放行| D[工具执行]
    D --> E[PostToolUse<br/>自动格式化+审计]
    E --> F{Stop 质量门禁}
    F -->|ruff/pytest 未过| G[decision:block<br/>强制继续修复]
    G --> C
    F -->|全绿放行| H[Notification<br/>桌面提醒]
    I[PreCompact<br/>抢救上下文] -.压缩前.-> A
    H --> J[SessionEnd<br/>清理/归档审计]
    style C fill:#ffe0e0
    style F fill:#fff3cd
    style Z fill:#ffe0e0
```

## 延伸阅读

- **官方 Hooks Reference**：`code.claude.com/docs/en/hooks`——全部事件的 stdin/stdout JSON schema、异步 Hook、MCP 工具 Hook。
- **prompt 型与 agent 型 Hook 深挖**：把"判断"外包给子模型/子代理，适合需要语义核对的门禁。
- **Hook + 权限系统协同**：Hook 收紧、deny 规则兜底，二者互补覆盖"动态拦截 + 静态确权"。
- **HTTP Hook 接团队审计平台**：把 `PostToolUse` 事件 POST 到统一服务，做跨团队工具调用合规留痕。

## 训练营适用性评估

- **适用类型**：企业级工程效能训练营、AI 编程工具链进阶营、DevOps/平台工程实战营。
- **不适合**：纯入门营（需先有 Claude Code 基础操作能力）。
- **建议时长**：理论 + 演示 1.5 小时，动手把"质量门禁闭环"跑通 2 小时，合计约半个工作日。
- **考核点**：能否独立写出带 `stop_hook_active` 守卫的 Stop 门禁、能否用 `--debug-file` 排出一个"不触发"的 Hook、能否解释"Hook 能收紧不能放宽"的权限铁律。

## 参考来源

1. [Automate actions with hooks — Claude Code Docs](https://code.claude.com/docs/en/hooks-guide) — 官方 Hooks 实战指南，含全部事件表、五种 handler、退出码语义与排障
2. [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks) — 官方参考：每个事件的精确 JSON 输入/输出 schema、matcher、超时
3. [Claude Code & Agent SDK Hooks (2026) — Morph](https://www.morphllm.com/claude-code-hooks) — 30 个事件的 stdin/exit code/matcher 速查，含 .env 防护范式
4. [disler/claude-code-hooks-mastery — GitHub](https://github.com/disler/claude-code-hooks-mastery) — 逐事件可运行示例仓库，适合对照动手
5. [Claude Code's Hooks: Stop Hoping Claude Remembers Your Standards — Towards AI](https://pub.towardsai.net/claude-codes-hooks-stop-hoping-claude-remembers-your-standards-make-them-enforce-themselves-c8ed2b4bf38f) — 八大事件 + 三例（format-on-edit、保护文件、回合末质量门禁）的工程化解读

> [!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)
