<!-- title: 多进程并行批量改造配质量钩子 -->

## 概述

本教程聚焦 Claude Code 自动化能力中最被低估的一组组合拳：**用 `-p` 无头模式启动多个独立进程，靠 `xargs -P` 在操作系统层并行批处理海量文件，再用 `PostToolUse` 钩子在每个编辑后自动跑校验、失败即阻断**。适合已经装通 Claude Code、写过一两个 Skill 或跑过单次 `-p` 命令、想把"一次改一个文件"升级成"一夜改完一个仓库"的中阶用户。学完你能搭出一条带质量闸口、可重跑、可审计的批量改造流水线。

## 前置条件

- 已安装 Claude Code（`claude --version` 可正常输出版本号）
- 一台能跑 Bash 的 Linux/macOS（Windows 用户用 WSL 或 Git Bash）
- `node`、`npx` 可用；用于实战项目的 `eslint` 会临时拉取
- `jq` 已安装（解析钩子入参与结构化输出都靠它）
- 已配置好可用的 Claude 凭证（API Key 或订阅登录）
- 基础：会读 `settings.json`、能看懂 shell 管道

---

## 一、为什么是"多进程"，而不是"子代理"

Claude Code 内部有两种并行思路，先厘清边界，才不会用错工具：

| 维度 | 会话内子代理（Task/Agent） | 多进程 `-p` 并行（本教程） |
|---|---|---|
| 并行单元 | 一个会话内 fork 多个 subagent | 操作系统级多个 `claude` 进程 |
| 上下文 | 子代理可共享父会话摘要 | 每个进程完全独立、互不可见 |
| 协调方式 | 由主模型统一编排 | 由 shell（xargs/GNU parallel）调度 |
| 适合场景 | 一个复杂任务拆成几块强相关子任务 | 大量同质、相互独立的文件级作业 |
| 失败隔离 | 一个子代理崩可能污染主会话 | 一个进程崩不影响其他进程 |

**判断口诀**：任务是"几块拼成一个整体"用子代理；任务是"一百个文件各改各的"用多进程并行。本教程处理后者——比如把一个有 200 个 `.js` 文件的遗留仓库，从回调地狱整体迁到 `async/await`。每个文件独立、彼此无依赖，天然适合进程级并行。

进程级并行的额外好处是**故障隔离彻底**：第 37 个文件改坏了，只是那一个进程的 JSON 输出标记 `is_error`，其余 199 个照常完成；你不必为一个坏文件重启整个批处理。

## 二、理解 `-p` 无头模式的五个关键旗标

无头模式（headless / print mode）是批量管道的发动机。把以下五个旗标吃透，管道就稳了一半。

```bash
claude -p "你的指令" \
  --permission-mode acceptEdits \
  --max-turns 15 \
  --append-system-prompt "附加约束" \
  --output-format json
```

**1. `-p` / `--print`**：非交互模式。读取一条指令、执行、输出、退出，绝不弹交互提示。这是能被 shell 调度的前提。

**2. `--permission-mode`**：无头模式下没有人在终端点"允许"，必须预先决定授权策略。四个取值：
- `default`：每个工具调用按 `settings.json` 的 allow 规则匹配，匹配不到就**卡住**——批处理里这是灾难
- `acceptEdits`：自动放行文件编辑类工具（Edit/Write），其余仍按规则——**批量改造的首选**
- `plan`：只规划不执行，适合先做 dry-run
- `bypassPermissions`：全部放行，速度最快但风险最高，仅用于隔离的可信沙箱

批处理推荐 `acceptEdits` + 一份精简的 allow 清单，既不卡住也不裸奔。

**3. `--max-turns N`**：限制单次调用的最大代理轮次。批处理的铁律是**必须设上限**。否则某个进程遇到诡异文件、陷入"改了又错、错了又改"的死循环，会烧掉整个预算。经验值：简单迁移 8–12 轮，复杂重构 15–20 轮。

