<!-- title: 造质量自愈钩子：拦截失败迭代修复 -->

# Claude Code 质量自愈钩子：让提交失败自动迭代修复到通过

## 概述

本教程教你构建一个"提交自愈"管道：用 Claude Code 的 PostToolUse / PreToolUse 钩子拦截每一次写文件和 git 提交，自动跑 lint、类型检查、格式化；任何不通过都通过退出码 2 反馈给 Claude，由它迭代修复直到门禁全绿。适合想用 -p 模式与 hooks 把质量保障变成无人值守循环的中高级开发者，预计学习时长 90 分钟。

## 前置条件

- 已安装 Claude Code（`claude --version` 可正常输出），熟悉交互式基本用法
- 本机有 Node.js 18+、`npm`，以及命令行工具 `jq`（钩子脚本用来解析 stdin JSON，没有则可用 `python3 -c` 替代）
- 一个可运行的 TS/JS 小项目；若没有，本教程会带你从零建一个
- 理解退出码（exit code）与 stdin/stderr 重定向的基础概念

## 第一步：理解"自愈循环"为什么成立

Claude Code 的钩子不是普通的 git hook，它是嵌在 Claude 工具调用生命周期里的拦截器。关键在于退出码的语义——这是整个自愈机制的物理基础：

| 退出码 | 含义 | stdout 去向 | stderr 去向 |
|--------|------|-------------|-------------|
| 0 | 成功，放行 | 显示给用户（不进 Claude 上下文） | 忽略 |
| 2 | **阻塞错误** | 忽略 | **反馈给 Claude，触发它修复** |
| 其他非 0 | 非阻塞错误 | 显示给用户 | 显示给用户 |

退出码 2 是灵魂：当你把 lint/tsc 的报错打到 stderr 再 `exit 2`，Claude 会像收到用户反馈一样**主动重新编辑文件**去消除错误。这就是自愈闭环——不需要人去复制粘贴报错，钩子把"红绿灯"焊进了 agent loop。

```mermaid
flowchart TD
    A[Claude 调用 Write/Edit] --> B[PostToolUse 钩子]
    B --> C[quality-gate.sh 读 stdin JSON]
    C --> D{改的是 ts/tsx/js?}
    D -->|否| Z[exit 0 放行]
    D -->|是| E[tsc --noEmit 与 eslint]
    E --> F{门禁通过?}
    F -->|是| Z
    F -->|否| G["exit 2 + 报错写 stderr"]
    G --> H[Claude 接收反馈]
    H --> I[自动重新编辑]
    I --> B
    style G fill:#fdd,stroke:#c00
    style Z fill:#dfd,stroke:#0a0
```

PostToolUse 与 PreToolUse 的分工：PostToolUse 负责"写完立刻查"，反馈快、覆盖每次编辑；PreToolUse 挂在 `Bash` 上负责"提交前最终把关"，因为 `git commit` 是通过 Bash 执行的，PreToolUse 能在命令真正运行前拦住它。

## 第二步：搭建演示项目

新建一个最小的 TypeScript 项目，故意让它容易出错：

```bash
mkdir heal-demo && cd heal-demo
npm init -y
npm i -D typescript eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
npx tsc --init
```

把 `package.json` 的 `scripts` 改成：

```json
"scripts": {
  "typecheck": "tsc --noEmit",
  "lint": "eslint src --ext .ts"
}
```

`tsconfig.json` 关键项：

```json
{
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noImplicitAny": true,
    "target": "ES2020",
    "module": "CommonJS",
    "outDir": "dist"
  },
  "include": ["src"]
}
```

新建一个"千疮百孔"的源文件 `src/calc.ts`：

```typescript
function add(a, b) {
  const unused = 42;
  return a + b
}
```

它至少违反三条规则：`a`/`b` 隐式 any、`unused` 未使用、缺分号（取决于 eslint 规则）。这正是让钩子有事可做的"病灶"。

## 第三步：编写质量门禁脚本

在项目根目录建 `scripts/quality-gate.sh`。这是整个系统的"裁判"：

