<!-- title: Headless批量迁移机：自动化改100文件 -->

## 概述

本教程教你把 Claude Code 当成一台**无人值守的批量迁移机**：用 headless 的 `-p` 模式接进 Unix 管道，让 Agent 逐个消化 100+ 个文件，自动拆任务、改代码、跑测试、原子提交，失败了还能断点续跑。适合有 CLI 基础、想用 Agent 自动化处理「量大、机械、但每个文件又要看上下文」的迁移与重构工作的开发者——例如统一 API 签名、框架升级、SQL 参数化、日志规范化。

读完本文，你能独立搭出一条「清单驱动 + 自拆解 + 可恢复 + 可观测」的批量迁移流水线，并理解其中每个旋钮为什么这样拧。

## 前置条件

- 已安装 Claude Code（v2.1.x，本文事实以 2.1.128+ 行为为准），能跑通 `claude -p "hi"`。
- 一台 macOS/Linux 终端，熟悉 `bash`、`git`、管道、退出码。
- 一个纳入 git 的待迁移仓库（**必须干净**，迁移机的护栏依赖版本控制兜底）。
- 能区分三类工作的判断力：纯文本替换（用 `sed`）、结构化生成（用脚本）、**语义级迁移**（交给 Agent）。

> 核心判断：如果 `sed` 能安全搞定，就别请 Agent。迁移机的价值在于**每个文件的改动都需要读上下文才能决定怎么改**——这正是正则的盲区、Agent 的主场。

## 第 1 步：认识迁移机的心脏——headless 与 stdin

`claude -p`（即 `--print`）是非交互模式：读一次输入、干完活、把结果打到 stdout、然后退出。它**读 stdin、写 stdout**，因此可以像 `cat`、`grep` 一样被塞进任何 Unix 管道。

```bash
# 最朴素的形态：把一个文件喂给 Agent，让它迁移
cat src/users/repo.ts | claude -p "把这个文件里所有 db.query(sql) 改成 db.execute(sql, params)，把字符串拼接的变量抽成参数绑定。只输出改后的完整文件，不要解释。"
```

关键事实（直接影响你怎么设计流水线）：

- **stdin 有 10MB 上限**（v2.1.128 起）。超过会非零退出并报错。对策：单文件喂入几乎不会触顶；别试图把整个仓库 pipe 进去，改成**在 prompt 里写文件路径**，让 Agent 自己用工具读。
- **stdout 即返回值**。可以用 `>` 重定向、用 `tee` 同时落盘与上屏、用 `jq` 解析。
- **退出码可信赖**。成功为 0，失败/超 stdin 上限为非零，便于在 shell 脚本里做条件分支。

一个"只读 + 吐结果"的最小验证，确认环境就绪：

```bash
echo "返回 JSON：{\"ok\":true}" | claude -p --output-format json "原样回显输入" | jq -r .result
# 预期输出：返回 JSON：{"ok":true}
```

如果这一步出不来结果，先别往下走——多半是登录、网络或版本问题。

## 第 2 步：让 Agent 自己拆活——系统提示词 + TodoWrite

批量迁移的失败，多半不是单文件改错，而是**Agent 在第 40 个文件时忘了最初约定的规则**。解法：用 `--append-system-prompt` 把"迁移规约"钉死在每个进程的系统提示里，再用 TodoWrite 让 Agent 把一个文件拆成有序子步骤。

把规约写成文件（比命令行字符串更易维护、可 diff、可入 git）：

```text
# migration-rules.md
你是 SQL 参数化迁移执行器。对每个目标文件：
1. 用 Read 读取完整文件。
2. 用 TodoWrite 拆出子任务：定位调用点 → 改写 → 自检 → 运行类型检查。
3. 每个非查询语句（INSERT/UPDATE/DELETE）必须参数化：db.query(`...${x}...`) → db.execute('... ?', [x])。
4. 纯 SELECT 且无外部拼接可保留，但在 TodoWrite 里标注"已评估，保留"。
5. 禁止改测试快照文件（*.snap）。禁止改迁移脚本本身。
6. 改完跑 `pnpm tsc --noEmit`，有错就修，修不掉就回滚该文件并在最终输出标记 SKIPPED。
7. 最后一行只输出一个 JSON：{"file":"<path>","status":"done|skipped","changes":<int>}。
```

启动单个文件迁移时挂上它：