**4. `--append-system-prompt`**：在默认系统提示后追加你的约束。这是批量作业里**统一行为**的关键——比如"只改指定文件，禁止新建文件"、"保持对外函数签名不变"、"改动不超过 50 行"。不用它的话，每个进程的"自由发挥"差异会很大。

**5. `--output-format`**：
- `text`（默认）：纯文本结果，人看舒服，机器解析难
- `json`：任务结束后一次性吐一个 JSON 对象，含 `result`、`session_id`、`num_turns`、`is_error`、`total_cost_usd`、`usage`——**批处理的标配**，方便后续审计与重跑
- `stream-json`：流式逐条 JSON，适合实时观测或做进度条

> 陷阱提示：还有一个 `--bare` 旗标，作用是"跳过钩子、技能等的自动发现，换取更快的冷启动"。如果你的管道**依赖钩子**做质量收口（本教程正是如此），就**绝对不能用 `--bare`**——它会把你的钩子一起跳过。`--bare` 适合纯无副作用的查询管道，不适合本场景。

## 三、质量闸口：PostToolUse 钩子工作原理

并行多进程解决了"快"，但快不等于对。200 个文件同时改，谁保证每个改完都能编译、能过 lint？靠人工抽检不现实。**钩子（Hook）就是那条自动质量闸口**。

Claude Code 在代理生命周期的关键节点会触发钩子。最常用的几个：

| 事件 | 触发时机 | 典型用途 |
|---|---|---|
| `SessionStart` | 会话开始 | 注入项目上下文、加载规则 |
| `UserPromptSubmit` | 用户提交指令 | 改写/校验输入 |
| `PreToolUse` | 工具执行前 | 拦截危险命令、二次授权 |
| **`PostToolUse`** | **工具执行后** | **自动 lint/format/测试，失败回灌** |
| `Stop` | 主代理回合结束 | 汇总通知、收尾校验 |
| `SubagentStop` | 子代理结束 | 子任务结果校验 |

我们的核心是 **`PostToolUse` + `matcher: "Edit|Write|MultiEdit"`**：每当模型完成一次文件编辑，钩子脚本立即对该文件跑校验，把结果回灌给模型。

钩子的"语言"是退出码与 JSON：

- **退出码 0**：成功。可选择性在 stdout 输出 JSON 做更精细控制。
- **退出码 2**：**阻断错误**。stderr 的内容会被回灌进模型的上下文——模型会"看见"错误并尝试修复。这是质量闸口的核心机制。
- **其他非零**：非阻断错误，stderr 显示给用户但不影响代理继续。

钩子通过 stdin 收到一段 JSON，关键字段：

```json
{
  "session_id": "abc123",
  "hook_event_name": "PostToolUse",
  "tool_name": "Edit",
  "tool_input": { "file_path": "/proj/legacy/cart.js", "old_string": "...", "new_string": "..." },
  "tool_response": { ... },
  "cwd": "/proj"
}
```

钩子脚本的任务：从 `tool_input.file_path` 取出刚改的文件，决定要不要校验，跑校验，按结果决定退出码。整条链路在 `settings.json` 里注册：

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/scripts/lint-on-edit.sh" }
        ]
      }
    ]
  }
}
```

`$CLAUDE_PROJECT_DIR` 会被自动展开为项目根目录。`matcher` 用正则匹配工具名，写 `"Edit|Write|MultiEdit"` 就只在这三类编辑工具后触发。

## 四、动手：搭建并行批量管道

下面分七步搭出一条完整的"回调→async/await 批量迁移"流水线。先把目录建好：

```bash
mkdir -p async-migrate/{legacy,scripts,out} && cd async-migrate
```

### 步骤 1　准备样本代码

在 `legacy/` 放几个回调风格的 `.js` 文件。这里给一个典型样本 `legacy/cart.js`：

```javascript
// legacy/cart.js —— 经典回调地狱
function fetchCart(userId, cb) {
  db.get(userId, function (err, user) {
    if (err) return cb(err);
    db.get(user.cartId, function (err, cart) {
      if (err) return cb(err);
      db.query(cart.items, function (err, items) {
        if (err) return cb(err);
        cb(null, items);
      });
    });
  });
}
```

再复制几份、改改函数名，凑出 6–10 个文件用于并行演示。真实场景里直接指向你的遗留目录即可。

### 步骤 2　配置 ESLint 基线

```bash
npm init -y >/dev/null
npm install --save-dev eslint >/dev/null
npx eslint --init   # 选默认即可，生成 .eslintrc
```

ESLint 在这里充当"客观裁判"。钩子不评价代码美不美，只问"能不能过 lint"——这是可自动化、可重复的质量底线。

### 步骤 3　编写 lint 钩子脚本

新建 `scripts/lint-on-edit.sh`：

```bash
#!/usr/bin/env bash
# PostToolUse 钩子：编辑 .js 文件后自动跑 eslint，失败即阻断
set -u

