<!-- title: 全事件钩子编排：自动开发守卫 -->

## 概述

本教程面向已经写过一两个钩子、却只停留在"拦截 + 格式化"两件套的 Claude Code 使用者。读完之后，你将把 Claude Code 的 **30 余个生命周期事件**当作一条**事件总线（event bus）**来编排，围绕一次完整的开发回合，串联"会话启动注入 → 提示词预处理 → 工具拦截 → 工具后验证 → 回合收尾 → 压缩保护 → 会话清理"七个阶段，搭出一套**全自动开发守卫**。适用人群：希望用 hooks 把团队规范、安全策略、上下文管理工程化，并最终落地到 CI 里的中高级开发者。

## 前置条件

- Claude Code 已安装并跑通至少一个项目（参见轮次 1 的首跑入门）。
- 熟悉 `settings.json` 的三份作用域：用户级 `~/.claude/settings.json`、项目级 `.claude/settings.json`（可提交）、本地级 `.claude/settings.local.json`（被 gitignore）。
- 能写基础 shell 脚本，会用 `jq` 解析 JSON（macOS `brew install jq`，Debian/Ubuntu `apt-get install jq`）。
- 已了解 PreToolUse / PostToolUse 的最基本用法——本教程不再重复"双钩护栏"的内容，而是站在它之上做**全生命周期编排**。

---

## 一、心智模型：把钩子当成事件总线

很多教程一上来就教"加一个 PreToolUse"，导致初学者以为钩子只是"两个开关"。真正的全貌是：Claude Code 在会话里**持续抛出事件**，你的脚本/HTTP 端点/MCP 工具/LLM prompt/子代理都是这些事件的**订阅者**。它本质上是一个进程内的事件驱动架构（EDA）。

官方把全部事件按**触发节奏**分成三档，记住这三档，编排时就不容易错位：

| 节奏 | 代表事件 | 适合做什么 |
|------|----------|-----------|
| 每会话一次 | `SessionStart` / `SessionEnd` / `Setup` | 环境准备、上下文注入、退出清理 |
| 每回合一次 | `UserPromptSubmit` / `Stop` / `StopFailure` / `PostToolBatch` | 提示词预处理、回合收尾、批量后处理 |
| 每次工具调用 | `PreToolUse` / `PostToolUse` / `PostToolUseFailure` / `PermissionRequest` / `PermissionDenied` | 拦截、审计、格式化、权限自动化 |

除此之外还有一批"旁路"事件：`Notification`（桌面提醒）、`MessageDisplay`（流式输出观察）、`SubagentStart`/`SubagentStop`（子代理生命周期）、`TaskCreated`/`TaskCompleted`（任务列表）、`InstructionsLoaded`（CLAUDE.md/rules 载入）、`ConfigChange`（配置被外部改动）、`CwdChanged`/`FileChanged`（目录与文件变化）、`WorktreeCreate`/`WorktreeRemove`（工作树生命周期）、`PreCompact`/`PostCompact`（压缩前后）、`Elicitation`/`ElicitationResult`（MCP 反向提问）、`TeammateIdle`（团队代理空闲）、`UserPromptExpansion`（斜杠命令展开前）。这套事件足够你构建一个"开发守卫中枢"。

## 二、配置三层架构：event → matcher → handler

一个钩子永远由三层嵌套构成。理解了这三层，写任何钩子都是套模板：

```json
{
  "hooks": {
    "PreToolUse": [                        // 第1层：钩子事件
      {
        "matcher": "Bash",                 // 第2层：matcher 过滤组
        "hooks": [
          {
            "type": "command",             // 第3层：handler
            "if": "Bash(rm *)",            // 可选：更细的条件
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}
```

**第 1 层——钩子事件**：选择在哪个生命周期点响应。

**第 2 层——matcher 过滤组**：决定这一组在什么时候激活。matcher 的求值规则只有三条，务必记牢：