```bash
#!/usr/bin/env bash
# PostToolUse 质量门禁：对刚改过的 TS 文件跑 tsc + eslint
# 失败时 exit 2，把报错反馈给 Claude 让它自愈

set -uo pipefail

# 1) 从 stdin 读取钩子输入 JSON
INPUT="$(cat)"

# 2) 用 jq 抽取字段（无 jq 时换 python3 -c 'import sys,json;...'）
FILE_PATH="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')"

# 3) 只关心源码文件，跳过配置/文档
case "$FILE_PATH" in
  *.ts|*.tsx|*.js|*.jsx) ;;
  *) exit 0 ;;
esac

# 4) 文件可能已删，存在性检查
[ -f "$FILE_PATH" ] || exit 0

# 5) 跑门禁，捕获合并输出
ERRS="$(npx --no-install tsc --noEmit 2>&1; npx --no-install eslint "$FILE_PATH" 2>&1)"

# 6) 有报错就 exit 2，stderr 是 Claude 唯一能看到的反馈
if [ -n "$ERRS" ]; then
  echo "❌ 质量门禁未通过，请修复后继续：" >&2
  echo "$ERRS" >&2
  exit 2
fi

exit 0
```

赋予执行权限：

```bash
chmod +x scripts/quality-gate.sh
```

**预期结果**：手动跑 `echo '{"tool_input":{"file_path":"src/calc.ts"}}' | bash scripts/quality-gate.sh`，应打印一串报错并以非零码退出。这是后面 Claude 能"读到"的内容。

**常见问题**：
- `jq: command not found` → `apt install jq` 或改用 `python3 -c "import sys,json;d=json.load(sys.stdin);print(d['tool_input']['file_path'] or '')"`
- `npx --no-install` 报找不到包 → 去掉 `--no-install` 或确保依赖已装；生产环境建议固定本地二进制路径
- 脚本对每个文件都跑全量 `tsc` 太慢 → 见第六步的增量优化

## 第四步：把钩子注册进 settings.json

Claude Code 读两层配置：项目级 `.claude/settings.json`（随仓库共享，团队统一）和个人级 `.claude/settings.local.json`（被 git 忽略，个人定制）。把质量门禁放项目级，让全员受益。

新建 `.claude/settings.json`：

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "bash scripts/quality-gate.sh"
          }
        ]
      }
    ]
  }
}
```

`matcher` 是工具名正则。`Write|Edit|MultiEdit` 覆盖所有文件写入工具。只写 `"Edit"` 会漏掉新建文件（走 Write）。

**预期结果**：在交互式 Claude 里输入"修复 src/calc.ts 让它通过类型检查和 lint"，你会看到 Claude 改完文件后，终端自动跑出 tsc/eslint 输出；若仍有错，Claude 无需你催促，立即发起下一轮编辑，直到门禁变绿、循环收敛。

**常见问题**：
- 钩子"没触发" → 用 `/hooks` 命令确认配置已加载，检查 matcher 拼写
- 死循环（Claude 改了又错、错了又改）→ 见第七步的熔断
- settings.json 不生效 → JSON 必须合法，一处语法错会静默丢弃整个 hooks 段

## 第五步：加 PreToolUse 提交门禁（守住最后一道关）

PostToolUse 只覆盖 Claude 自己的编辑，但 `git commit` 是 Bash 命令，需要在 PreToolUse 里拦。这能防止 Claude（或你）把"暂时通过单文件、整体仍坏"的状态提交上去。

再建一个脚本 `scripts/commit-gate.sh`：

```bash
#!/usr/bin/env bash
# PreToolUse 提交门禁：拦截 git commit，强制全量检查

set -uo pipefail
INPUT="$(cat)"

CMD="$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')"

# 只关心 commit 类命令
echo "$CMD" | grep -qE '\bgit\b.*\bcommit\b' || exit 0

# 全量门禁（不只是单文件）
if ! npm run -s typecheck 2>&1 || ! npm run -s lint 2>&1; then
  echo "🛑 提交被拦截：全量 typecheck/lint 未通过，请先修复。" >&2
  exit 2
fi
exit 0
```

把它接进 settings.json 的 hooks（与 PostToolUse 并列）：

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [{ "type": "command", "command": "bash scripts/quality-gate.sh" }]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "bash scripts/commit-gate.sh" }]
      }
    ]
  }
}
```