input=$(cat)
event=$(printf '%s' "$input" | jq -r '.hook_event_name')
[[ "$event" == "PostToolUse" ]] || exit 0

file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
case "$file_path" in
  *.js) ;;              # 只校验 .js
  *) exit 0 ;;
esac

[[ -f "$file_path" ]] || exit 0   # 文件可能被删，跳过

if ! out=$(npx --no-install eslint --no-eslintrc -c .eslintrc "$file_path" 2>&1); then
  # 退出码 2 = 阻断错误，stderr 回灌给 Claude，迫使其继续修复
  printf 'ESLint 校验失败（%s），请修复后再次编辑：\n%s\n' "$file_path" "$out" >&2
  exit 2
fi
exit 0
```

```bash
chmod +x scripts/lint-on-edit.sh
```

**预期结果**：脚本对非 `.js` 文件、不存在的文件直接放行；对 `.js` 文件跑 eslint，通过则退出 0，失败则把错误打到 stderr 并退出 2。

**常见问题**：
- *钩子不触发*：检查 `chmod +x` 是否执行、`command` 路径是否用绝对路径或 `$CLAUDE_PROJECT_DIR`。
- *eslint 卡在交互*：`npx --no-install` 避免触发安装提示；或直接用本地 `./node_modules/.bin/eslint`。
- *钩子把整个进程拖慢*：`eslint` 比较重，可换更轻的 `node --check`（只查语法）做第一道闸，复杂规则留给最后的 `Stop` 钩子。

### 步骤 4　注册钩子到 settings.json

在项目根创建 `.claude/settings.json`：

```json
{
  "permissions": {
    "allow": ["Bash(npx eslint:*)", "Bash(./node_modules/.bin/eslint:*)", "Edit", "Write", "Read"]
  },
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/scripts/lint-on-edit.sh" }
        ]
      }
    ]
  }
}
```

`permissions.allow` 预授权了 eslint 和编辑类工具，避免 `acceptEdits` 模式下仍因未授权而卡住。

### 步骤 5　单文件试跑与自愈观察

先单文件验证整条链路，别一上来就并行：

```bash
claude -p "把 legacy/cart.js 里所有回调式异步改写成 async/await，保持 fetchCart 的对外签名与错误语义不变。" \
  --permission-mode acceptEdits \
  --max-turns 12 \
  --append-system-prompt "只允许修改 legacy/cart.js 这一个文件，禁止新建任何文件。" \
  --output-format json | jq '{ok: .is_error, turns: .num_turns, cost: .total_cost_usd, session: .session_id}'