- `*`、`""` 或省略 → 匹配该事件的**全部**出现；
- **只含**字母、数字、下划线和 `|` → 当作**精确字符串或 `|` 分隔的精确列表**，例如 `Bash`、`Edit|Write`；
- **含任何其它字符** → 当作 **JavaScript 正则**，例如 `^Notebook`、`mcp__memory__.*`。

注意一个高频踩坑点：MCP 工具名形如 `mcp__<server>__<tool>`。想匹配某服务器的全部工具必须写 `mcp__memory__.*`——少了 `.*` 会被当成精确字符串，匹配到 0 个工具。

每个事件的 matcher 过滤的字段不同：工具类事件过滤 `tool_name`；`SessionStart` 过滤启动方式（`startup`/`resume`/`clear`/`compact`）；`Notification` 过滤通知类型（`permission_prompt`/`idle_prompt`/`auth_success`/`elicitation_dialog`/`elicitation_complete`/`elicitation_response`）；`ConfigChange` 过滤配置来源；`StopFailure` 过滤错误类型（`rate_limit`/`overloaded`/`authentication_failed`/`billing_error`/`max_output_tokens` 等）。而 `UserPromptSubmit`、`PostToolBatch`、`Stop`、`TaskCreated`、`TaskCompleted`、`WorktreeCreate/Remove`、`CwdChanged`、`MessageDisplay` 这些**不支持 matcher**，写了会被静默忽略——别浪费时间调试。

**第 3 层——handler**：真正执行的东西。`type` 目前支持五种，覆盖几乎所有集成场景：

| type | 形态 | 适用场景 |
|------|------|---------|
| `command` | 本地 shell 命令/脚本 | 绝大多数场景，最快、最可控 |
| HTTP endpoint | 向 URL 发 POST | 接入远程服务、webhook、企业内网 API |
| MCP tool | 调用 MCP 工具 | 复用既有 MCP 能力，无需重写逻辑 |
| prompt | LLM prompt 钩子 | 让模型做判断（如多准则审查），返回结构化 JSON |
| agent | 子代理钩子 | 需要长上下文/多步骤推理的复杂判定 |

## 三、决策控制：让钩子"会说话"

钩子不只是"做点事"，它还能**反过来指挥 Claude Code**。掌握决策控制，是"护栏"和"智能守卫"的分水岭。

**退出码（exit code）语义**——对所有 `command` 钩子通用：

- `exit 0` 且无 stdout → 静默通过，走正常流程（**注意：静默 ≠ 批准**，工具调用仍受正常权限流约束）；
- `exit 2` → 主动阻断。对不同事件含义不同：在 `PreToolUse` 里是阻止该工具调用，在 `UserPromptSubmit` 里是拒绝该提示词，在 `Stop` 里是阻止回合结束。Claude 会收到 stderr 上的原因，从而**自我修正**；
- 其它非零码 → 非阻断错误，记入日志。

**JSON 输出（更精细的控制）**——当退出码不够用时，在 stdout 打印一个 JSON 对象。以 `PreToolUse` 为例，最关键字段是 `permissionDecision`：

```json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Destructive command blocked by hook"
  }
}
```

`permissionDecision` 取值 `deny`（拒绝，带 reason 反馈给模型）/ `approve`（直接批准，绕过权限弹窗）/ 不返回（走正常权限流）。`Stop` 事件则可返回 `{"decision": "block"}` 强制继续对话——这正是实现"提交前必须过测试"这类约束的钥匙。

一个关键认知：**钩子能拒绝，但不能默认批准**。`exit 0` 只是"我没意见"，真正放行与否仍由权限系统决定。想自动批准，必须显式返回 `approve`。

## 四、`if` 字段：省掉进程派生开销

matcher 只能按工具名粗筛。当你要在"Bash 工具中只有 `rm` 子命令才拦截"时，用 handler 上的 `if` 字段做二次过滤：

```json
{ "type": "command", "if": "Bash(rm *)", "command": ".../block-rm.sh" }
```

