-p 批量扇出本教程聚焦 Claude Code 的自动化三件套——无头 -p 模式、Hooks 事件护栏、批量并发执行——并将它们组装成一个可投产的 Worker 池:一条任务队列被分发到 N 个并发 claude -p 进程,每个进程由同一套 Hooks 策略约束(禁止越权写、自动格式化、回合预算熔断),最后由 stream-json 输出汇聚成一份汇总报告。适合需要在 CI、夜间批跑、多仓库批量改造等场景下"一次跑几十个独立任务"的工程师。
claude --version 可输出 ≥ 1.x)xargs -P 或 GNU parallelclaude logingit worktree 做并发隔离)很多团队第一次用 claude -p 跑批量任务时,会踩两类坑:
claude -p 进程改同一个工作区,互相覆盖。rm -rf、git push --force、写密钥。解法是把职责拆成两层:
| 层 | 载体 | 职责 |
|---|---|---|
| 执行层 | claude -p 进程池 |
承载一个独立任务,产生 stream-json 结果 |
| 策略层 | .claude/settings.json 的 Hooks |
在每个 Worker 内部拦截/放行/熔断 |
| 调度层 | Shell 脚本(xargs -P / parallel) |
拆队列、控并发、隔离工作区、汇聚结果 |
关键洞察:Hooks 是"声明一次、对每个 Worker 进程都生效"的策略。你不需要在每个 prompt 里反复叮嘱"别 force push",而是把它写进 PreToolUse 护栏,由 harness 在每次工具调用前强制执行。这就是"确定性"——它不依赖模型的"自觉"。
flowchart LR
Q[任务队列 jobs.jsonl] --> SCHED[调度器 xargs -P N]
SCHED --> W1[Worker 1 claude -p]
SCHED --> W2[Worker 2 claude -p]
SCHED --> WN[Worker N claude -p]
W1 -.策略.-> H[(Hooks settings.json)]
W2 -.策略.-> H
WN -.策略.-> H
H -->|PreToolUse 拦截| GUARD{放行/阻断/提醒}
W1 --> R1[stream-json 结果]
W2 --> R2[stream-json 结果]
WN --> RN[stream-json 结果]
R1 --> AGG[结果汇聚 report.md]
R2 --> AGG
RN --> AGG
-p 无头模式:单 Worker 的解剖# text:纯文本,最简单
claude -p "把 README 里所有的 TODO 收集成一个列表"
# json:单条 JSON 对象,便于 jq 解析
claude -p "..." --output-format json | jq -r '.result'
# stream-json:换行分隔的事件流,批处理的黄金格式
claude -p "..." --output-format stream-json
stream-json 会按时间顺序吐出几类事件,批处理最关心的是首尾两个:
system 事件(subtype init):包含 session_id,这是断点续跑的钥匙。result 事件(subtype success/error):包含 result(最终文本)、cost_usd、duration_ms、is_error、num_turns。一个最小的事件抽取器:
# 从 stream-json 抓出 session_id 和最终结果
claude -p "修复 utils.py 里的类型错误" \
--output-format stream-json \
--allowedTools "Edit,Read,Bash(pytest *)" \
> /tmp/w1.jsonl
# 取 session_id(用于 --resume)
grep '"type":"system"' /tmp/w1.jsonl | head -1 | jq -r '.session_id'
# 取最终结果与成本
grep '"type":"result"' /tmp/w1.jsonl | jq -r '"cost=\(.cost_usd) turns=\(.num_turns)\n\(.result)"'
--allowedTools 限定 Worker 只能用列出的工具,逗号分隔,支持通配:
claude -p "..." \
--allowedTools "Read,Edit,Write,Bash(git *),Bash(npm test)"
常见问题:Worker 报"Tool not allowed"。原因:prompt 触发了一个未在白名单里的工具。解决:要么放宽白名单,要么改 prompt 让模型不依赖该工具。生产中建议先收紧、再按需放开。
--max-turns 给单 Worker 设硬上限无人值守必须设回合预算,否则一个 Worker 卡在死循环能烧掉一整夜的额度:
claude -p "重构这个模块" --max-turns 25 --output-format stream-json
--max-turns 25 会在第 25 个回合强制终止,result 事件 subtype 会标记非成功。
--resume 与 --continue# 第一次跑,记住 session_id
SID=$(claude -p "..." --output-format stream-json | \
grep '"type":"system"' | jq -r '.session_id' | head -1)
# 之后基于这个会话继续(保留全部上下文)
claude -p "继续刚才的重构,跑一下测试" --resume "$SID" --output-format stream-json
--continue(-c)则直接续最近一次会话,适合交互式补刀;批处理里更推荐显式 --resume <id>,避免多 Worker 抢同一个"最近会话"。
Hooks 定义在 .claude/settings.json(项目级)或 ~/.claude/settings.json(用户级)。批处理时,我们通常把策略放在队列共享的项目根 .claude/settings.json,让每个 claude -p 进程自动继承。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": ".claude/hooks/guard-bash.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": ".claude/hooks/auto-fmt.sh" }]
}
],
"Stop": [
{
"hooks": [{ "type": "command", "command": ".claude/hooks/budget-guard.sh" }]
}
]
}
}
matcher 是正则,匹配工具名(Bash、Edit、Write、Read 等),留空表示匹配所有工具。
每个 hook 进程通过 stdin 收到一段 JSON,关键字段:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/repo/worktree-3",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "git push --force origin main", "description": "..." }
}
退出码决定行为:
| 退出码 | 含义(PreToolUse) |
|---|---|
0 |
放行;stdout 可作为额外上下文喂给模型 |
2 |
阻断该工具调用,stderr 内容会回显给模型(让它知道为什么被拦) |
| 其他非零 | 非阻断的错误,harness 会告警但继续 |
PreToolUse).claude/hooks/guard-bash.sh:
#!/usr/bin/env bash
# 读取 hook 输入
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')
# 危险模式黑名单(确定性拦截,不依赖模型判断)
deny_patterns='rm[[:space:]]+-rf|git[[:space:]]+push.*--force|git[[:space:]]+push.*-f|>[[:space:]]*/dev/sd|chmod[[:space:]]+-R[[:space:]]+777'
if echo "$cmd" | grep -Eq "$deny_patterns"; then
echo "⛔ 被护栏拦截:检测到危险命令模式 → $cmd" >&2
exit 2 # 阻断,stderr 回显给模型
fi
# 放行
exit 0
chmod +x 之后,任何 Worker 试图跑 git push --force 都会被挡下,模型会收到"被护栏拦截"的反馈并自行改路。
常见坑:很多人在
Edit/Write上也用exit 2想阻止写入,但忘了Edit的tool_input结构不同(有file_path/old_string/new_string)。匹配器要用对应的字段,否则永远拦不住。
PostToolUse).claude/hooks/auto-fmt.sh:
#!/usr/bin/env bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // ""')
[ -z "$file" ] || [ ! -f "$file" ] && exit 0
case "$file" in
*.py) ruff format "$file" 2>/dev/null; ruff check --fix "$file" 2>/dev/null ;;
*.ts|*.tsx|*.js) npx --no-install prettier --write "$file" 2>/dev/null ;;
*.go) gofmt -w "$file" 2>/dev/null ;;
esac
exit 0 # PostToolUse 用 0,格式化失败也不阻断
这样每个 Worker 改完文件立即格式化,批跑结束后仓库就是干净的,无需单独跑 lint。
Stop)Stop 事件在模型准备结束回合时触发。我们可以在这里读取累计成本,超阈值就强制继续(continue: false 配合 reason),或者干脆记录告警。
.claude/hooks/budget-guard.sh(轻量版:只告警不强行续):
#!/usr/bin/env bash
input=$(cat)
transcript=$(echo "$input" | jq -r '.transcript_path')
# 从 transcript 估算 token 用量,超阈值就输出一个醒目提醒
# stdout 会被注入为额外上下文(exit 0 时)
echo "⏱️ Worker 即将停止,请确认任务已完成且测试通过。"
exit 0
若要硬性强制模型继续工作直到测试通过,可以让脚本输出 JSON:
# 强制继续(让模型别急着停)
printf '{"continue": true, "stopReason": "测试尚未全部通过,继续工作"}'
exit 0
注意:
continue: true配合"必须满足某条件才停"的 prompt 容易造成无限循环。务必同时用--max-turns做硬上限兜底。
除了退出码,hook 还能向 stdout 输出结构化 JSON 做更精细控制,关键字段:
{
"continue": false,
"stopReason": "预算耗尽",
"suppressOutput": false,
"additionalContext": "(注入到对话的额外上下文)"
}
例如 UserPromptSubmit hook 可以给每个 Worker 的输入注入共享上下文(编码规范、文件清单),避免在每个 prompt 里重复粘贴。
jobs.jsonl,每行一个独立任务:
{"id":"auth","repo":"https://github.com/acme/svc-auth.git","task":"把所有 print 换成 logging,并补类型注解"}
{"id":"billing","repo":"https://github.com/acme/svc-billing.git","task":"为 /invoice 接口补 pytest 覆盖到 80%"}
{"id":"gateway","repo":"https://github.com/acme/svc-gateway.git","task":"把 requests 替换为 httpx,保持行为不变"}
每行一个任务的好处:jq/xargs 处理天然友好,断点续跑只需删掉已完成的行。
worker.sh 接收一行 job,做四件事:克隆/切 worktree → 跑 claude -p(带护栏与预算)→ 抽取结果 → 写报告。
#!/usr/bin/env bash
# worker.sh —— 单个 Worker 的生命周期
set -u
LINE="$1"
OUT_DIR="${2:-./out}"
id=$(echo "$LINE" | jq -r '.id')
repo=$(echo "$LINE" | jq -r '.repo')
task=$(echo "$LINE" | jq -r '.task')
WORK=".work/$id"
LOG="$OUT_DIR/$id.jsonl"
mkdir -p "$WORK" "$OUT_DIR"
# 1) 获取代码(已存在则 fetch)
if [ -d "$WORK/.git" ]; then
git -C "$WORK" fetch --quiet && git -C "$WORK" reset --hard origin/HEAD -q
else
git clone --quiet --depth 1 "$repo" "$WORK"
fi
# 把共享 hooks 策略软链进去(策略唯一来源)
mkdir -p "$WORK/.claude/hooks"
ln -sf "$(pwd)/.claude/settings.json" "$WORK/.claude/settings.json"
ln -sf "$(pwd)/.claude/hooks/"* "$WORK/.claude/hooks/" 2>/dev/null || true
# 2) 跑 Worker(带工具白名单 + 回合预算 + stream-json)
cd "$WORK"
claude -p "$task" \
--output-format stream-json \
--allowedTools "Read,Edit,Write,Bash(git *),Bash(pytest*),Bash(npm test)" \
--max-turns 30 \
> "$OUT_DIR/$id.jsonl" 2> "$OUT_DIR/$id.err"
cd - >/dev/null
# 3) 抽取结果摘要
sid=$(grep '"type":"system"' "$LOG" | head -1 | jq -r '.session_id')
result=$(grep '"type":"result"' "$LOG" | head -1)
cost=$(echo "$result" | jq -r '.cost_usd // "?"')
turns=$(echo "$result" | jq -r '.num_turns // "?"')
ok=$(echo "$result" | jq -r '.is_error // "true"')
# 4) 汇报行(供后续聚合)
printf '%s\t%s\t%s\t%s\t%s\n' "$id" "$ok" "$cost" "$turns" "$sid" >> "$OUT_DIR/_summary.tsv"
echo "[$id] done ok=$ok cost=\$$cost turns=$turns session=$sid"
隔离要点:每个 Worker 用独立的
.work/<id>目录克隆(或git worktree add),从根上避免多进程写同一棵树。git clone --depth 1让大批仓库快速就绪。
xargs -P 与 parallel 两种姿势# 姿势 A:xargs -P(POSIX 通用)
mkdir -p out
cat jobs.jsonl | xargs -P 4 -I{} ./worker.sh {} ./out
# 姿势 B:GNU parallel(输出更整齐,自带进度)
mkdir -p out
parallel --bar -j 4 ./worker.sh {} ./out :::: jobs.jsonl
-P 4 / -j 4 = 最多 4 个 Worker 同时跑。并发度选择经验:
claude 进程,主要等 API;CPU 占用低,并发瓶颈在网络与速率限制。429 后回调。_summary.tsv 看单任务成本再放量。# 把 _summary.tsv 渲染成 Markdown 总表
{
echo "# 批处理报告"
echo
echo "| 任务 | 成功 | 成本(\$) | 回合数 | session_id |"
echo "|---|---|---|---|---|"
sort out/_summary.tsv | awk -F'\t' '{printf "| %s | %s | %s | %s | `%s` |\n",$1,$2,$3,$4,$5}'
echo
total=$(awk -F'\t' '{s+=$3} END{print s}' out/_summary.tsv)
echo "**总成本:\$${total}**"
} > report.md
把上述拼成一个一键脚本 batch.sh:
#!/usr/bin/env bash
set -euo pipefail
JOBS="${1:-jobs.jsonl}"
PARALLEL="${2:-4}"
rm -rf out .work && mkdir -p out .work
echo "→ 扇出 $PARALLEL 个 Worker 处理 $(wc -l < "$JOBS") 个任务"
parallel -j "$PARALLEL" ./worker.sh {} ./out :::: "$JOBS"
echo "→ 汇聚报告"
./report.sh # 即上一节的 awk 片段
echo "完成,见 report.md"
预期结果:out/<id>.jsonl 各自记录完整事件流;out/_summary.tsv 汇总每任务成败/成本/会话;report.md 是给人看的总表。任一任务失败(is_error: true),可直接用其 session_id 续跑:
claude -p "上一次中断了,继续完成并把测试跑绿" --resume <session_id>
常见问题:部分 Worker 报
is_error。先看out/<id>.err与 transcript 末尾。九成是护栏阻断(说明策略生效了)或--allowedTools太紧(放宽)、或单任务超--max-turns(拆任务或上调预算)。
| 维度 | 手段 |
|---|---|
| 越权写 | PreToolUse 黑名单 rm -rf / git push -f / 写 /etc |
| 密钥泄露 | PreToolUse 对 Write/Bash 扫描 AKIA*、sk-、-----BEGIN 等模式,命中即 exit 2 |
| 预算失控 | 每个 Worker --max-turns + Stop hook 双保险 |
| 并发写冲突 | 每 Worker 独立 worktree/clone,绝不共享工作区 |
| 速率限制 | 并发从 4 起步,监控 429,xargs/parallel 失败自动重排 |
| 可审计 | 全量保留 *.jsonl 事件流,含每步工具调用与成本 |
当单个任务内部有可并行的子项(如一个大仓里改 10 个互不相关的模块),不必起 10 个 claude -p,而是在一个 Worker 内用 Subagent 扇出。在仓库的 .claude/agents/ 下定义:
# .claude/agents/refactor-worker.yaml
---
name: refactor-worker
description: 独立重构一个模块并跑测试,用于批量子任务并行
tools: Read, Edit, Write, Bash(git *), Bash(pytest*)
model: sonnet
---
你是模块重构专员。只改分配给你的模块目录,完成后必须运行测试并报告结果。
不要触碰其他模块。
主 Worker(claude -p)在 prompt 里被指示"对每个模块派发一个 refactor-worker 子代理并行处理",harness 会并发调度它们。子代理与外层 Worker 池的区别:子代理共享同一仓库上下文但有独立 token 窗口,适合"同仓多模块";外层 Worker 池适合"多仓多任务"。两者可叠加:外层多仓 × 内层多模块。
flowchart TD
A[批量任务需求] --> B{任务间共享代码库?}
B -- 多个独立仓库 --> C[外层 Worker 池<br/>xargs -P + claude -p]
B -- 同一仓库多模块 --> D[Subagent 内层扇出]
A --> E{需要确定性安全约束?}
E -- 是 --> F[PreToolUse/PostToolUse Hooks]
E -- 仅需预算控制 --> G[Stop hook + --max-turns]
C --> F
D --> F
F --> Z[汇聚 stream-json → report]
G --> Z
-p 全部 flag 与三种 --output-format 的权威说明,含 --json-schema 结构化输出.claude/agents/*.yaml frontmatter 字段与并行调度语义git worktree add 在同一仓库创建多个独立工作目录,是并发隔离的轻量替代-j、--bar、--resume --joblog 实现"失败重跑"的批处理调度claude -p 单进程用法与 settings.json 基本配置(建议先修"无头模式结构化输出"与"钩子事件总线"两节)。-p 解剖;1.5 学时动手写三个 Hook 护栏;1.5 学时搭建 Worker 池并跑通 3 仓库小队列;0.5–1 学时讨论成本控制与故障续跑。batch.sh + 护栏集合,能直接接入自己团队的批量改造任务。-p、--output-format stream-json、--resume、--allowedTools 的权威定义.claude/agents/*.yaml frontmatter 与并行子代理调度xargs -P / parallel 配合 --allowedTools 的并发实战与速率控制经验[!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 等场景全覆盖。
