首页 › tutorials › asyncRewake+stream-json 造可观测自愈流水线

本教程聚焦 Claude Code 无头自动化中最被忽视的两条主线:输出端的可观测性(-p 的 stream-json 事件流)与执行端的自愈性(asyncRewake / Setup / StopFailure 生命周期钩子)。学完你将掌握如何把一段"盲跑即停"的批量脚本,升级为"看得见每一步、失败了能自己爬起来"的 CI 流水线。

一、概述与适用人群

很多团队第一次把 Claude Code 接进 CI 时,写出来的脚本长这样:

claude -p "审查这次的 diff 并修复问题" --allowedTools "Bash,Read,Edit"

跑完看一眼退出码,0 就算成功,非 0 就重试。这种用法有三个致命缺陷:

  1. 盲跑——你不知道 Claude 中途调了哪些工具、有没有触发限流、有没有插件加载失败,出了问题只能猜。
  2. 不可复现——队友 ~/.claude 里的钩子、项目 .mcp.json 里的服务器会被自动加载,同一份脚本在不同机器上行为不一致。
  3. 失败即停——构建或测试在后台失败时,整条流水线直接挂掉,没有"自己发现错误并修复"的机制。

本教程围绕这三个痛点,给出两条主线的工程化解法,并用一个自愈式 CI 审查器把所有原语串成一条可运行的流水线。

适用人群:已经能用 claude -p 跑通基础无头任务的工程师;正在把 Claude Code 接入 GitHub Actions / GitLab CI / Jenkins 的 DevOps 与平台团队;想给 AI 编排加上遥测与自愈能力的架构师。

二、前置条件

# 版本自检
claude --version
jq --version

三、第一根柱子:可观测性——把 -p 变成事件流

3.1 用 --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 仍走各自的提供商凭证。

3.2 三种输出格式与它们的取舍

--output-format 决定了你如何消费 Claude 的产出:

# 拿结构化结论与单次成本
claude --bare -p "总结本项目" --output-format json | jq '{result, cost: .total_cost_usd, session: .session_id}'

3.3 用 --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"]}

3.4 解构 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 是把多步任务串成"有状态流水线"的钥匙(见第五章)。

3.5 一个最小遥测脚本

把上面四类事件汇成一个实时遥测器,就能在 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"进化成一张覆盖整个会话生命周期的反应网。本节只挑与自动化自愈直接相关的事件,避免重复造"提交守卫"那种拦截型用法。

先记住一条贯穿全场的规则——退出码语义:

关键认知:钩子想"硬阻断"必须 exit 2;exit 1 在大多数事件里只是非阻断告警,并不会真的拦住动作。要强制策略,别用钩子,用权限系统。

4.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 凭空假设版本。

4.2 asyncRewake——后台失败即"唤醒" Claude 的自愈原语

这是整篇文章最值得记住的一个字段。给 type:"command" 钩子加上 "asyncRewake": true,它会:

  1. 后台运行,不阻塞 Claude(隐含 async: true);
  2. 后台进程以 exit 2 退出时,立即唤醒 Claude——哪怕会话正闲着;
  3. 把它的 stderr(若 stderr 为空则取 stdout)作为 system reminder 展示给 Claude,让它对"一个长时间运行的后台失败"做出反应。

普通 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,但更稳妥的做法是把"无限"交给外层编排器(见第五章)。

4.3 Stop 与 StopFailure——轮次结束的两个出口

{
  "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 等。

4.4 PostToolBatch——批量工具结算后注入上下文

当一个整批并行工具调用结算完、下一轮模型调用之前,PostToolBatch 触发(无 matcher,每次都触发)。它适合做"低成本的全局检查点"——比如统计本轮改了多少文件,把摘要塞回上下文,帮 Claude 在长任务里保持方向感。

4.5 钩子调试三件套

钩子不触发是最常见的坑。排查靠这三招:

# 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 审查器

把三条柱子合起来,造一个自愈式 CI 审查器:它审查 PR、自动跑测试,测试挂了就唤醒 Claude 修复,全部过程通过 stream-json 暴露遥测,最终用 --json-schema 产出结构化报告写进 PR 评论。

6.1 工作流总览

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

6.2 项目结构与配置

.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" }
        ] }
    ]
  }
}

6.3 编排主脚本

#!/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

6.4 三个关键设计决策

  1. 双层自愈:asyncRewake 负责进程内的即时修复(改完文件→后台测试→失败唤醒),--resume 循环负责进程外的跨调用兜底。前者快但有 10 分钟天花板,后者慢但可控、可计费、可观测。
  2. --bare + 显式 --allowedTools:CI 在任何队友机器上都行为一致,且 Bash(npm test) 这种带空格的权限规则精确限定只能跑测试相关命令——注意 Bash(npm test*) 没空格会误匹配 npm test-foo,空格不可省。
  3. 遥测与决策分离: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' 进监控即可。

八、延伸阅读

九、训练营适用性评估

维度 评估
适合的训练营类型 进阶 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 工程化实战。

参考来源

  1. Run Claude Code programmatically — Claude Code Docs — 官方 headless 模式文档,-p/--bare/stream-json/--resume/--json-schema 的权威来源。
  2. Hooks reference — Claude Code Docs — 官方钩子参考,涵盖 30+ 生命周期事件、asyncRewake、Setup、StopFailure 与退出码语义。
  3. Automate actions with hooks — Claude Code Docs — 官方钩子上手指南,含 20+ 可复制配置示例。
  4. claude-code-automation — Skills — 社区封装的 headless + streaming 编排技能,对照参考实现。
  5. Headless --print --output-format=stream-json mode — GitHub Issue #53584 — 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学通信知识星球海报-扫码加入