`if` 采用权限规则语法，能同时匹配工具名与参数：`Bash(git *)` 匹配任意 git 子命令，`Edit(*.ts)` 只对 TypeScript 文件触发。**匹配失败时脚本根本不会启动**，省掉一次进程派生。在高频事件（如 `PostToolUse`）上，这一优化能显著降低延迟。

## 五、七阶段编排：把守卫铺满整条回合

下面用一个真实可运行的渐进式项目，把七个阶段串成一条完整工作流。每段代码都可直接放进 `.claude/settings.json` 或对应脚本。

### 阶段 1 · 会话启动注入（SessionStart）

用 `startup` matcher 在会话开始时把"项目宪法"动态注入上下文。脚本的 stdout 会原样进入 Claude 的上下文：

```json
{
  "hooks": {
    "SessionStart": [{
      "matcher": "startup",
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/inject-context.sh"
      }]
    }]
  }
}
```

`inject-context.sh` 动态产出当前 sprint 与最近提交：

```bash
#!/bin/bash
echo "## 当前约束（自动注入）"
echo "- 包管理器统一用 pnpm，禁止 npm/yarn"
echo "- 提交前必须跑 pnpm test"
echo "### 最近 5 次提交"
git log --oneline -5 2>/dev/null
echo "### 本周 sprint：认证模块重构"
```

```mermaid
flowchart TD
    A[SessionStart: startup] --> B[inject-context.sh]
    B --> C{stdout 注入上下文}
    C --> D[Claude 拿到项目宪法+最新提交]
```

### 阶段 2 · 提示词预处理（UserPromptSubmit）

用户每按一次回车，`UserPromptSubmit` 都会触发。它能**改写、拒绝、附加**提示词。一个常见用途是给每个提问自动追加"思考前先复述需求"的指令：

```json
{
  "hooks": {
    "UserPromptSubmit": [{
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/augment-prompt.sh"
      }]
    }]
  }
}
```

```bash
#!/bin/bash
# augment-prompt.sh：读取原始 prompt，追加规范提醒
echo "请先复述你的理解再动手。提交相关改动前必须通过 pnpm test。" 
```

若要**拒绝**某个提示词（例如检测到生产环境关键词），脚本输出原因并以 `exit 2` 退出即可。

### 阶段 3 · 工具前拦截（PreToolUse）

这是守卫的"门卫"。下面这个脚本组合了文件保护与危险命令拦截，返回结构化 `deny`：

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{
          "type": "command",
          "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect-files.sh"
        }]
      },
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "if": "Bash(rm *)",
          "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
        }]
      }
    ]
  }
}
```

`protect-files.sh`（注意 `exit 2` 会把 stderr 反馈给模型）：

```bash
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED=(".env" "package-lock.json" ".git/" "pnpm-lock.yaml")
for p in "${PROTECTED[@]}"; do
  if [[ "$FILE_PATH" == *"$p"* ]]; then
    echo "Blocked: $FILE_PATH 命中受保护模式 '$p'，如需修改请人工介入" >&2
    exit 2
  fi
done
exit 0
```

`block-rm.sh` 用 JSON 精准拒绝 `rm -rf`，但放过普通 `rm file.txt`：

```bash
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "rm -rf 属于破坏性命令，已由守卫拦截"
    }
  }'
else
  exit 0
fi
```

### 阶段 4 · 工具后验证（PostToolUse）

写完文件立刻校验，避免错误累积。matcher `Edit|Write` 让它只在文件编辑后触发，用 `jq` 提取路径喂给 linter：

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

### 阶段 5 · 回合收尾收口（Stop）

回合结束是"强制质量门"的最佳挂载点。返回 `{"decision":"block"}` 能阻止回合结束，把控制权还给 Claude——这正是"测试没过就不许停"的实现：

```bash
#!/bin/bash
# stop-gate.sh：回合结束时跑一次类型检查，失败则阻止结束
if ! pnpm exec tsc --noEmit > /tmp/stop-gate.log 2>&1; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "Stop",
      decision: "block",
      reason: "类型检查未通过，请修复后再结束"
    }
  }'
