本教程教你用 Claude Code 的 Agent 模式(-p 无头运行)+ Hooks 钩子 + 外层重试循环,搭建一个「写代码 → 跑测试 → 失败自动诊断 → 自动修复 → 重试」的自愈型 Agent。学完你能让 Claude Code 在无人值守下把自己写的代码改到测试通过为止,并掌握权限模式选择、钩子拦截、失控防护三大自动化关键。适合已有 Claude Code 基础、想让 AI 真正「端到端干活」的开发者与 DevOps 工程师。
claude --version 可正常运行),完成过首次鉴权。jq(解析钩子 stdin)。npm test,可替换为 pytest、go test)。exit code(0 成功、非 0 失败)和标准输入输出流。让 Agent「自愈」不是一句 prompt 就能搞定的魔法,而是三个明确角色的配合。把它想清楚,后面每一行配置都有归属:
| 角色 | 职责 | 在 Claude Code 里由谁扮演 |
|---|---|---|
| 执行者(Agent) | 读代码、改代码、调用工具 | claude -p 无头进程本身 |
| 验证器(Verifier) | 客观判定「现在对不对」 | 测试命令 / 类型检查 / Lint(npm test 等) |
| 调度器(Orchestrator) | 决定「再试一次还是停」 | 外层 Bash 循环 + Stop 钩子 |
自愈的本质是闭环反馈:验证器的失败信号,必须能流回执行者,触发下一轮修改。少了反馈通道,Agent 改完就停,永远是「单次尝试」而非「自愈」。
很多人卡在这一步——以为加了 --dangerously-skip-permissions 就叫自动化,其实那只是「不再问你」,并不会让 Agent 自己验证结果。真正的自动化 = 无人值守 × 自我验证 × 失败重试,三者缺一不可。
预期结果:你能用一句话说清自己要搭的系统里,谁负责改、谁负责判、谁负责循环。
常见问题: - 「Agent 改了但没说改对没」 → 缺验证器,必须有一条非交互、有明确退出码的测试命令。 - 「Agent 一直说完成了,其实没通过」 → 见第六步,用 Stop 钩子卡住「假完成」。
Claude Code 有四种权限模式,选错要么被频繁打断,要么门户大开。Shift+Tab 可在交互里循环切换,但自愈 Agent 是无头的,要用命令行参数固化。
# 查看当前模式相关设置
claude config get defaultMode 2>/dev/null || true
四种模式对比:
| 模式 | 标志 | 文件编辑 | 危险命令/网络 | 适合场景 |
|---|---|---|---|---|
default |
默认 | 每次询问 | 询问 | 人盯着 |
acceptEdits |
--permission-mode acceptEdits |
自动接受 | 仍询问 | 自愈 Agent 首选 |
plan |
--permission-mode plan |
禁止(只读) | 禁止 | 只调研不改 |
bypassPermissions |
--dangerously-skip-permissions |
自动接受 | 自动接受 | 沙箱/容器内激进 |
关键决策:自愈 Agent 推荐用 acceptEdits 而不是 bypassPermissions。前者让 Agent 自由改文件,但碰到 rm -rf、git push --force、读 .env 这类危险操作仍会停下问你——这正是你要的安全垫。bypassPermissions(俗称 YOLO 模式)只在一次性容器或隔离 worktree 里用,因为它对一切放行。
预期结果:你能用一句话向同事解释为什么自愈循环用 acceptEdits 而不是 --dangerously-skip-permissions。
常见问题:
- 「循环跑到一半停下来等确认」 → 某条 Bash 命令或网络请求没在允许列表里。要么改用 acceptEdits,要么把该命令加进 settings.json 的 permissions.allow。
- 「plan 模式下 Agent 不改文件」 → 这是设计如此,plan 模式只读。要让它动手必须切回 acceptEdits 或 default。
自愈调度器要靠机器可读的结果做决策,不能靠「肉眼读终端」。-p(即 --print)让 Claude Code 跑完即退,配合 --output-format 拿到结构化输出。
# 最简无头调用:改完即退,stdout 是纯文本结果
claude -p "把 src/utils.js 里的 add 函数修好" \
--permission-mode acceptEdits
# 拿 JSON 结果,便于脚本解析
claude -p "修复 add 函数" \
--permission-mode acceptEdits \
--output-format json \
> result.json
# result.json 含 result 文本、cost_usd、duration_ms、is_error 等字段
# 流式 JSON:每个事件一行,适合长任务监控
claude -p "重构整个 utils 模块" \
--permission-mode acceptEdits \
--output-format stream-json \
--verbose
--output-format json 返回的退出码同样可靠:进程退出码 0 = 正常结束,非 0 = 出错。但注意——「Agent 正常结束」≠「代码正确」。这正是下一步要补的洞。
预期结果:跑一次上面的命令,能看到 result.json 里有 is_error 字段,并能用 jq -r .is_error result.json 取出布尔值。
常见问题:
- 「流式输出刷屏」 → 加 --include-partial-messages 控制粒度,或干脆用非流式 json。
- 「Agent 跑很久不退出」 → 加 --max-turns 20 设硬上限,防止单轮无限循环(见第七步)。
钩子(Hooks)是 Claude Code 的确定性自动化层——它不是 LLM 判断,而是事件触发的脚本,保证每次都执行。这是它和「在 prompt 里叮嘱 Agent 跑测试」的根本区别:叮嘱可能被忽略,钩子不会。
钩子配置写在 .claude/settings.json(项目级)或 ~/.claude/settings.json(全局)。PostToolUse 在每次工具调用之后触发,matcher 按工具名过滤。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | { read f; case \"$f\" in *.js|*.ts) npx prettier --write \"$f\" >/dev/null 2>&1 ;; esac; }"
}
]
}
]
}
}
这段做的事:Agent 每编辑一个 .js/.ts 文件,钩子立刻用 Prettier 格式化它。stdin 传进来的是一段 JSON,含 tool_input.file_path,用 jq 取出即可。
把格式化换成「跑受影响文件的单元测试」,就是即时自愈的第一道反馈:
{
"type": "command",
"command": "npm test -- --silent >/tmp/quick.log 2>&1 || (echo '⚠️ 测试失败,相关错误已记录' >> /tmp/feedback.txt; tail -5 /tmp/quick.log >> /tmp/feedback.txt)"
}
预期结果:编辑任意 .js 文件后,无需手动操作,文件已被格式化;测试失败时 /tmp/feedback.txt 里累积了错误片段。
常见问题:
- 「钩子没触发」 → 90% 是 JSON 语法错。用 cat .claude/settings.json | jq . 验证能否解析;matcher 用正则,Edit|Write 这种竖线分隔必须有效。
- 「钩子拖慢每次编辑」 → 别在 PostToolUse 跑全量测试套件,只跑该文件的快速测试,全量交给第六步的 Stop 钩子。
钩子负责「改完立刻反馈」,但要实现「失败就自动再改、改到通过为止」,需要一个有边界的外层循环。这是调度器的核心:
#!/usr/bin/env bash
# heal.sh — 自愈循环:让 Agent 把测试改到通过
set -uo pipefail
MAX_RETRIES="${MAX_RETRIES:-5}"
TASK="$1"
FEEDBACK=""
for i in $(seq 1 "$MAX_RETRIES"); do
echo "=== 第 $i / $MAX_RETRIES 轮 ==="
# 角色1·执行者:Agent 在无头模式下改代码
claude -p "${TASK}${FEEDBACK}" \
--permission-mode acceptEdits \
--output-format json \
> /tmp/agent.json 2>/dev/null || true
# 角色2·验证器:客观判定
if npm test > /tmp/test.log 2>&1; then
echo "✅ 第 $i 轮修复成功,测试通过"
exit 0
fi
# 失败 → 角色3·调度器:把错误喂回执行者,进入下一轮
echo "❌ 测试失败,提取错误反馈给 Agent"
FEEDBACK=$'\n\n注意:上一轮改动后测试仍未通过,错误如下,请只针对失败点修复:\n'"$(tail -25 /tmp/test.log)"
done
echo "🔥 达到最大重试次数 $MAX_RETRIES 仍未通过,请人工介入"
echo " 最后一次测试日志:/tmp/test.log"
exit 1
运行:
chmod +x heal.sh
./heal.sh "修复 src/utils.js 中 add 函数的错误,使测试通过"
这个脚本的精髓在 FEEDBACK 变量——每轮把上一轮的真实测试错误拼进下一轮的 prompt,闭环就此闭合。MAX_RETRIES 是防失控的第一道闸(见第七步)。
预期结果:终端依次打印「第 1 轮…失败」「第 2 轮…失败」「第 3 轮…✅修复成功」,退出码为 0;若 5 轮没修好,退出码 1 并留下日志。
常见问题:
- 「Agent 每轮都重复同一个错误修法」 → 反馈文本里追加「不要重复上一轮无效的修改」,并要求它先复述失败原因再动手。
- 「npm test 退出码不准」 → 某些测试框架默认即使有失败也返回 0。检查 package.json 的 test 脚本,确保失败时非 0(如 mocha ... || exit 1)。
Agent 有个通病:明明测试没过,它却自信地说「已修复,任务完成」。第五步的外层循环能兜底,但更优雅的做法是在 Agent 即将结束的那一刻强制验证——这就是 Stop 钩子。
写一个门禁脚本 stop-gate.sh:
#!/usr/bin/env bash
# stop-gate.sh — Agent 想结束前,先跑测试
if npm test > /tmp/gate.log 2>&1; then
exit 0 # 通过 → 允许结束
fi
# 失败 → 输出 JSON,decision=block 把原因喂回 Agent,禁止它停下
REASON=$(tail -15 /tmp/gate.log | tr '\n' ' ' | sed 's/"/\\"/g')
cat <<EOF
{"decision":"block","reason":"测试未通过,禁止结束。请先修复:${REASON}"}
EOF
exit 0
注册到 settings.json:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [{ "type": "command", "command": "bash .claude/stop-gate.sh" }]
}
]
}
}
机制:Agent 每次想停下,Stop 钩子跑测试;不通过就返回 decision: block,这个 reason 会被当作新反馈塞回 Agent,逼它继续改。Claude Code 内置 stop_hook_active 标志防止钩子无限递归——当 Stop 是因自身反馈而再次触发时,系统会安全放行,不会死循环。
于是第五步的 Bash 循环和第六步的 Stop 钩子形成双层自愈:钩子做「单轮内」的反复修,Bash 循环做「跨进程」的总重试。
预期结果:即便 Agent 自信宣布完成,只要测试没过它就会被钩子顶回去继续干;最终真正退出时,测试必然是绿的。
常见问题:
- 「Stop 钩子把 Agent 卡到死」 → 必须配 --max-turns(下一步),且确认 stop_hook_active 逻辑生效;钩子脚本本身要有 exit 0 兜底,别让脚本异常导致行为不可预测。
- 「reason 里 JSON 转义炸了」 → 用 jq 构造 JSON 而非手工拼接:jq -nc --arg r "$REASON" '{decision:"block",reason:$r}'。
自动化最大的风险是「跑飞了」。三道闸缺一不可:
① 危险操作拦截(PreToolUse,退出码 2 = 阻断)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_input.command' | grep -qE 'rm -rf|\\.env|git push --force|curl .*\\| *sh' && exit 2 || exit 0"
}]
}
]
}
}
退出码 2 是 Claude Code 的硬约定——PreToolUse 钩子返回 2,该工具调用被取消,Agent 收到「被阻止」的反馈。
② 单轮硬上限:--max-turns 30,防 Agent 在一个进程里无限折腾。
③ 隔离运行环境:在 Git worktree 或容器里跑自愈循环,改动不污染主分支。改完用 git diff 审查、git checkout . 一键回滚:
git worktree add ../heal-sandbox -b heal/$(date +%s)
cd ../heal-sandbox && ../project/heal.sh "修复 X"
# 满意 → git commit;不满意 → git worktree remove .
预期结果:即便 Agent 试图 rm -rf,调用被即时阻止;单轮最多 30 步;所有改动隔离在 worktree,主仓库零风险。
把以上七步捏成一个完整例子。项目结构:
heal-demo/
├── src/utils.js # 故意写错的 add 函数
├── test/utils.test.js # 会失败的测试
├── package.json # test 脚本
├── .claude/
│ ├── settings.json # 三类钩子
│ ├── stop-gate.sh
└── heal.sh # 自愈循环
src/utils.js(故意有 bug:把 + 写成了 -):
function add(a, b) {
return a - b; // 故意写错
}
module.exports = { add };
test/utils.test.js:
const assert = require('assert');
const { add } = require('../src/utils');
assert.strictEqual(add(2, 3), 5, 'add(2,3) 应等于 5');
console.log('✅ 测试通过');
package.json:
{ "scripts": { "test": "node test/utils.test.js" } }
.claude/settings.json(整合第四、六、七步的钩子):
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{ "type": "command",
"command": "jq -r '.tool_input.command' | grep -qE 'rm -rf|git push --force' && exit 2 || exit 0" }]
}],
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{ "type": "command",
"command": "jq -r '.tool_input.file_path' | grep -qE '\\.js$' && echo '已记录编辑' >> /tmp/edits.log || true" }]
}],
"Stop": [{
"matcher": "",
"hooks": [{ "type": "command", "command": "bash .claude/stop-gate.sh" }]
}]
}
}
一键启动:
cd heal-demo
./heal.sh "让 src/utils.js 的 add 函数返回两数之和,使 npm test 通过"
典型运行轨迹:
=== 第 1 / 5 轮 ===
❌ 测试失败,提取错误反馈给 Agent # Agent 把 - 改成了 +,但可能改错地方
=== 第 2 / 5 轮 ===
✅ 第 2 轮修复成功,测试通过
期间 stop-gate.sh 会在第 1 轮里把 Agent 顶回去——它本想宣布完成,但测试红着,钩子不放行,直到 add 真的返回 a + b。整个过程无人值守,结束时 git diff 只剩一行正确改动:
- return a - b;
+ return a + b;
把这个模式套到真实仓库,把 npm test 换成 pytest -q、go test ./...、tsc --noEmit,就是一条通用的「自愈 CI」管线。
flowchart TD
A[启动 heal.sh<br/>带初始任务] --> B[执行者: claude -p 改代码<br/>acceptEdits 模式]
B --> C[PostToolUse 钩子<br/>即时格式化/快测]
B --> D{Agent 想结束?}
D -->|是| E[Stop 钩子 stop-gate.sh<br/>跑全量测试]
E -->|失败 decision=block| B
E -->|通过| F[Agent 进程正常退出]
F --> G[验证器: npm test<br/>判定通过/失败]
G -->|通过 exit 0| H[✅ 自愈成功 结束]
G -->|失败| I{已达 MAX_RETRIES?}
I -->|否| J[把错误拼进下一轮 prompt<br/>反馈通道闭合]
J --> B
I -->|是| K[🔥 放弃 留日志 待人工]
style H fill:#c8e6c9
style K fill:#ffcdd2
style E fill:#fff9c4
读图要点:内环(B→C→D→E→B)是单进程内由 Stop 钩子驱动的「改-验」循环;外环(A→B…→G→I→J→B)是 Bash 调度器驱动的跨进程重试。两条环都通向同一个验证器 npm test,保证「正确性」有且只有一个客观来源。
code.claude.com/docs/en/hooks——钩子事件、JSON 输入输出、退出码约定的权威出处。code.claude.com/docs/en/permission-modes——四种模式的精确语义与 Shift+Tab 循环。anthropic.com/news/enabling-claude-code-to-work-more-autonomously——官方对自主 Agent 与 Claude Agent SDK 的定位说明。github.com/hesreallyhim/awesome-claude-code——社区钩子、脚本、自动化模式合集。npm test 换成自己项目栈并跑通真实用例。Shift+Tab 切换。-p 无头模式与 Claude Agent SDK 的定位说明。[!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 等场景全覆盖。