```

**预期结果**：模型读取文件 → 编辑 → 触发钩子跑 eslint。若第一版编辑有语法问题，钩子退出 2、错误回灌，模型会再改一轮，直到 eslint 通过或撞到 `--max-turns` 上限。最终 JSON 输出 `is_error: false`、`turns` 通常 2–5。

**常见问题**：
- *模型无视钩子反馈*：确认钩子退出码确实是 2（不是 1）。退出码 1 是非阻断，模型不会因此重试。
- *改动溢出到别的文件*：`--append-system-prompt` 里把"禁止新建/改其他文件"写死，必要时在 `permissions` 里只 allow 目标路径。

### 步骤 6　多进程并行批跑

单文件跑通后，扩到并行。核心是 `xargs -P N`：

```bash
mkdir -p out
find legacy -name '*.js' -printf '%f\n' | \
  xargs -I{} -P 4 sh -c '
    claude -p "把 legacy/{} 里所有回调式异步改写成 async/await，保持函数签名与错误语义不变。" \
      --permission-mode acceptEdits \
      --max-turns 15 \
      --append-system-prompt "只允许修改 legacy/{} 这一个文件，禁止新建文件；改完确保能通过 eslint。" \
      --output-format json > "out/{}.json" 2> "out/{}.log"
  '