fi
exit 0
```

> 经验法则：`Stop` 里只做**快速**检查（类型检查、lint）。完整测试套件建议放到下一节的 async hook，避免阻塞交互。

### 阶段 6 · 上下文压缩保护（PreCompact + SessionStart compact）

压缩会丢失细节。两道防线配合：`PreCompact` 在压缩前把关键状态落盘，`SessionStart` 的 `compact` matcher 在压缩后重注入：

```json
{
  "hooks": {
    "PreCompact": [{
      "hooks": [{
        "type": "command",
        "command": "echo \"[$(date)] 压缩前快照: $(git rev-parse --short HEAD)\" >> .claude/snapshots.log"
      }]
    }],
    "SessionStart": [{
      "matcher": "compact",
      "hooks": [{
        "type": "command",
        "command": "echo '⚠️ 刚发生压缩：请记得用 pnpm、提交前必过测试、当前 sprint=认证重构'"
      }]
    }]
  }
}
```

### 阶段 7 · 会话结束清理（SessionEnd）

`SessionEnd` 的 matcher 区分退出原因（`clear`/`resume`/`logout`/`prompt_input_exit` 等）。在真实退出时归档日志、清理临时分支：

```json
{
  "hooks": {
    "SessionEnd": [{
      "matcher": "other",
      "hooks": [{
        "type": "command",
        "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/cleanup.sh"
      }]
    }]
  }
}
```

把七阶段合起来，就得到一张完整的事件总线流转图：

```mermaid
flowchart LR
    S1[SessionStart startup<br/>注入上下文] --> S2[UserPromptSubmit<br/>提示词预处理]
    S2 --> S3[PreToolUse<br/>拦截/门禁]
    S3 --> S4[工具执行]
    S4 --> S5[PostToolUse<br/>格式化/校验]
    S5 --> S6{PostToolBatch<br/>批次完成?}
    S6 -->|否| S3
    S6 -->|是| S7[Stop 门<br/>类型检查]
    S7 -->|失败 block| S3
    S7 -->|通过| S8[SessionEnd<br/>清理归档]
    C1[PreCompact 落盘] -.压缩.-> C2[SessionStart compact<br/>重注入]
    style S3 fill:#fde2e2
    style S7 fill:#fff3cd
```

## 六、进阶：五种 handler 与异步钩子

**prompt 钩子**适合需要语义判断的场景。比如一个多准则 `Stop` 钩子，让模型按 schema 评估"这次改动是否破坏了向后兼容"，返回结构化结论再决定是否 block——这比写正则脚本鲁棒得多。

**HTTP 钩子**把事件 POST 到远端，可对接企业审计平台、Slack webhook、内网合规系统，无需在本地落代码。

**MCP 工具钩子**直接复用既有 MCP 服务器的能力做判定，逻辑零重复。

**agent 钩子**派一个子代理做需要长上下文的复杂推理判定。

**异步钩子（async hooks）**是编排长任务的关键：把它声明为异步，Claude Code 不会等它返回就继续，脚本在后台跑。典型用法是 `FileChanged` 后跑完整测试套件——既不阻塞交互，又能在文件一变就自动验证：

```json
{
  "hooks": {
    "FileChanged": [{
      "matcher": "src/**/*.ts",
      "hooks": [{
        "type": "command",
        "async": true,
        "command": "pnpm test --silent > .claude/last-test.log 2>&1"
      }]
    }]
  }
}
```

注意 `FileChanged` 的 matcher 是**字面文件名**（`|` 分隔），不是正则；`src/**/*.ts` 这类通配行为以官方 `FileChanged` 条目为准，落地前先用 `/hooks` 菜单确认已注册。

## 七、常见问题与排查

- **钩子没触发？** 八成是 matcher 写错：MCP 工具漏了 `.*`、或在不支持 matcher 的事件（`Stop`/`UserPromptSubmit` 等）上写了 matcher 被静默忽略。用 `/hooks` 菜单逐项核对。
- **脚本不执行？** shell 脚本必须 `chmod +x`；路径用 `${CLAUDE_PROJECT_DIR}` 而非相对路径。
- **拦截了但模型不停撞墙？** 把 `exit 2` 的原因写具体（"用 X 替代"），模型才有修正方向。
- **`Stop` 门太慢拖慢交互？** 改用 async hook 或只放最快的检查；重活挪到 `FileChanged` 异步执行。
- **权限管理被钩子绕过？** 企业可用 `allowManagedHooksOnly` 关闭用户/项目/插件钩子，仅允许经审批的托管钩子，并通过 plugin marketplace 分发可信钩子。

## 八、实战案例：一键启用的"守卫中枢"插件

把上述七阶段抽成一个可复用插件 `dev-guard`，目录结构：

```
dev-guard/
├── .claude-plugin/plugin.json
├── hooks/hooks.json          # 七阶段事件注册
└── scripts/                  # 全部可执行脚本
    ├── inject-context.sh
    ├── augment-prompt.sh
    ├── protect-files.sh
    ├── block-rm.sh
    ├── stop-gate.sh
    └── cleanup.sh
