本教程教你构建一个"提交自愈"管道:用 Claude Code 的 PostToolUse / PreToolUse 钩子拦截每一次写文件和 git 提交,自动跑 lint、类型检查、格式化;任何不通过都通过退出码 2 反馈给 Claude,由它迭代修复直到门禁全绿。适合想用 -p 模式与 hooks 把质量保障变成无人值守循环的中高级开发者,预计学习时长 90 分钟。
claude --version 可正常输出),熟悉交互式基本用法npm,以及命令行工具 jq(钩子脚本用来解析 stdin JSON,没有则可用 python3 -c 替代)Claude Code 的钩子不是普通的 git hook,它是嵌在 Claude 工具调用生命周期里的拦截器。关键在于退出码的语义——这是整个自愈机制的物理基础:
| 退出码 | 含义 | stdout 去向 | stderr 去向 |
|---|---|---|---|
| 0 | 成功,放行 | 显示给用户(不进 Claude 上下文) | 忽略 |
| 2 | 阻塞错误 | 忽略 | 反馈给 Claude,触发它修复 |
| 其他非 0 | 非阻塞错误 | 显示给用户 | 显示给用户 |
退出码 2 是灵魂:当你把 lint/tsc 的报错打到 stderr 再 exit 2,Claude 会像收到用户反馈一样主动重新编辑文件去消除错误。这就是自愈闭环——不需要人去复制粘贴报错,钩子把"红绿灯"焊进了 agent loop。
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 项目,故意让它容易出错:
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 改成:
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint src --ext .ts"
}
tsconfig.json 关键项:
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"target": "ES2020",
"module": "CommonJS",
"outDir": "dist"
},
"include": ["src"]
}
新建一个"千疮百孔"的源文件 src/calc.ts:
function add(a, b) {
const unused = 42;
return a + b
}
它至少违反三条规则:a/b 隐式 any、unused 未使用、缺分号(取决于 eslint 规则)。这正是让钩子有事可做的"病灶"。
在项目根目录建 scripts/quality-gate.sh。这是整个系统的"裁判":
#!/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
赋予执行权限:
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 太慢 → 见第六步的增量优化
Claude Code 读两层配置:项目级 .claude/settings.json(随仓库共享,团队统一)和个人级 .claude/settings.local.json(被 git 忽略,个人定制)。把质量门禁放项目级,让全员受益。
新建 .claude/settings.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 段
PostToolUse 只覆盖 Claude 自己的编辑,但 git commit 是 Bash 命令,需要在 PreToolUse 里拦。这能防止 Claude(或你)把"暂时通过单文件、整体仍坏"的状态提交上去。
再建一个脚本 scripts/commit-gate.sh:
#!/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 并列):
{
"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 会劝它回头继续修。
到这里你已经有了一个交互式自愈器。但 Round 4 的精髓是无人值守——用 -p(print/headless 模式)把人彻底移出循环。
先认识三个最关键的 flag:
# 非交互执行,限制最多 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 会话,互不污染上下文:
#!/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 兜底防失控。
自愈最怕"修不动还死磕"。三层熔断:
--max-turns(已在第六步用):会话级硬上限,最有效。# 加在 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
exit 0 前加 rm -f "$COUNTER"。可观测性方面,建议钩子脚本把每次校验结果追加到 logs/quality.log,并打时间戳:
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
完整跑通的命令序列:
# 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 字段显示成功。
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
rm -rf、强制走镜像源、拦截外发请求,把钩子变成安全栅栏exit 2 时让 Claude 调用一个"修复 Skill"(如专门的 ESLint 自动修复器),比裸 agent 更可控、更省 token-p 模式、--max-turns、--output-format、permission-mode 的官方说明[!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 等场景全覆盖。