```bash
claude -p "迁移 src/users/repo.ts" \
  --append-system-prompt-file ./migration-rules.md \
  --allowedTools "Read,Edit,Bash(pnpm tsc --noEmit)" \
  --max-turns 30
```

三个旋钮的意义：

- `--append-system-prompt-file`：规约进系统层，每轮都看得见，抗"长任务漂移"。
- `--allowedTools`：**白名单**，只放行迁移必需的工具。这里连通用 `Bash` 都不给，只放行一条精确到命令的 `pnpm tsc`，把破坏面压到最小。
- `--max-turns 30`：单文件迁移不该无限循环，设硬上限防"原地打转烧 token"。

## 第 3 步：清单驱动的批处理循环——逐文件原子提交

现在把单文件能力组装成批处理。核心思路：**先扫出清单，再逐项消费，每个文件独立成一个 git 提交**。这样任何一个文件改坏，都能 `git revert <commit>` 精准回滚，不会污染其它文件。

```bash
#!/usr/bin/env bash
# migrate.sh — 清单驱动的批量迁移机
set -euo pipefail

MANIFEST="manifest.txt"          # 目标文件清单，一行一个路径
LOG="migrate.log"                # 运行日志
RULES="migration-rules.md"

# 1) 生成清单：用 grep 初筛候选，Agent 只看"可能要改"的文件
git grep -l "db\.query(" -- 'src/**/*.ts' > "$MANIFEST" || true
echo "[plan] 共 $(wc -l < "$MANIFEST") 个候选文件"

# 2) 逐文件消费
while IFS= read -r file; do
  [ -z "$file" ] && continue
  echo "==== $(date +%T) $file ====" | tee -a "$LOG"

  # 跑迁移：输出最后一行 JSON 摘要
  summary=$(claude -p "迁移 $file" \
    --append-system-prompt-file "$RULES" \
    --allowedTools "Read,Edit,Bash(pnpm tsc --noEmit)" \
    --max-turns 30 2>>"$LOG" | tail -n 1)

  status=$(echo "$summary" | jq -r .status 2>/dev/null || echo "parse-error")

  if [ "$status" = "done" ]; then
    # 原子提交：一个文件一个 commit，message 自带文件名便于回滚
    git add "$file"
    git commit -m "refactor(sql): 参数化 $file" -q
    echo "[ok]   $file → committed" | tee -a "$LOG"
  else
    # 失败/跳过：丢弃该文件的全部改动，保持工作区干净
    git checkout -- "$file" 2>/dev/null || true
    echo "[skip] $file ($status)" | tee -a "$LOG"
  fi
done < "$MANIFEST"

echo "[done] 完成。日志见 $LOG"
```

设计要点拆解：

- **清单先于迁移**。`git grep` 做粗筛，把 Agent 的注意力收窄到候选集。120 个文件可能只有 80 个真要改，省下的 40 个就是纯利润。
- **一文件一提交**。这是整条流水线最重要的护栏——把"批量大爆炸"降级成"可逐个撤销的小步"。
- **失败即回滚**。`git checkout --` 保证工作区永远是干净的已知态，下一轮不背历史包袱。
- **结构化摘要**。强制最后一行 JSON，便于 `jq` 解析、便于日志统计、便于断点续跑时知道停在哪。

## 第 4 步：护栏——权限模式与 dry-run

`--allowedTools` 是软白名单。还有更强的手段：**权限模式**。迁移机推荐用 `acceptEdits`（自动接受文件编辑，但 Bash 仍需授权）配合精确的 `--allowedTools`，而不是图省事上 `--dangerously-skip-permissions`（关掉一切确认）。后者在自动化里一旦 Agent 走偏，破坏不可控。

正式跑前的 **dry-run**（干跑）：把规约里的"改写"换成"只报告不改"，先让 Agent 全仓库走一遍，产出一份"将要改什么"的清单给你审。审完再开真正写入。

```bash
# dry-run：allowedTools 里不给 Edit，Agent 只能读和报告
claude -p "评估 src/users/repo.ts 需要哪些参数化改动，只读不改，输出改动清单 JSON" \
  --append-system-prompt-file ./migration-rules.md \
  --allowedTools "Read" \
  --max-turns 20 > dryrun/src-users-repo.json
```

dry-run 产物汇总后，人工抽查 5~10 个文件，确认规约被正确理解，再放开 `Edit`。这一步能挡掉 80% 的"批量翻车"。