```

`hooks/hooks.json` 把七阶段一次性声明，团队任何人启用插件即获得一致的守卫行为，无需各自改 `settings.json`。配合 `SessionStart` 注入的"项目宪法"，从单人工具升级为**团队级、可审计、可回滚**的自动化基线——这正是 hooks 从"个人小抄"走向"工程化基础设施"的关键一跃。

## 延伸阅读

- **Automate actions with hooks（官方 quickstart）** —— 20+ 可复制配置的最佳起点。
- **Hooks reference（官方完整参考）** —— 逐事件的 input schema 与 decision control，案头必备。
- **prompt 钩子与 agent 钩子** —— 让钩子具备语义判断能力，突破脚本能表达的上限。
- **`FileChanged` / `CwdChanged` / `CLAUDE_ENV_FILE`** —— 与 direnv/devbox 联动做反应式环境管理。
- **async hooks** —— 把长任务移出关键路径，兼顾安全与流畅。

## 训练营适用性评估

- **适用类型**：面向中高级开发者的"工程化"进阶营、DevOps/SRE 自动化营、企业级 AI 编码规范落地营。不建议作为零基础首营（需先过 hooks 双件套）。
- **预计时长**：讲授 + 动手 3～4 学时（1 学时心智模型与三层架构，2 学时七阶段动手搭建，0.5 学时插件化打包）。
- **验收标准**：学员能独立用 `SessionStart`+`PreToolUse`+`PostToolUse`+`Stop` 四个事件搭出一条"门禁—校验—收口"闭环，并用 `/hooks` 菜单自检注册结果。
- **延伸课题**：把守卫中枢做成插件，结合企业 `allowManagedHooksOnly` 演示托管钩子分发。

## 参考来源

1. [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks) — 30+ 钩子事件的完整 schema、matcher 规则、decision control 与 async/prompt/agent 钩子权威定义。
2. [Automate actions with hooks — Claude Code Docs](https://code.claude.com/docs/en/hooks-guide) — 官方 quickstart，含保护文件、自动格式化、压缩重注入、direnv 联动等可复制示例。
3. [Claude Code Hooks: Complete Guide with 20+ Examples — aiorg.dev](https://aiorg.dev/blog/claude-code-hooks) — 从零到生产的实战配置集，覆盖安全检查、格式化、测试编排。
4. [Claude Code Hooks — Complete Event-Driven Automation Guide — pyshine.com](https://pyshine.com/claude-code-hooks-guide/) — 以事件驱动视角系统讲解 handler 类型与分发器模式。
5. [Hooks System and Event-Driven Automation — DeepWiki](https://deepwiki.com/FlorianBruniaux/claude-code-ultimate-guide/5.4-hooks-system-and-event-driven-automation) — 事件驱动自动化层的架构拆解与生命周期拦截机制说明。

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