`chmod +x scripts/commit-gate.sh`。现在哪怕 Claude 跑 `git commit -m "..."`，只要全量检查不过，命令根本不会执行，stderr 会劝它回头继续修。

## 第六步：用 -p 模式驱动批量自愈

到这里你已经有了一个交互式自愈器。但 Round 4 的精髓是**无人值守**——用 `-p`（print/headless 模式）把人彻底移出循环。

先认识三个最关键的 flag：

```bash
# 非交互执行，限制最多 20 轮 agent 循环，自动接受文件编辑
claude -p "让 src/ 下所有文件通过 tsc 和 eslint，修完用 git 提交" \
  --permission-mode acceptEdits \
  --max-turns 20 \
  --output-format json \
  --allowedTools "Edit Write Bash" \
  > heal-run.json
```

- `-p`：print 模式，跑完即退，不进交互界面
- `--permission-mode acceptEdits`：自动批准文件编辑（绕过逐个确认），但 Bash 等仍受钩子约束
- `--max-turns 20`：硬上限，防止自愈死循环烧 token
- `--output-format json`：结构化输出，便于脚本解析"是否真的提交成功"

**批量场景**：一个 monorepo 有多个历史遗留文件。写一个驱动脚本 `scripts/batch-heal.sh`，对文件清单逐个起独立 -p 会话，互不污染上下文：

```bash
#!/usr/bin/env bash
set -uo pipefail
FILES="src/legacy-a.ts src/legacy-b.ts src/legacy-c.ts"
for f in $FILES; do
  echo "=== 自愈 $f ==="
  claude -p "只修复 $f 使其通过 tsc 与 eslint，不要动其他文件" \
    --permission-mode acceptEdits \
    --max-turns 10 \
    --allowedTools "Edit Read Bash" \
    > "heal-$(basename $f).log" 2>&1
done
echo "全部完成，结果见 heal-*.log"
```

每个文件一个干净会话、一个上限、一份日志——这正是 hooks（每次 Edit 后 PostToolUse 立刻校验）与 -p（批量、可脚本化、有上限）协同的威力。Claude 改一次、钩子验一次，错了 exit 2 回弹，直到该文件绿了，`--max-turns` 兜底防失控。

## 第七步：防死循环与可观测性

自愈最怕"修不动还死磕"。三层熔断：

1. **`--max-turns`**（已在第六步用）：会话级硬上限，最有效。
2. **脚本内重试计数**：用临时文件记录同一文件的连续失败次数，超阈值就 exit 0 放行并在 stderr 提示人工介入：

```bash
# 加在 quality-gate.sh 报错分支里
COUNTER="/tmp/heal-counter.$(md5sum <<<"$FILE_PATH" | cut -c1-8)"
COUNT=$(cat "$COUNTER" 2>/dev/null || echo 0)
COUNT=$((COUNT + 1))
echo "$COUNT" > "$COUNTER"
if [ "$COUNT" -ge 5 ]; then
  echo "⚠️ $FILE_PATH 连续 5 次未通过，疑似修不动，请人工查看" >&2
  rm -f "$COUNTER"
  exit 0   # 放行，跳出死循环
fi
```

3. **通过即清零**：门禁通过的那次 `exit 0` 前加 `rm -f "$COUNTER"`。

可观测性方面，建议钩子脚本把每次校验结果追加到 `logs/quality.log`，并打时间戳：

```bash
echo "[$(date -Is)] $FILE_PATH result=$?" >> logs/quality.log
```

事后 `grep "result=2" logs/quality.log` 即可统计哪些文件最"难治"，反向指导团队补 lint 规则或写 Skill。

## 实战案例：一个能自愈的提交流水线

把以上拼成一个端到端演示。目录结构：

```
heal-demo/
├── .claude/
│   └── settings.json        # 两个 hook 注册
├── scripts/
│   ├── quality-gate.sh      # PostToolUse 单文件校验
│   ├── commit-gate.sh       # PreToolUse 提交拦截
│   └── batch-heal.sh        # -p 批量驱动
├── src/
│   └── calc.ts              # 故意写坏的文件
├── logs/
├── package.json
└── tsconfig.json
```