## 第 5 步：断点续跑——session-id 持久化

100 个文件跑到第 60 个断了网/被 Ctrl-C，怎么办？两条路：

**路线 A（推荐，工程化）**：脚本自身可重入。上面的 `migrate.sh` 已经具备——因为它基于"清单 + 干净工作区 + 逐文件提交"，重跑时只要从清单里**剔除已经提交过的文件**即可：

```bash
# 续跑：去掉已迁移（已出现在 commit message 里）的文件
grep -vxFf <(git log --oneline | grep 'refactor(sql)' | sed 's/.*参数化 //') "$MANIFEST" > todo.txt
mv todo.txt "$MANIFEST"
./migrate.sh
```

**路线 B（会话级续命）**：用 `--resume <session-id>` / `--continue`。`--continue` 接最近一次会话，`--resume` 接指定 session。适合"一个 Agent 跨多次调用追同一个上下文"的场景，但批量迁移通常是**N 个独立短会话**，路线 A 更稳、更可观测。

```bash
# 记下 session-id，事后可续
SID=$(claude -p "迁移 src/x.ts" --append-system-prompt-file ./migration-rules.md \
      --output-format json | jq -r .session_id)
# 之后
claude -p "继续刚才没改完的部分" --resume "$SID"
```

判断标准：**任务是无状态的 N 个独立单元 → 路线 A；任务是单个长链条需保留中间推理 → 路线 B**。

## 第 6 步：可观测——stream-json 实时流

跑 100 个文件，光看 `tail -f migrate.log` 不够，你想要的是**每个文件里 Agent 真正在干什么**（读了哪些行、改了哪几处、是否在空转）。切到流式输出：

```bash
claude -p "迁移 $file" \
  --append-system-prompt-file "$RULES" \
  --allowedTools "Read,Edit,Bash(pnpm tsc --noEmit)" \
  --output-format stream-json --verbose --include-partial-messages \
  --max-turns 30 >> "stream/$file.jsonl"
```

`stream-json` 是逐行的 JSON 事件流：工具调用、工具结果、思考片段、文本增量都是独立一行。你可以：

```bash
# 只看某个文件迁移过程中发生了哪些工具调用
jq -r 'select(.type=="tool_use") | .name + "  " + (.input|tostring|.[0:80])' "stream/src-users-repo.jsonl"
```

这把"黑盒 Agent"变成了**可审计的事件日志**——出问题时能精确回放第几个 turn 走歪了。

> 一个常被忽略的坑：headless 模式下，若 Agent 起了**后台进程**（dev server、watch 构建），它会在 Agent 给出最终结果、stdin 关闭后约 **5 秒**被回收（v2.1.163 起）。若你让 Agent 起了**后台子代理/工作流**，`claude -p` 会等它完成——从 v2.1.182 起这个等待默认封顶 **10 分钟**，可用环境变量 `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS` 调整，设 0 表示无限等。批量迁移里别让 Agent 起常驻进程，免得一个文件卡死整条线。

## 实战案例：120 文件 SQL 参数化迁移

场景：一个 Node/TypeScript 单体仓，120 个 `.ts` 文件里散落着 `db.query(\`SELECT ... ${userInput} ...\`)` 式的字符串拼接，存在 SQL 注入风险。要求全部迁移到 `db.execute(sql, params)` 参数绑定。

**为什么必须用 Agent 而不是 sed**：每个调用点的拼接方式都不一样——有的嵌在模板字符串里、有的先 `+` 拼成变量再传入、有的在循环里动态拼 WHERE；参数顺序、类型、是否需转义都依赖上下文判断。正则会误伤注释和字符串字面量，Agent 能读懂语义。

落地步骤：

1. **建仓护体**：`git checkout -b refactor/sql-parametrize`，保证主干安全。
2. **粗筛清单**：`git grep -l "db\.query(" -- 'src/**/*.ts' > manifest.txt`，得 120 行。
3. **写规约**：即上面的 `migration-rules.md`，重点钉死"非查询必参数化、快照文件禁改、改完过 tsc"。
4. **dry-run 抽查**：随机抽 8 个文件跑只读评估，人工确认规约被正确执行。
5. **正式批跑**：`./migrate.sh`，边跑边 `tail -f migrate.log` 看 ok/skip 计数。
6. **全局验证**：跑完执行 `pnpm tsc --noEmit && pnpm test`，红的是个别漏网，逐个修。
7. **抽样复审**：`git log --oneline | grep 'refactor(sql)'` 随机挑 10 个 commit 人工 code review。
8. **合并**：开 PR，描述附迁移前后注入面 diff、tsc/test 全绿证据。

