本教程教你用 Claude Code 的生命周期钩子(hooks)把无头 -p 批处理跑成一个"自驱动"流水线:写完即测、压缩前存档、未完自动续跑、卡住自动呼叫人类。学完你能让一个跨几十个文件的代码迁移任务在无人值守下跑到全部测试变绿。适合已经会用 -p 跑单条命令、想把它扩展成可靠批处理流水线的工程师与 DevOps。
claude -p "hello" 并拿到输出jq、git、python3(或 node)的 Linux/macOS 机器settings.json 在哪-p 批处理总是会"半路趴窝"很多人第一反应是把 -p 塞进 for 循环跑批处理:
for f in src/**/*.py; do
claude -p "给 $f 加类型注解并补测试"
done
跑起来很快就会发现三类致命问题:
Stop,剩下十几个文件没动,循环却以为它干完了。-p 静默挂在那里,既不报错也不前进。这三类问题的共同点是:它们都发生在生命周期事件上。而 Claude Code 恰好为每一个关键事件留了钩子。本教程的核心思路就是——用钩子把这三道墙全部拆掉,让批处理真正"自驱动"。
先把武器库摆清楚。Claude Code 的钩子都配在 .claude/settings.json 的 hooks 字段下,按事件名分组:
| 事件 | 触发时机 | 本教程用途 |
|---|---|---|
SessionStart |
一次会话开始 | 注入任务清单 + 规则 |
PreToolUse |
调用工具前 | 拦危险命令(护栏) |
PostToolUse |
工具执行后 | 写完即测,失败回灌 |
PreCompact |
上下文压缩前 | 存档进度,防遗忘 |
Notification |
需要人介入时 | 卡住呼叫人类 |
Stop |
模型打算停时 | 未完续跑 |
SessionEnd |
会话结束 | 收尾汇总 |
每个钩子都是一段 shell 命令,通过 stdin 收到一段 JSON,通过退出码和 stdout JSON 反控 Claude。两条关键约定记牢:
2:把 stderr 当成"工具错误"回灌给模型,触发它自我纠正。decision/reason 或 hookSpecificOutput 精细控制(放行、阻断、追加上下文)。flowchart TD
A[run-batch.sh 编排器] -->|逐文件/并行| B[claude -p 无头会话]
B --> C{SessionStart}
C -->|注入清单| D[模型干活]
D -->|Edit/Write| E[PostToolUse 写完即测]
E -->|测试失败 exit 2| D
E -->|测试绿| D
D -->|上下文满| F[PreCompact 存档]
F --> D
D -->|需要权限| G[Notification 呼叫人]
D -->|想停| H{Stop 钩子}
H -->|未完 block| D
H -->|已完成 allow| I[SessionEnd 收尾]
I --> A
闭环的精髓在右下角:Stop 不让停 → 回到干活;PostToolUse 测试不过 → 回到干活。模型被这两股力量拽着,直到任务真正完成。
mkdir -p auto-migrate/.claude/state/checkpoints auto-migrate/hooks
cd auto-migrate
git init -q && touch src/app.py src/util.py # 你的真实代码替换这里
mkdir -p tests
.claude/settings.json 是整条流水线的"控制台",一次写好所有钩子:
{
"hooks": {
"SessionStart": [{
"hooks": [{ "type": "command", "command": "bash hooks/on-session-start.sh" }]
}],
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "bash hooks/guard.sh" }]
}],
"PostToolUse": [{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{ "type": "command", "command": "bash hooks/post-tool-use.sh" }]
}],
"PreCompact": [{
"hooks": [{ "type": "command", "command": "bash hooks/pre-compact.sh" }]
}],
"Notification": [{
"hooks": [{ "type": "command", "command": "bash hooks/on-notify.sh" }]
}],
"Stop": [{
"hooks": [{ "type": "command", "command": "bash hooks/on-stop.sh" }]
}]
}
}
matcher是正则,Edit|Write|MultiEdit表示这三种工具执行后才触发。PreToolUse的Bashmatcher 让我们能审查每条命令。
无头模式下没有人在对话里唠叨,所以必须把"要干啥、干到什么程度"在会话一开就钉死。hooks/on-session-start.sh:
#!/usr/bin/env bash
cat <<'EOF'
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "项目目标:为 src/ 下所有 .py 补全类型注解并写对应测试。验收标准:(1) 每个函数参数和返回值都有注解;(2) pytest 全绿;(3) 完成后在 .claude/state/ 写 done.marker。每改完一个文件就把它从 .claude/state/tasklist.txt 划掉。不要提前停止。"
}
}
EOF
这段 additionalContext 会被注入到模型上下文最前面。预期结果:模型第一轮就知道边界条件,不会瞎改也不会提前收工。
这是整条流水线最值钱的一环。每次模型 Edit/Write 完一个文件,钩子立刻格式化 + 跑该文件相关测试;失败就用退出码 2 把错误回灌,模型把它当成工具报错,于是自动接着改。hooks/post-tool-use.sh:
#!/usr/bin/env bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // empty')
case "$file" in
*.py)
ruff format "$file" >/dev/null 2>&1 || true
ruff check --fix "$file" >/dev/null 2>&1 || true
# 推断对应测试文件
base=$(basename "$file" .py)
if [ -f "tests/test_${base}.py" ]; then
log=$(python -m pytest -q "tests/test_${base}.py" 2>&1 | tail -8)
if echo "$log" | grep -q "passed"; then
echo "[auto-check] $file OK" >&2 # 普通提示
else
echo "[auto-check] $file 测试失败:$log" >&2
exit 2 # 关键:回灌为工具错误,逼模型自纠
fi
fi
;;
esac
exit 0
预期结果:模型改完 util.py,钩子跑 test_util.py,红了就吐出失败堆栈,模型下一轮立刻去修,形成"改→测→改"的闭环,完全不需要人盯着。常见坑:exit 2 的内容走 stderr,务必把可执行的错误信息(断言、行号)塞进 stderr 而不是 stdout。
长任务必然触发上下文压缩,压缩会把早期细节扔掉。对策:压缩前一秒把进度落盘,并叮嘱模型压缩后先读盘。hooks/pre-compact.sh:
#!/usr/bin/env bash
ckpt=".claude/state/checkpoints/last.md"
{
echo "# 压缩前快照($(date -Iseconds))"
echo "## 已改动文件"
git diff --stat 2>/dev/null | tail -15
echo "## 未完成项"
grep -v '^x ' .claude/state/tasklist.txt 2>/dev/null || echo "(无)"
} > "$ckpt"
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreCompact",
"additionalContext": "即将压缩上下文。压缩后请立即读取 .claude/state/checkpoints/last.md 和 .claude/state/tasklist.txt 恢复进度,从下一个未完成项继续,不要重做已完成的。"
}
}
EOF
预期结果:哪怕任务跑到第 40 轮触发压缩,模型也能在压缩后精确接续,而不是从零开始。这一步是把"健忘"变成"自带存档"。
这是让批处理"自驱动"的发动机。模型每次想停,on-stop.sh 检查 done.marker 是否存在:不存在就返回 decision: "block",模型被迫继续。hooks/on-stop.sh:
#!/usr/bin/env bash
input=$(cat)
stop_active=$(echo "$input" | jq -r '.stop_hook_active // false')
# 防死循环:Claude Code 续跑后再次 Stop 时 stop_hook_active=true,放行
if [ "$stop_active" = "true" ] || [ -f .claude/state/done.marker ]; then
exit 0
fi
cat <<'EOF'
{
"decision": "block",
"reason": "任务尚未完成:done.marker 不存在。请读取 .claude/state/tasklist.txt,处理下一个未勾选项,运行 pytest 直到全绿,全部完成后写 .claude/state/done.marker 再停。"
}
EOF
关键机制:Claude Code 在因 Stop 钩子续跑后,下一次触发 Stop 时会在 payload 里带 stop_hook_active: true,这就是天然的熔断器,避免无限续跑。所以脚本第一行判断就是"已经在续跑了,这次就让它停"。预期结果:单次 -p 调用能自动跨越多次"想停",一直跑到 done.marker 落地。
有些情况必须人来定夺(要花钱的命令、需要确认的外部调用)。Notification 事件在模型等待时触发,钩子负责"摇人"。hooks/on-notify.sh:
#!/usr/bin/env bash
input=$(cat)
msg=$(echo "$input" | jq -r '.message // "Claude Code 需要介入"')
printf '\a' # 终端响铃
# 可选:推送到 Slack / 企业微信 / Bark
if [ -n "${SLACK_HOOK:-}" ]; then
curl -s -X POST "$SLACK_HOOK" -H 'content-type: application/json' \
--data "{\"text\":\":rotating_light: $msg\"}" >/dev/null 2>&1 || true
fi
预期结果:模型不会静默挂死,而是主动把"我卡在 X 上"推给你。配合下一步的 --max-turns,挂死也只会挂到上限就退出。
批处理无人值守,最怕 rm -rf 之类误伤。一条最简护栏 hooks/guard.sh:
#!/usr/bin/env bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')
if echo "$cmd" | grep -qE 'rm -rf|sudo |>|>>( *)/|curl.*\| *sh'; then
cat <<'EOF'
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"护栏拦截:批处理模式禁止破坏性命令。"}}
EOF
exit 0
fi
exit 0
最后用 xargs -P 做有界并行,每个文件起一个独立 -p 会话,互不污染。run-batch.sh:
#!/usr/bin/env bash
set -euo pipefail
JOBS="${JOBS:-2}"
mkdir -p .claude/state
find src -name '*.py' ! -path '*/__pycache__/*' | \
xargs -P "$JOBS" -I{} bash -c '
f="{}"; base=$(basename "$f" .py)
echo "$f" >> .claude/state/tasklist.txt
claude -p \
--permission-mode acceptEdits \
--allowedTools "Edit Read Write Bash(pytest:*) Bash(ruff:*)" \
--max-turns 25 \
--output-format json \
--session-id "migrate-$base" \
--add-dir "$PWD" \
"为 $f 补全类型注解,并保证对应测试通过。完成所有工作后创建 .claude/state/done.marker。" \
> ".claude/state/run-$base.json" 2> ".claude/state/run-$base.err" \
|| echo "$base FAILED(exit $?)" >> .claude/state/failures.log
'
echo "批处理结束。失败项见 .claude/state/failures.log"
预期结果:JOBS=2 时两个文件并行处理;每个会话有独立 --session-id,可事后用 claude --resume migrate-util 接着跑。--max-turns 25 是硬熔断,配合 Stop 钩子的软续跑,双保险。
把一次完整循环拆成时序看更清楚:
sequenceDiagram
participant O as 编排器
participant C as claude -p
participant H as Hooks
O->>C: claude -p "迁移 util.py"
C->>H: SessionStart
H-->>C: 注入任务清单
C->>C: Edit util.py
C->>H: PostToolUse(Edit)
H->>H: ruff + pytest
alt 测试失败
H-->>C: exit 2 + 失败堆栈
C->>C: 自纠继续改
else 测试绿
H-->>C: OK
end
C->>H: Stop(想停)
alt done.marker 不存在
H-->>C: block + 续跑理由
else 已完成
H-->>C: allow,会话结束
end
输入仓库:src/ 下 12 个无注解的 Python 模块,tests/ 下只有 3 个测试文件。
一条命令启动:
chmod +x hooks/*.sh run-batch.sh
JOBS=3 ./run-batch.sh
观察现象:
- .claude/state/run-*.json 里能看到每个会话的完整轨迹(--output-format json)
- 任一会话触发压缩时,checkpoints/last.md 被刷新
- 真卡住时终端响铃 / Slack 收到推送
- 全部完成后 done.marker 落地,pytest 全绿
结果核验:
python -m pytest -q # 应全绿
git diff --stat # 看 12 个文件都被注解覆盖
cat .claude/state/failures.log 2>/dev/null || echo "无失败"
无人值守 = 失控风险放大,务必上四道闸:
--max-turns 硬上限:单会话最多 N 轮,到点必停,防止 Stop 续跑把钱包烧穿。--allowedTools 白名单:只给 Edit Read Write Bash(pytest:*),堵死任意 Bash。guard.sh 再拦一道破坏性命令。stop_hook_active 熔断:Stop 钩子必须判断它,否则就是死循环。本教程的脚本已处理。建议先用 JOBS=1 --max-turns 5 在一个小文件上干跑一次,确认 run-*.json 行为符合预期,再放大并发。
Q1:Stop 钩子不触发续跑?
检查 settings.json 的 JSON 是否合法(jq . .claude/settings.json 能解析);确认脚本可执行(chmod +x);确认输出的是合法 JSON 且字段名是 decision/reason。
Q2:PostToolUse 把所有 Edit 都跑测试,太慢?
缩小 matcher 或在脚本里只对 tests/ 已存在的文件跑,新文件跳过自动测;或改成只跑 ruff,把完整 pytest 留给 Stop 前一次性跑。
Q3:并行会话之间抢同一个文件?
按文件粒度分派(本教程就是一文件一会话),避免两个会话改同一文件。--session-id 用文件名保证可追溯。
Q4:--permission-mode acceptEdits 安全吗?
配合 --allowedTools 白名单 + PreToolUse 护栏是安全的。绝对不要在无人值守时用 bypassPermissions。
-p 全部标志位run-batch.sh + 钩子套件可直接用于真实代码迁移、依赖升级、批量加测试等场景。建议课时 4 小时(1 小时讲事件模型 + 2 小时动手搭六钩子 + 1 小时调通并行与护栏)。-p 单条命令与 settings.json 基础,建议作为第 3–4 天的进阶主题。-p 无头模式、--output-format、--permission-mode、--max-turns、--session-id 等标志位[!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 等场景全覆盖。
