<!-- title: 造自愈Agent：失败自检与自动修复闭环 -->

## 概述

本教程教你用 Claude Code 的 **Agent 模式（`-p` 无头运行）+ Hooks 钩子 + 外层重试循环**，搭建一个「写代码 → 跑测试 → 失败自动诊断 → 自动修复 → 重试」的**自愈型 Agent**。学完你能让 Claude Code 在无人值守下把自己写的代码改到测试通过为止，并掌握权限模式选择、钩子拦截、失控防护三大自动化关键。适合已有 Claude Code 基础、想让 AI 真正「端到端干活」的开发者与 DevOps 工程师。

## 前置条件

- 已安装 Claude Code（`claude --version` 可正常运行），完成过首次鉴权。
- 一台带 Bash 的机器（macOS/Linux/WSL），装好 `jq`（解析钩子 stdin）。
- 一个有测试命令的真实或练习项目（本文用 Node.js + `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 钩子卡住「假完成」。

---

## 第二步：选对权限模式，让 Agent 无人值守

Claude Code 有四种权限模式，选错要么被频繁打断，要么门户大开。`Shift+Tab` 可在交互里循环切换，但自愈 Agent 是无头的，要用命令行参数固化。

```bash
# 查看当前模式相关设置
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` 拿到结构化输出。

```bash
# 最简无头调用：改完即退，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` 设硬上限，防止单轮无限循环（见第七步）。

---

## 第四步：用 PostToolUse 钩子挂「即时自动测试」

钩子（Hooks）是 Claude Code 的确定性自动化层——它不是 LLM 判断，而是**事件触发的脚本**，保证每次都执行。这是它和「在 prompt 里叮嘱 Agent 跑测试」的根本区别：叮嘱可能被忽略，钩子不会。

钩子配置写在 `.claude/settings.json`（项目级）或 `~/.claude/settings.json`（全局）。`PostToolUse` 在每次工具调用**之后**触发，`matcher` 按工具名过滤。

```json
{
  "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` 取出即可。

把格式化换成「跑受影响文件的单元测试」，就是即时自愈的第一道反馈：

```json
{
  "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 钩子。

---

## 第五步：外层 Bash 重试循环——自愈的真正引擎

钩子负责「改完立刻反馈」，但要实现「失败就自动再改、改到通过为止」，需要一个**有边界的外层循环**。这是调度器的核心：

```bash
#!/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
```

运行：

```bash
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`）。

---

## 第六步：用 Stop 钩子卡住「假完成」

Agent 有个通病：明明测试没过，它却自信地说「已修复，任务完成」。第五步的外层循环能兜底，但更优雅的做法是在 Agent **即将结束的那一刻**强制验证——这就是 `Stop` 钩子。

写一个门禁脚本 `stop-gate.sh`：

```bash
#!/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`：

```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}'`。

---

## 第七步：加三道边界，防止 Agent 失控

自动化最大的风险是「跑飞了」。三道闸缺一不可：

**① 危险操作拦截（PreToolUse，退出码 2 = 阻断）**

```json
{
  "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 .` 一键回滚：

```bash
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**：把 `+` 写成了 `-`）：

```js
function add(a, b) {
  return a - b;   // 故意写错
}
module.exports = { add };
```

`test/utils.test.js`：

```js
const assert = require('assert');
const { add } = require('../src/utils');
assert.strictEqual(add(2, 3), 5, 'add(2,3) 应等于 5');
console.log('✅ 测试通过');
```

`package.json`：

```json
{ "scripts": { "test": "node test/utils.test.js" } }
```

`.claude/settings.json`（整合第四、六、七步的钩子）：

```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" }]
    }]
  }
}
```

一键启动：

```bash
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` 只剩一行正确改动：

```diff
-  return a - b;
+  return a + b;
```

把这个模式套到真实仓库，把 `npm test` 换成 `pytest -q`、`go test ./...`、`tsc --noEmit`，就是一条通用的「自愈 CI」管线。

---

## 自愈闭环工作流

```mermaid
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`，保证「正确性」有且只有一个客观来源。

---

## 延伸阅读

- **Claude Code 官方 Hooks 文档**：`code.claude.com/docs/en/hooks`——钩子事件、JSON 输入输出、退出码约定的权威出处。
- **Permission Modes 官方指南**：`code.claude.com/docs/en/permission-modes`——四种模式的精确语义与 `Shift+Tab` 循环。
- **Anthropic：让 Claude Code 更自主地工作**：`anthropic.com/news/enabling-claude-code-to-work-more-autonomously`——官方对自主 Agent 与 Claude Agent SDK 的定位说明。
- **awesome-claude-code**：`github.com/hesreallyhim/awesome-claude-code`——社区钩子、脚本、自动化模式合集。

---

## 训练营适用性评估

- **适用类型**：中高级开发者训练营、DevOps/SRE 自动化训练营、AI 工程实战营。不适合纯零基础——学员需先掌握 Claude Code 基本交互与 Bash。
- **前置模块**：建议排在「Claude Code 入门」「权限配置」「Hooks 基础」之后，作为「自动化」主题的收官项目。
- **教学时长**：讲授 + 跟做约 **3.5–4 小时**（讲解 1 小时、七步逐步实操 2 小时、实战案例调通与讨论 1 小时）。
- **评估方式**：以「坏计算器」能否在 5 轮内自愈通过为通过线；加分项为学员把 `npm test` 换成自己项目栈并跑通真实用例。
- **延伸挑战**：把单机自愈升级为 GitHub Actions 触发（PR 失败自动派 Agent 修），向 CI/CD 自愈管线进阶。

---

## 参考来源

1. [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks) — 钩子事件类型、matcher、JSON 输入输出与退出码（exit 2 阻断、Stop 的 decision=block）的官方规范。
2. [Choose a permission mode — Claude Code Docs](https://code.claude.com/docs/en/permission-modes) — default/acceptEdits/plan/bypassPermissions 四模式语义与 `Shift+Tab` 切换。
3. [Enabling Claude Code to work more autonomously — Anthropic](https://www.anthropic.com/news/enabling-claude-code-to-work-more-autonomously) — 官方对自主 Agent、`-p` 无头模式与 Claude Agent SDK 的定位说明。
4. [Claude Code Autonomous Debugging: 7 Atomic Task Patterns](https://ralphable.com/blog/claude-code-autonomous-debugging-atomic-tasks) — 带 pass/fail 判据的原子任务编排模式，本文重试循环的设计来源之一。
5. [Self-Healing AI Coding Agents — OpenHelm](https://www.openhelm.ai/blog/self-healing-ai-coding-agent) — 自愈反馈环、阈值调参与失控防护的工程实践。

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