预期结果：120 个文件，约 95 个一次过，10~15 个被标 SKIPPED（多是边界情况，如动态表名无法参数化，规约要求保留并标注），总耗时约 1~2 小时无人值守，token 成本远低于让人手改一天的工时，且每个改动都可单点回滚。

## 工作流程图

```mermaid
flowchart TD
    A[主干建分支] --> B[git grep 粗筛候选]
    B --> C[生成 manifest.txt 清单]
    C --> D{清单还有文件?}
    D -- 是 --> E[取下一个文件]
    E --> F["claude -p 单文件迁移<br/>规约+白名单+max-turns"]
    F --> G[Agent: Read→TodoWrite拆解→Edit→tsc自检]
    G --> H{输出 status?}
    H -- done --> I[git add + 原子提交]
    H -- skipped/err --> J[git checkout 回滚该文件]
    I --> K[stream-json 事件落盘]
    J --> K
    K --> D
    D -- 否 --> L[全局 tsc + test 验证]
    L --> M[抽样 code review]
    M --> N[开 PR 合并]
    J -.失败可续.-> P["续跑：剔除已提交文件<br/>重跑脚本即可"]
```

## 常见问题与解决

- **Agent 改到一半超 `--max-turns` 被截断**：调高上限，或把规约里"自检+修错"拆成两段——先纯改、再单独跑一轮"修 tsc 错误"，避免单文件无限循环。
- **stdin 报 10MB 上限**：别再 pipe 大文件，改成 prompt 里写路径让 Agent 用 `Read` 工具读。
- **批量跑偏、风格不统一**：99% 是规约没钉进系统层。确认用了 `--append-system-prompt-file`，而非塞在普通 prompt 里。
- **token 失控**：清单太粗（喂了不该改的文件）。先用更严的 `git grep` 模式收窄候选集。
- **想全程无人值守又怕闯祸**：把 `--allowedTools` 收到极致（只 Read+Edit+一条固定 test 命令），绝不给通用 Bash；靠"一文件一提交"做最终兜底。

## 延伸阅读

- Claude Code 官方 headless 文档（`-p`、stdin、stream-json、后台进程行为）
- Claude Code CLI 参考（全部 flag 速查）
- Claude Code 会话管理（`--continue` / `--resume` / `--fork`）
- TodoWrite / Tasks 在自治长任务里的演进用法

## 训练营适用性评估

- **适合**：面向有 1~3 年经验的开发者、DevOps/SRE、平台工程的**进阶训练营**（Agent 自动化方向），2~3 天营期。
- **教学时长**：讲解 + 跟练约 **3~4 小时**（含 dry-run、批跑、回滚演练），建议留 1 小时让学员拿自己仓库的小规模迁移实操。
- **不适合**：零基础营——需要 shell、git、管道前置知识；建议先完成"环境变量三件套首跑"与"计划模式入门"两轮再上本文。
- **考核点**：能否独立产出可重入的迁移脚本、能否用 dry-run 先验证、能否用"一文件一提交"实现精准回滚。

## 参考来源

1. [Run Claude Code programmatically — Claude Code Docs](https://code.claude.com/docs/en/headless) — 官方 headless 模式权威文档，含 `-p`、stdin 10MB 上限、stream-json、后台进程 5 秒回收与 `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS` 等关键行为。
2. [CLI reference — Claude Code Docs](https://code.claude.com/docs/en/cli-reference) — 全部命令行 flag 速查，含 `--allowedTools`、`--max-turns`、`--append-system-prompt-file`、`--settings`、`--mcp-config`。
3. [Manage sessions — Claude Code Docs](https://code.claude.com/docs/en/sessions) — `--continue` / `--resume <session-id>` / `--fork` 会话续命与分叉机制。
4. [Claude Code stream-json: the output format that changes everything](https://backgroundclaude.com/blog/stream-json) — 逐行 JSON 事件流在可观测自动化里的实战解析。
5. [CCFlow: Claude Code CLI wrapper (GitHub)](https://github.com/JumpBearCode/CCFlow) — 开源的 `claude -p` + stream-json 编排封装，批量流水线设计可借鉴。

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