本教程聚焦 Claude Code 无头自动化中最被忽视的两条主线:输出端的可观测性(
-p的stream-json事件流)与执行端的自愈性(asyncRewake/Setup/StopFailure生命周期钩子)。学完你将掌握如何把一段"盲跑即停"的批量脚本,升级为"看得见每一步、失败了能自己爬起来"的 CI 流水线。
很多团队第一次把 Claude Code 接进 CI 时,写出来的脚本长这样:
claude -p "审查这次的 diff 并修复问题" --allowedTools "Bash,Read,Edit"
跑完看一眼退出码,0 就算成功,非 0 就重试。这种用法有三个致命缺陷:
~/.claude 里的钩子、项目 .mcp.json 里的服务器会被自动加载,同一份脚本在不同机器上行为不一致。本教程围绕这三个痛点,给出两条主线的工程化解法,并用一个自愈式 CI 审查器把所有原语串成一条可运行的流水线。
适用人群:已经能用 claude -p 跑通基础无头任务的工程师;正在把 Claude Code 接入 GitHub Actions / GitLab CI / Jenkins 的 DevOps 与平台团队;想给 AI 编排加上遥测与自愈能力的架构师。
asyncRewake、--bare、CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS 均需较新版本)。ANTHROPIC_API_KEY(裸模式下 OAuth/钥匙串不会被读取,必须用环境变量或 apiKeyHelper)。jq(解析事件流的核心工具)。npm test 可跑)。# 版本自检
claude --version
jq --version
-p 变成事件流--bare 锁定可复现性从 v2.1.x 起,官方为脚本与 CI 场景引入了 --bare 模式。它跳过钩子、技能、插件、MCP 服务器、自动记忆、CLAUDE.md 的自动发现,只认你显式传入的 flag。这意味着队友机器上的任何本地配置都不会污染 CI 结果。
官方明确写道:"--bare 是脚本与 SDK 调用的推荐模式,未来会成为 -p 的默认行为。"
| 想加载什么 | 用哪个 flag |
|---|---|
| 追加系统提示 | --append-system-prompt / --append-system-prompt-file |
| 设置项 | --settings <file-or-json> |
| MCP 服务器 | --mcp-config <file-or-json> |
| 自定义子代理 | --agents <json> |
| 插件 | --plugin-dir <path> / --plugin-url <url> |
裸模式下 Claude 仍拥有 Bash、文件读、文件写三类工具,所以一个最小可复现的 CI 调用是:
claude --bare -p "总结 auth.py 的职责" --allowedTools "Read"
注意:裸模式会跳过 OAuth 与钥匙串读取,鉴权必须来自
ANTHROPIC_API_KEY或通过--settings传入的apiKeyHelper。Bedrock / Vertex / Foundry 仍走各自的提供商凭证。
--output-format 决定了你如何消费 Claude 的产出:
text(默认):纯文本,适合直接给人看,不适合程序解析。json:单个 JSON 对象,含 result、session_id、total_cost_usd、按模型拆分的成本明细。适合"跑完一次性拿结论"。stream-json:换行分隔的 JSON(NDJSON),每个事件一行,实时流出。这是可观测性的核心。# 拿结构化结论与单次成本
claude --bare -p "总结本项目" --output-format json | jq '{result, cost: .total_cost_usd, session: .session_id}'
--json-schema 拿"机器能直接吃"的决策当流水线下游需要确定性结构(比如把审查结果写进 PR 评论)时,组合 --output-format json 与 --json-schema,响应会把结构化结果放进 structured_output 字段:
claude --bare -p "从 auth.py 中提取主函数名" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
| jq '.structured_output'
# {"functions": ["login", "verify_token", "refresh_session"]}
stream-json:四类关键事件加上 --verbose --include-partial-messages,stream-json 才会逐 token 流出。每一行是一个独立 JSON 事件,可以用 jq 单独过滤。一条无头调用里,你最该盯住四类事件:
① system/init——会话握手,第一件事
它报告本次会话的模型、工具、MCP 服务器、已加载插件,以及插件加载错误。这是"在 CI 里第一时间发现插件没装上"的关键:
claude --bare -p "hi" --output-format stream-json --verbose \
| jq -c 'select(.type=="system" and .subtype=="init") | {plugins: .plugins, errors: .plugin_errors}'
如果 plugin_errors 非空,说明某个插件依赖未满足或 --plugin-dir 路径无效,受影响插件会被降级。CI 里可以用它直接 fail-fast。
② system/api_retry——限流与过载的实时信号
当 API 请求遇到可重试错误时,Claude Code 会先发一条 api_retry 事件再重试。字段包括 attempt(当前第几次)、max_retries、retry_delay_ms、error_status(HTTP 码,连接错误时为 null)、error(错误类别,如 rate_limit / overloaded / billing_error)。
# 实时累计重试次数与类别,喂给监控
... stream-json ... | jq -rc 'select(.subtype=="api_retry) | "\(.attempt)/\(.max_retries) \(.error) delay=\(.retry_delay_ms)ms"'
这让"流水线变慢"不再是个黑盒——你能精确看到是限流在拖后腿,还是账单出了问题。
③ 文本增量——把输出"流"给终端
过滤出文本 delta,-rj 原样拼接,体验等同于打字机:
claude --bare -p "用三句话解释递归" --output-format stream-json --verbose --include-partial-messages \
| jq -rj 'select(.type=="stream_event" and .event.delta.type?=="text_delta") | .event.delta.text'
④ result——最终账单与会话句柄
流的最后一行是 result 事件,带 result(全文)、session_id、total_cost_usd、用量统计与 stop reason。session_id 是把多步任务串成"有状态流水线"的钥匙(见第五章)。
把上面四类事件汇成一个实时遥测器,就能在 CI 日志里一眼看清"这次运行到底发生了什么":
#!/usr/bin/env bash
# telem.sh —— 把 stream-json 压成可读的运行日志
set -euo pipefail
claude --bare -p "$1" \
--allowedTools "${2:-Read}" \
--output-format stream-json --verbose --include-partial-messages \
| jq -rc '
if .subtype=="init" then "🟢 init model=\(.model // "?") plugins=\(.plugins|length // 0) errs=\(.plugin_errors|length // 0)"
elif .subtype=="api_retry" then "🟡 retry \(.attempt)/\(.max_retries) \(.error) +\(.retry_delay_ms)ms"
elif .type=="result" then "✅ done cost=$\(.total_cost_usd // 0) session=\(.session_id)"
else empty end
'
陷阱:
-p的 stdin 有 10MB 上限(v2.1.128 起)。超大会直接非零退出。大输入请写进文件,在 prompt 里引用路径,而不是cat huge.log | claude -p ...。
钩子(hooks)不是新概念,但 2026 版的钩子系统已经从"拦个 rm -rf"进化成一张覆盖整个会话生命周期的反应网。本节只挑与自动化自愈直接相关的事件,避免重复造"提交守卫"那种拦截型用法。
先记住一条贯穿全场的规则——退出码语义:
PreToolUse 会因此拦下工具调用,UserPromptSubmit 会因此拒绝 prompt。<hook> hook error,执行继续。关键认知:钩子想"硬阻断"必须
exit 2;exit 1在大多数事件里只是非阻断告警,并不会真的拦住动作。要强制策略,别用钩子,用权限系统。
Setup——CI 的一次性环境准备Setup 事件在用 --init-only,或在 -p 模式下用 --init / --maintenance 启动时触发,专门用于 CI/脚本里"跑正事之前先把环境备好"。它不能阻断,但可以把信息通过 additionalContext 注入 Claude 上下文,还能往 CLAUDE_ENV_FILE 写变量,让后续 Bash 命令继承。
{
"hooks": {
"Setup": [
{
"matcher": "init",
"hooks": [
{ "type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/bootstrap.sh" }
]
}
]
}
}
#!/usr/bin/env bash
# .claude/hooks/bootstrap.sh —— 装依赖、探测版本,把结论喂给 Claude
set -euo pipefail
npm ci --silent
NODE_VER=$(node -v)
TS_VER=$(npx tsc --version 2>/dev/null || echo "no-ts")
# additionalContext 会在后续对话里被 Claude 看到
jq -nc --arg c "CI bootstrap done: node=$NODE_VER tsc=$TS_VER. 依赖已就绪,可用 npm test 跑测试。" \
'{hookSpecificOutput:{hookEventName:"Setup", additionalContext:$c}}'
用法:claude --bare -p --init "审查改动" ... 会在正式对话前先跑一次 bootstrap,把"环境就绪"这个事实写进上下文,避免 Claude 凭空假设版本。
asyncRewake——后台失败即"唤醒" Claude 的自愈原语这是整篇文章最值得记住的一个字段。给 type:"command" 钩子加上 "asyncRewake": true,它会:
async: true);普通 async 钩子的输出要等"下一个对话轮"才送达,会话空闲时甚至会一直等下去;而 asyncRewake 的 exit 2 是即时打断的。这正是"自愈"的机理:Claude 自以为干完了,但后台测试在烧,一失败就把错误甩到 Claude 脸上,逼它再修一轮。
{
"hooks": {
"PostToolUse": [
{ "matcher": "Write|Edit",
"hooks": [
{ "type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/watch-tests.sh",
"args": [],
"asyncRewake": true,
"timeout": 300 }
] }
]
}
}
#!/usr/bin/env bash
# watch-tests.sh —— 后台跑测试;失败用 exit 2 唤醒 Claude 去修
set -uo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# 只关心源码改动
case "$FILE_PATH" in
*.ts|*.js|*.tsx) ;;
*) exit 0 ;;
esac
OUT=$(npm test 2>&1)
STATUS=$?
if [ $STATUS -ne 0 ]; then
# 把失败输出打到 stderr,配合 exit 2 唤醒 Claude
echo "测试失败(刚改了 $FILE_PATH)。请根据下面输出修复,不要降低测试标准:" >&2
echo "$OUT" >&2
exit 2 # ← 自愈的关键:即时唤醒
fi
exit 0
进进程 vs 进程外的边界:
asyncRewake在同一次-p调用内完成"发现失败→修复"。但-p是有界的——后台子代理/钩子的等待被CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS封顶(默认 10 分钟,v2.1.182 起)。需要无限循环时设=0,但更稳妥的做法是把"无限"交给外层编排器(见第五章)。
Stop 与 StopFailure——轮次结束的两个出口Stop:Claude 正常结束一轮响应时触发,适合归档、发通知、写运行日志。StopFailure:轮次因 API 错误而中断时触发,此时 output 与 exit code 都被忽略。它是"流水线因为限流/鉴权挂了"时唯一能挂告警的地方。{
"hooks": {
"StopFailure": [
{ "hooks": [
{ "type": "command",
"command": "curl -s -X POST $SLACK_WEBHOOK -d '{\"text\":\"Claude CI 因 API 错误中断\"}' || true" }
] }
],
"Stop": [
{ "hooks": [
{ "type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/archive-run.sh" }
] }
]
}
}
注意
StopFailure的 matcher 只接受|作分隔(,会被当字面量),取值是错误类型:rate_limit/overloaded/authentication_failed/billing_error/server_error等。
PostToolBatch——批量工具结算后注入上下文当一个整批并行工具调用结算完、下一轮模型调用之前,PostToolBatch 触发(无 matcher,每次都触发)。它适合做"低成本的全局检查点"——比如统计本轮改了多少文件,把摘要塞回上下文,帮 Claude 在长任务里保持方向感。
钩子不触发是最常见的坑。排查靠这三招:
# 1. 把调试日志写到固定路径
claude --debug-file ./cc-debug.log -p "..."
# 2. 或用默认位置(~/.claude/debug/<session-id>.txt)
claude --debug -p "..."
# 3. 打开更细的匹配日志
CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose claude -p "..."
日志里能看到"哪些钩子被匹配、退出码、完整 stdout/stderr",定位"matcher 没命中"或"exit 1 被当成非阻断"非常快。
--resume 把单次调用串成流水线单次 -p 是无状态的。但很多 CI 任务是多步的:"先审查→再聚焦数据库→最后出总结"。用 --continue(续最近一次)或 --resume <session_id>(续指定会话),就能把多步串成一条有记忆的流水线:
# 第一步:开一个审查会话,记住它的 id
SID=$(claude --bare -p "审查本仓库的性能问题" \
--output-format json | jq -r '.session_id')
# 第二步:在同一会话里追问,复用上文
claude --bare -p "现在聚焦数据库查询" --resume "$SID"
claude --bare -p "把所有发现汇总成清单" --resume "$SID" --output-format json \
| jq '.structured_output'
这就是"进程外的自愈循环"的基础:你的编排脚本拿到 result 事件里的成败信号,决定是 --resume 让 Claude 继续修,还是收工。
把三条柱子合起来,造一个自愈式 CI 审查器:它审查 PR、自动跑测试,测试挂了就唤醒 Claude 修复,全部过程通过 stream-json 暴露遥测,最终用 --json-schema 产出结构化报告写进 PR 评论。
flowchart TD
A[CI 触发<br/>拉取 PR diff] --> B[bootstrap.sh<br/>Setup 钩子装依赖]
B --> C[claude --bare -p<br/>审查 + 修复建议]
C --> D{Claude 改了源码?}
D -->|是, PostToolUse| E[watch-tests.sh<br/>asyncRewake 后台跑测试]
D -->|否| F[stream-json 遥测<br/>system/init, api_retry]
E -->|exit 0 通过| F
E -->|exit 2 失败| G[即时唤醒 Claude<br/>stderr 当反馈]
G --> C
F --> H{result 事件<br/>成功?}
H -->|API 异常| I[StopFailure 钩子<br/>Slack 告警]
H -->|成功| J[--json-schema<br/>结构化报告]
J --> K[gh pr comment<br/>写入审查结论]
H -->|失败且需续修| L[--resume SID<br/>进程外自愈]
L --> C
.claude/
├── settings.json # 钩子注册
└── hooks/
├── bootstrap.sh # Setup:装依赖
├── watch-tests.sh # asyncRewake:后台测试守卫
└── archive-run.sh # Stop:归档运行
ci/
└── review.sh # 编排主脚本
.claude/settings.json(可提交进仓库,全队共享):
{
"hooks": {
"Setup": [
{ "matcher": "init",
"hooks": [
{ "type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/bootstrap.sh" }
] }
],
"PostToolUse": [
{ "matcher": "Write|Edit",
"hooks": [
{ "type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/watch-tests.sh",
"asyncRewake": true,
"timeout": 300 }
] }
],
"StopFailure": [
{ "hooks": [
{ "type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/alert-api-failure.sh" }
] }
]
}
}
#!/usr/bin/env bash
# ci/review.sh —— 自愈式 CI 审查器
set -euo pipefail
PR_ID="${1:?usage: review.sh <pr-number>}"
MAX_HEAL_ROUNDS="${MAX_HEAL_ROUNDS:-3}"
gh pr diff "$PR_ID" > /tmp/pr.diff
SCHEMA='{"type":"object","properties":{
"summary":{"type":"string"},
"issues":{"type":"array","items":{"type":"object",
"properties":{"file":{"type":"string"},"severity":{"type":"string"},
"fix":{"type":"string"}},
"required":["file","severity","fix"]}},
"tests_pass":{"type":"boolean"}},
"required":["summary","issues","tests_pass"]}'
# 用 Setup 钩子备环境,开审查会话
SID=$(claude --bare -p --init \
--allowedTools "Bash(npm test),Bash(npm run *),Read,Edit,Write" \
--append-system-prompt "你是资深审查者。修复必须让 npm test 通过,禁止跳过或降级测试。" \
--output-format json \
"审查 /tmp/pr.diff 代表的改动,修复发现的问题,确认 npm test 通过。" \
| jq -r '.session_id')
# 进程外自愈循环:拿不到 tests_pass=true 就 --resume 续修,最多 N 轮
for i in $(seq 1 "$MAX_HEAL_ROUNDS"); do
REPORT=$(claude --bare -p --resume "$SID" \
--output-format json --json-schema "$SCHEMA" \
"确认当前状态:测试是否通过?给出最终审查与修复总结。" \
| jq '.structured_output')
if [ "$(echo "$REPORT" | jq -r '.tests_pass')" = "true" ]; then
echo "✅ 第 $i 轮自愈成功"
echo "$REPORT" | jq '.summary' > /tmp/review-summary.json
gh pr comment "$PR_ID" --body-file <(echo "$REPORT" | jq -r '.summary')
exit 0
fi
echo "🔁 第 $i 轮仍未通过,续修..."
done
echo "❌ 达到自愈上限 $MAX_HEAL_ROUNDS,转人工"
exit 1
asyncRewake 负责进程内的即时修复(改完文件→后台测试→失败唤醒),--resume 循环负责进程外的跨调用兜底。前者快但有 10 分钟天花板,后者慢但可控、可计费、可观测。--bare + 显式 --allowedTools:CI 在任何队友机器上都行为一致,且 Bash(npm test) 这种带空格的权限规则精确限定只能跑测试相关命令——注意 Bash(npm test*) 没空格会误匹配 npm test-foo,空格不可省。stream-json 给人/监控看(重试、成本、插件错误),--json-schema 给机器吃(结构化报告),各司其职。Q:钩子写好了但不触发?
九成是 matcher 没命中或被当成非阻断。StopFailure / FileChanged 只认 | 不认 ,;纯字母下划线的 matcher(如 mcp__memory)是精确匹配,要匹配整台服务器的工具必须写成 mcp__memory__.*。用 --debug-file 看匹配日志。
Q:asyncRewake 钩子在 -p 里没等到修复就退出了?
检查 CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS。默认 10 分钟封顶,超时后台进程被终止。要更长设大些,要无限制设 0,但务必配外层超时防失控。
Q:stdin 报错 "exceeds 10MB"?
v2.1.128 起 -p 的 stdin 上限 10MB。把大文件落盘,prompt 里引用路径即可。
Q:exit 1 明明失败了却没拦住动作?
设计如此。除 WorktreeCreate 外,只有 exit 2 是阻断性的。要硬策略,别赌钩子,用 --permission-mode dontAsk 配 permissions.allow 白名单。
Q:裸模式下 skill / /config 还能用吗?
能用。-p 模式下在 prompt 里写 /skill-name 会被展开;/config thinking=false 这类设置也能以 key=value 形式传入(v2.1.181 起)。但 --bare 默认不加载技能,需要时通过 --plugin-dir 等显式带入。
Q:怎么精确控制单次成本?
--output-format json 的 total_cost_usd 与按模型拆分明细是官方给的钱包接口。CI 里加一句 | jq '.total_cost_usd' 进监控即可。
https://code.claude.com/docs/en/headless —— -p / --bare / stream-json / --resume 的权威说明。https://code.claude.com/docs/en/hooks —— 30+ 事件、5 种钩子类型、退出码与 JSON 输出的完整 schema。https://code.claude.com/docs/en/hooks-guide —— 配 20+ 可复制示例,适合先读这篇再读 reference。https://code.claude.com/docs/en/cli-reference —— 所有 flag 的精确语义,含 --system-prompt 等系统提示旗标。https://skills.rest/skill/claude-code-automation —— 把 headless + streaming 封装成可复用编排的参考实现。| 维度 | 评估 |
|---|---|
| 适合的训练营类型 | 进阶 DevOps / AI 工程化训练营;已掌握 claude -p 基础与简单钩子的学员。不适合零基础(需先过入门篇与钩子入门篇)。 |
| 建议教学时长 | 2 课时(90 分钟讲解 + 60 分钟动手造自愈审查器)。 |
| 核心练习 | ① 给现有 CI 脚本加 stream-json 遥测;② 实现 asyncRewake 测试守卫;③ 用 --resume 写跨调用自愈循环。 |
| 验收标准 | 学员能独立产出:一个 --bare 可复现、stream-json 可观测、asyncRewake 能自愈、--resume 能续修的四合一流水线,并解释双层自愈的边界。 |
| 风险点 | asyncRewake 与后台等待天花板依赖较新版本(v2.1.182+),老环境需先升级;自愈循环务必配外层超时与成本上限,防止"修到天荒地老"。 |
本文是「Claude Code 深度使用」系列的一篇,由 51学通信 出品,站长 爱卫生(微信:gprshome201101)主理。系列从入门安装到高级编排逐层覆盖,关注获取更多 Claude Code 工程化实战。
-p/--bare/stream-json/--resume/--json-schema 的权威来源。asyncRewake、Setup、StopFailure 与退出码语义。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 等场景全覆盖。