```

`-P 4` 表示同时跑 4 个 `claude` 进程。你的 CPU 核数、API 并发上限、预算共同决定这个值。

**预期结果**：4 个进程并发，每个进程独立加载自己的钩子、独立编辑自己的文件、独立跑 eslint。终端看不到交互，全部结果落盘到 `out/`。10 个文件、`-P 4` 大约相当于串行耗时的 1/3 到 1/4（受 API 延迟主导时加速比更接近并发数）。

**常见问题**：
- *API 限流（429）*：并发太高，把 `-P` 调小，或在 `xargs` 外层加 `sleep` 节流。
- *进程互相踩文件*：确保每个 `-p` 实例只改一个文件（靠 prompt + system prompt 双重约束）。本设计的进程隔离前提就是"输入互不重叠"。
- *想看实时进度*：把 `--output-format json` 换成 `--output-format stream-json`，配合 `jq --unbuffered` 过滤事件类型，能看到每个进程的工具调用流。但落盘用 `json` 更省心。

### 步骤 7　结构化结果与失败重跑

批处理跑完，第一件事是生成清单、揪出失败项：

```bash
# 汇总每个文件的结果
for f in out/*.json; do
  jq -r '"\(.is_error)\t\(.num_turns)\t\(.total_cost_usd)\t\(.session_id)\t'"$f" "$f"
done | column -t -N 'error,turns,cost,session,file'
```

对失败项，用 `--resume <session_id>` 接着原上下文重跑，而不是从零开始——模型记得自己改到哪、钩子报了什么错：

```bash
# 假设 cart.json 标记 is_error=true
SID=$(jq -r '.session_id' out/cart.json)
claude -p "继续完成上次的 async/await 迁移，确保最终能通过 eslint。" \
  --resume "$SID" \
  --permission-mode acceptEdits \
  --max-turns 15 \
  --output-format json > out/cart.retry.json
```

> `--resume` 续跑的是**同一会话**，模型带着完整的编辑历史与钩子反馈。这比对失败文件重新发一条新指令更高效——尤其当失败原因是"改到一半撞了轮次上限"时。

## 五、实战案例：一夜迁完 200 文件的完整脚本

把上面七步封装成一个可复用的批处理脚本 `run-batch.sh`：

```bash
#!/usr/bin/env bash
# 用法: ./run-batch.sh <目录> <并发数> <迁移指令文件>
set -euo pipefail

DIR="${1:-legacy}"
PARALLEL="${2:-4}"
PROMPT_FILE="${3:-prompt.txt}"
OUT=out; mkdir -p "$OUT"

INSTRUCTION=$(cat "$PROMPT_FILE")

find "$DIR" -name '*.js' -printf '%P\n' | while read -r rel; do
  printf '%s\n' "$rel"
done | xargs -I{} -P "$PARALLEL" sh -c '
  rel="$1"; instr="$2"; out="out/$rel.json"; log="out/$rel.log"
  mkdir -p "$(dirname "$out")"
  claude -p "对文件 $rel 执行：$instr" \
    --permission-mode acceptEdits \
    --max-turns 15 \
    --append-system-prompt "只允许修改 $rel 这一个文件，禁止新建文件，改完必须能通过 eslint。" \
    --output-format json > "$out" 2> "$log" \
    && echo "[OK]   $rel" \
    || echo "[FAIL] $rel (见 $log)"
' _ {} "$INSTRUCTION"

echo "---- 批处理完成，汇总 ----"
ok=0; fail=0
for f in out/**/*.json out/*.json; do
  [[ -f "$f" ]] || continue
  if jq -e '.is_error == false' "$f" >/dev/null 2>&1; then ok=$((ok+1)); else fail=$((fail+1)); fi
done
echo "成功: $ok　失败: $fail"

# 自动重跑失败项
for f in out/*.json; do
  [[ -f "$f" ]] || continue
  if ! jq -e '.is_error == false' "$f" >/dev/null 2>&1; then
    sid=$(jq -r '.session_id' "$f")
    [[ "$sid" != "null" ]] || continue
    echo "[RETRY] $f session=$sid"
    claude -p "继续完成迁移任务，确保通过 eslint。" --resume "$sid" \
      --permission-mode acceptEdits --max-turns 15 --output-format json > "${f%.json}.retry.json"
  fi
done
```

配套 `prompt.txt`：

```
把该文件中所有基于回调（callback / function(err, ...)）的异步逻辑改写为 async/await。
要求：保持每个对外导出函数的签名与错误语义不变；Promise 化内部调用；移除冗余回调嵌套。
```

这个脚本一次性完成"分发 → 并行执行 → 钩子收口 → 结果汇总 → 失败重跑"全链路。

## 六、流程与架构图

**整体管道架构**：

```mermaid
flowchart LR
    A[find 列出 N 个文件] --> B[xargs -P 4 并行分发]
    B --> P1[claude -p 进程1]
    B --> P2[claude -p 进程2]
    B --> P3[claude -p 进程3]
    B --> P4[claude -p 进程4]

    subgraph 每个进程独立闭环
      P1 --> E1[Edit 编辑文件]
      E1 --> H{PostToolUse 钩子}
      H -->|eslint 通过| C1[继续/完成]
      H -->|eslint 失败 exit 2| E1
    end

    C1 --> O1[out/file1.json]
    P2 --> O2[out/file2.json]
    P3 --> O3[out/file3.json]
    P4 --> O4[out/file4.json]

    O1 --> S[汇总清单]
    O2 --> S
    O3 --> S
    O4 --> S
    S --> R{is_error?}
    R -->|true| RR[--resume 重跑]
    R -->|false| DONE[完成]
```

**钩子内部决策流**：

```mermaid
sequenceDiagram
    participant M as 模型(代理)
    participant T as Claude Code 运行时
    participant K as lint-on-edit.sh
    participant L as eslint

    M->>T: 调用 Edit(file_path)
    T->>T: 执行编辑
    T->>K: PostToolUse 事件(stdin=JSON)
    K->>K: 解析 file_path，判断 .js
    K->>L: eslint file_path
    L-->>K: 结果
    alt 通过
        K-->>T: exit 0
        T-->>M: 编辑成功，继续
    else 失败
        K-->>T: stderr=错误详情, exit 2
        T-->>M: 阻断错误回灌上下文
        M->>T: 再次调用 Edit 修复
    end
```

## 七、性能与成本调优

- **并发数 `-P`**：起步建议 `min(CPU核数-2, 8)`。瓶颈通常在 API 速率限制而非本地 CPU，先用 `-P 2` 试跑，逐步上调直到出现 429。
- **`--max-turns`**：设得过松会烧钱，过紧会误杀。先观察单文件平均轮次（从 `num_turns` 字段统计），把上限设为"平均值 × 2 + 3"。
- **冷启动成本**：每个 `claude -p` 都有一次进程冷启动 + 上下文加载。文件少于 ~15 个时，进程级并行的启动开销可能不划算，改用会话内子代理更省。
- **预算护栏**：关注每个 JSON 的 `total_cost_usd`，批量前先在 5 个文件上估单价，乘以总数得到预算预期，避免意外。
- **`--bare` 的取舍**：如果你的某条管道**不需要钩子**（比如纯查询、纯分析），加 `--bare` 能显著降低冷启动开销；本教程的迁移管道强依赖钩子，不能用。

## 八、常见问题速查

- **进程卡住不动**：几乎都是 `--permission-mode` 没设对，导致无头模式下等不到授权。改用 `acceptEdits` 并在 `permissions.allow` 里预授权所需命令。
- **钩子没生效**：确认 `settings.json` 在项目 `.claude/` 下、JSON 格式合法、`command` 路径正确、脚本有执行位、**没有**叠加 `--bare`。
- **结果 JSON 解析失败**：`-p` 的 stderr 会进 `.log`，stdout 才是干净的 JSON。检查是不是把 stderr 也重定向进了 JSON 文件。
- **同文件被多个进程同时改**：根本原因是文件清单有重叠。确保 `find` 产出的路径在进程间互斥，prompt 里也钉死"只改这一个文件"。
- **想统一风格再放行**：把单一 eslint 换成 `prettier --check` 或自定义脚本，钩子逻辑不变——只要"失败即 exit 2"。

## 延伸阅读

- Claude Code 官方《Run Claude Code programmatically》（headless / Agent SDK）：`https://code.claude.com/docs/en/headless`
- Claude Code 官方《Hooks reference》（事件、schema、退出码）：`https://code.claude.com/docs/en/hooks`
- Claude Code 官方《Automate actions with hooks》（钩子快速上手与示例）：`https://code.claude.com/docs/en/hooks-guide`
- Claude Code 官方《CLI reference》（全部旗标速查）：`https://code.claude.com/docs/en/cli-reference`
- Claude Code 官方《Manage sessions》（`--resume` / `--continue` 会话续跑）：`https://code.claude.com/docs/en/sessions`

## 训练营适用性评估

- **适合类型**：中阶工程师训练营、DevOps/平台工程训练营、AI 辅助研发实战营。不建议作为零基础第一课——需要先具备 Claude Code 基本使用与 shell 基础。
- **前置衔接**：建议放在"安装配置入门"与"Skill/MCP 开发"之后，作为"规模化自动化"主题的核心实战。
- **教学时长**：讲授 + 跟练约 **3–4 学时**（含环境准备 0.5h、原理讲解 1h、七步实操 1.5h、调优与答疑 1h）。
- **考核点**：学员能独立搭出带钩子的并行管道、能解释 `exit 2` 的阻断机制、能根据 `num_turns` 与 `total_cost_usd` 调参、能用 `--resume` 处理失败项。
- **可拓展作业**：把 eslint 闸口换成"编辑后自动跑单元测试"（`PostToolUse` 触发 `npm test`），或追加一个 `Stop` 钩子做整仓库最终校验并发 Slack 通知。

## 参考来源

1. [Run Claude Code programmatically — Claude Code Docs](https://code.claude.com/docs/en/headless) — 官方无头模式与 Agent SDK 编程入口，`--bare`、`--permission-mode` 等旗标权威说明
2. [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks) — 钩子事件、JSON 输入输出 schema、退出码（0/2/其他）与 `$CLAUDE_PROJECT_DIR` 展开的官方规范
3. [Claude Code Hooks: PreToolUse & PostToolUse Tutorial — paul-schick.com](https://paul-schick.com/posts/claude-code-hooks-pretooluse-posttooluse/) — 把 CLAUDE.md 约束落成可执行闸口的实战范例
4. [Claude Code Batch Processing Complete Guide — smartscope.blog](https://smartscope.blog/en/generative-ai/claude/claude-code-batch-processing/) — `/batch` 命令与 `claude -p` 大规模批处理对比，含并发与重跑策略
5. [CLI reference — Claude Code Docs](https://code.claude.com/docs/en/cli-reference) — `--output-format`、`--max-turns`、`--append-system-prompt`、`--resume` 等全部旗标速查

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