完整跑通的命令序列：

```bash
# 1) 启动一次交互式自愈（亲眼看到循环）
claude
# 输入：把 src/calc.ts 修到通过 tsc 和 eslint，然后用一句话提交
# 观察：Claude 编辑→钩子报错→Claude 再改→钩子变绿→git commit→commit-gate 放行

# 2) 退出交互，改用 -p 无人值守重跑（先把 calc.ts 再改坏以验证）
claude -p "修复 src/ 下所有类型与 lint 错误并提交" \
  --permission-mode acceptEdits --max-turns 15 --output-format json

# 3) 多文件批量
bash scripts/batch-heal.sh
```

成功标志：`git log` 出现 Claude 写的提交；`logs/quality.log` 里每个文件最后一次 result=0；`heal-*.log` 的 JSON 里 `result` 字段显示成功。

## 工作流程总览

```mermaid
flowchart LR
    U[开发者 / 定时任务] -->|claude -p| C[Claude Agent]
    C -->|Write/Edit| F[改文件]
    F --> P1[PostToolUse: 单文件门禁]
    P1 -->|exit 2| C
    P1 -->|exit 0| G[继续]
    G -->|Bash: git commit| P2[PreToolUse: 全量门禁]
    P2 -->|exit 2 拦截| C
    P2 -->|exit 0| CM[(commit 落库)]
    CM --> L[logs/quality.log 留痕]
    style P1 fill:#fef3c7
    style P2 fill:#fef3c7
    style CM fill:#d1fae5
```

## 延伸阅读

- **退出码与 stdin JSON 字段全集**：官方 hooks 参考列出所有事件（SessionStart、UserPromptSubmit、Stop、SubagentStop 等）及可注入的上下文
- **PreToolUse 拦截任意命令**：同样的 exit 2 机制可用于禁止 `rm -rf`、强制走镜像源、拦截外发请求，把钩子变成安全栅栏
- **与 Skills 联动**：钩子里 `exit 2` 时让 Claude 调用一个"修复 Skill"（如专门的 ESLint 自动修复器），比裸 agent 更可控、更省 token
- **Agent SDK 替代 -p**：当 -p 的 shell 拼接变得笨重时，改用 Python/TS SDK 以代码控制会话、注入工具、解析结构化输出

## 参考来源

1. [Automate actions with hooks — Claude Code Docs](https://code.claude.com/docs/en/hooks-guide) — 官方钩子指南，退出码语义与配置格式的权威来源
2. [Run Claude Code programmatically — Claude Code Docs](https://code.claude.com/docs/en/headless) — `-p` 模式、`--max-turns`、`--output-format`、permission-mode 的官方说明
3. [claude-code-quality-hook (GitHub)](https://github.com/dhofheinz/claude-code-quality-hook) — PostToolUse 自动跑 lint/类型检查并反馈修复的开源实现，本文脚本的灵感来源
4. [Pre-commit review pipeline for Claude Code (Gist)](https://gist.github.com/Shekhar854/f06278557eb8d0d3da8c4e6317b5c50f) — PreToolUse hook + skill 拦截 git commit 执行质量门禁的实战范例
5. [Claude Code Hooks: Complete Guide with 20+ Examples (2026)](https://aiorg.dev/blog/claude-code-hooks) — 覆盖自动格式化、提交门禁、通知等 20+ 可复制配置的综合教程

## 训练营适用性评估

- **适用类型**：DevOps / 工程效能训练营、AI 辅助研发训练营、代码质量与自动化测试训练营。不适合纯零基础营——需要学员理解 shell、退出码、JSON 解析。
- **教学时长**：建议 1 个半天（约 3–4 小时）。前 60 分钟讲原理与退出码语义；90 分钟动手搭项目、写脚本、注册钩子；剩余时间做批量 -p 实战与死循环调试。
- **进阶作业**：把质量门禁从 TS 扩展到 Python（ruff + mypy）或 Go（golangci-lint）；为钩子加单测；用 Agent SDK 重写 batch-heal.sh 以获得更细的会话控制。

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