把 claude -p 丢进 CI 或定时任务后,最痛的不是它跑得慢,而是跑到一半断在网络抖动、限流(rate_limit)、模型过载(overloaded)或账单异常上,全部上下文归零,从头再来。本教程聚焦一个被多数自动化教程忽略的能力:会话持久化与断点续跑。你将学会用 --output-format json 捕获 session_id、用 --resume 在崩溃后接续同一对话、用 StopFailure 钩子做失败观测、并写一个能在 API 错误后自动续跑的"自愈式"包装脚本。适合已经会用 -p 跑一次性任务、想把 Claude Code 真正塞进长流程自动化的工程师。
defer、v2.1.128 的 stdin 上限、v2.1.163 的后台任务回收、v2.1.181 的 -p 下 /config 等行为,这些在当前 2.1.x 版本均已落地)。claude CLI 并完成认证的机器;jq 必备,column 可选。git 仓库的真实项目目录——续跑是按"项目目录 + git worktree"作用域的,所以目录不能随便换。-p/--print 非交互模式的基础用法(管道、--allowedTools、--output-format)。很多人写自动化脚本失败,是因为默认 claude -p 是"无状态"的——其实不是。每一段对话都被持续落盘成 JSONL 转录文件:
~/.claude/projects/<把工作目录路径编码后的串>/<session-id>.jsonl
也就是说,你的工作目录路径决定了会话存到哪个项目桶里。这带来三条铁律,后面每一步都绕不开:
No conversation found with session ID: ...。会话查找的作用域是"当前项目目录及其 git worktree",换个不相关的目录就找不到。-p 创建的会话不会出现在 /resume 选择器里,只能用 --resume <session-id> 按 ID 续。这是脚本场景最常踩的坑——你不能指望人去选择器里点。cleanupPeriodDays 默认 30)。如果你的巡检要跨月,要么调大这个值,要么把状态目录挪到不会被清的地方。记住这三点,后面的设计就顺了。整张状态流转如下:
flowchart LR
subgraph 一次claude-p调用
A["claude -p '任务'<br/>--output-format json"] -->|stdout| B["JSON: result / session_id /<br/>is_error / stop_reason"]
end
B --> C["解析出 session_id<br/>写入状态文件"]
C --> D[("~/.claude/projects/<cwd>/<sid>.jsonl<br/>持续落盘的对话转录")]
D -.崩溃后.-> E["claude -p '继续' --resume SID"]
E --> B
自愈的前提是"能找回现场"。--output-format json 会在响应里带上 session_id,把它捕获下来:
# 跑一次审查任务,拿到 session_id 并落盘
out=$(claude -p "审查 src/ 目录里的性能问题,先列出 top 5" \
--allowedTools "Read,Grep,Glob" \
--output-format json)
# 提取关键字段
session_id=$(jq -r '.session_id' <<<"$out")
cost=$(jq -r '.total_cost_usd // 0' <<<"$out")
result=$(jq -r '.result' <<<"$out")
# 落盘,供后续续跑或排障使用
echo "$session_id" > .last-session
printf '%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "$session_id" "$cost" >> .agent-runs.tsv
echo "$result"
echo "session_id = $session_id (已保存到 .last-session)"
预期结果:out 是一个 JSON 对象,至少包含 result(文本结论)、session_id(UUID)、total_cost_usd(本次花费)。.last-session 里写入了这次对话的 ID。
常见问题:
jq 报错、session_id 为空:八成是这次调用压根没进到"会话持久化"。检查是否误加了 --no-session-persistence(它显式关闭转录写入,那就没法续了)。另一个原因是 stdin 输入超过 10MB 上限(v2.1.128 引入),超了会非零退出、不给 JSON——把大输入写进文件、在 prompt 里引用路径,而不是用管道喂。~/.claude/projects/ 下找到对应当前目录编码名的文件夹,能看到一个 <session_id>.jsonl,每行是一条消息/工具调用/元数据。--continue vs --resume <id>拿到 session_id 后,续跑有两条路,别用混:
| 命令 | 行为 | 适用场景 |
|---|---|---|
claude -p "..." --continue |
续接当前目录最近一次会话 | 单线推进、你确定没别的并发会话 |
claude -p "..." --resume <id> |
续接指定会话 | 脚本场景、多仓库并发——唯一可靠的选择 |
claude --from-pr <url> |
续接与某个 PR 绑定的会话 | CI 里按 PR 续跑 |
脚本里永远用 --resume <id>,因为 --continue 的"最近一次"在并发或重试时会被别的调用顶掉,不可靠:
session_id=$(cat .last-session)
claude -p "刚才你列了 top 5,现在重点看数据库查询那两条,给出优化方案" \
--resume "$session_id" \
--allowedTools "Read,Grep,Glob" \
--output-format json | jq -r '.result'
续接后,上一轮的上下文(读过的文件、得出的中间结论)都在,模型不会重复劳动——这正是省 token、省时间的关键。
给会话起名(强烈推荐):用 -n <name> 或 /rename 命名,--resume <name> 就能按名续,且名字会在同仓库的 worktree 间解析:
claude -n dep-upgrade-api -p "升级 services/api 的依赖" --output-format json | jq -r .session_id
# 之后任意位置、同仓库内:
claude -p "继续" --resume dep-upgrade-api
常见问题:
No conversation found with session ID:99% 是目录不对。续跑必须在创建该会话的同一目录(或其 worktree)下发起。脚本里务必先 cd 到目标项目目录。--continue 续到了"错误的"会话:因为最近一次被别的任务占了。换成 --resume <id> 即可。claude --resume <id> --fork-session 会复制一份对话进入新分支,原会话不动;交互里则是 /branch。注意"本会话允许"的权限不会带到分支。StopFailure 钩子观测每一次崩溃续跑能力有了,但"什么时候该续"得先能发现。StopFailure 钩子专门在"一轮因 API 错误而结束"时触发,并且 matcher 可以精确到错误类型:
| matcher 值 | 含义 |
|---|---|
rate_limit |
触发限流,通常等一会能好 |
overloaded |
模型过载,服务端 529 类 |
server_error |
服务端错误 |
billing_error |
账单/额度问题——续跑也没用,要人工 |
authentication_failed |
鉴权失败 |
max_output_tokens |
单轮输出超长 |
在 .claude/settings.json(项目级,可提交)里挂一个失败记录钩子:
{
"hooks": {
"StopFailure": [
{
"matcher": "rate_limit|overloaded|server_error",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/on-failure.sh",
"args": []
}
]
}
]
,
"Stop": [
{
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/on-stop.sh", "args": [] }
]
}
]
}
}
StopFailure 的关键特性:它的输出和退出码会被忽略——纯做副作用(日志、告警、打标记)。钩子脚本从 stdin 收到 JSON(含 session_id、transcript_path 等公共字段),把"需要续跑"的标记写出来:
#!/usr/bin/env bash
# .claude/hooks/on-failure.sh —— chmod +x
input=$(cat)
sid=$(jq -r '.session_id' <<<"$input")
evt=$(jq -r '.hook_event_name' <<<"$input")
ts=$(date -u +%FT%TZ)
# 写失败日志
printf '%s\t%s\t%s\n' "$ts" "$sid" "$evt" >> "${CLAUDE_PROJECT_DIR}/.agent-failures.log"
# 打一个"待续跑"标记,供包装脚本或定时器读取
echo "$sid" > "${CLAUDE_PROJECT_DIR}/.resume-needed"
exit 0
on-stop.sh(正常结束时清掉待续跑标记、记录成本):
#!/usr/bin/env bash
# .claude/hooks/on-stop.sh —— chmod +x
input=$(cat)
sid=$(jq -r '.session_id' <<<"$input")
rm -f "${CLAUDE_PROJECT_DIR}/.resume-needed"
printf '%s\t%s\tCLEAN_STOP\n' "$(date -u +%FT%TZ)" "$sid" >> "${CLAUDE_PROJECT_DIR}/.agent-runs.tsv"
exit 0
预期结果:一旦某轮撞上 rate_limit/overloaded/server_error,.agent-failures.log 多一行、.resume-needed 里写下 session_id;正常结束则标记被清。
常见问题:
--bare。--bare 会跳过 hooks/skills/plugins/MCP/CLAUDE.md 的自动发现,钩子根本不加载。所以"钩子观测"和"bare 模式"二选一;要省启动开销用 bare,要钩子就用普通模式。exit 1 不生效:钩子里退出码 1 是非阻塞错误,会被忽略;要真"阻断"得用 exit 2。但 StopFailure 本来就忽略退出码,所以这里 exit 0 即可。cwd,脚本里可以按目录分流日志。⚠️ 注意
Stop/UserPromptSubmit/PostToolBatch等不支持 matcher,写了会被静默忽略;StopFailure才支持错误类型 matcher。
把前面几步合起来:包装脚本捕获 session_id,一旦检测到 is_error 或进程异常退出,就用 --resume 把同一个会话接续下去,并告诉它"上次中断了,从断点继续,别重复已完成的步骤"。
#!/usr/bin/env bash
# selfheal.sh —— 在 API 错误后自动用 --resume 续跑,直到完成或超上限
set -uo pipefail
PROMPT="$1"
MAX_ATTEMPTS="${2:-5}"
SESSION_ID=""
PERMISSIONS="Read,Edit,Bash(git *),Bash(npm test)"
for attempt in $(seq 1 "$MAX_ATTEMPTS"); do
if [[ -z "$SESSION_ID" ]]; then
# 首次运行
out=$(claude -p "$PROMPT" \
--permission-mode acceptEdits \
--allowedTools "$PERMISSIONS" \
--output-format json 2>.selfheal.err)
else
# 续跑:明确告知中断、要求接着干
out=$(claude -p "上一次运行因错误中断(第 ${attempt} 次重试)。请从断点继续,不要重复已完成的工作。" \
--resume "$SESSION_ID" \
--permission-mode acceptEdits \
--allowedTools "$PERMISSIONS" \
--output-format json 2>.selfheal.err)
fi
# 进程本身崩了(OOM/网络断/SIGKILL),没有 JSON
if ! jq -e . >/dev/null 2>&1 <<<"$out"; then
echo "⚠️ 第 $attempt 次:进程异常退出(见 .selfheal.err)" >&2
sleep $((attempt * 5))
continue
fi
SESSION_ID=$(jq -r '.session_id // ""' <<<"$out")
is_error=$(jq -r '.is_error // false' <<<"$out")
reason=$(jq -r '.stop_reason // ""' <<<"$out")
if [[ "$is_error" == "false" ]]; then
jq -r '.result' <<<"$out"
exit 0
fi
echo "⚠️ 第 $attempt 次失败 (stop_reason=$reason),session=$SESSION_ID,退避后续跑..." >&2
sleep $((attempt * 5)) # 简单指数退避
done
echo "❌ 超过最大重试次数 $MAX_ATTEMPTS。最终会话:$SESSION_ID" >&2
echo "手动续跑:claude --resume $SESSION_ID" >&2
exit 1
调用:
./selfheal.sh "把 auth 模块的老式回调改写成 async/await,并保证 npm test 全绿"
预期结果:哪怕中途撞上限流,脚本也会在退避后用同一个 session_id 续上,模型接着干而不是从头改。.selfheal.err 留有每次的 stderr 便于排障。
设计要点:
rate_limit/overloaded 多半等一会就好,线性/指数退避比硬刚强。--resume 与权限模式独立:--resume 只恢复对话上下文;--allowedTools/--permission-mode 仍要每次显式传。脚本里我固定传 acceptEdits+白名单,保证续跑后权限行为一致。常见问题:
--compact 压缩过上下文时偶发。可在续跑提示里补一句目标摘要;或对超长任务先 fork 再续(--fork-session),保留干净主线。selfheal.sh 必须在被操作项目的根目录运行。billing_error/authentication_failed 也被无脑重试:这两类续跑无用。把它们的判断加进循环(读到这两类就立即 exit 并告警),别浪费重试次数。stream-json 的 api_retry 事件做退避可视化--output-format stream-json 会在 API 重试时发出 system/api_retry 事件,字段含 attempt、max_retries、retry_delay_ms、error 类别。你可以一边跑一边把重试进度打到看板:
claude -p "生成迁移脚本" \
--output-format stream-json --verbose \
| jq -rc 'select(.type=="system" and .subtype=="api_retry")
| "[retry] #\(.attempt)/\(.max_retries) 等 \(.retry_delay_ms)ms — \(.error)"' \
>> .retry-trace.log &
这样你能在 .retry-trace.log 里看到"第 2/10 次重试,等了 1200ms,原因是 rate_limit",据此判断是该继续等、还是该熔断告警。结合第五步的包装脚本,重试由包装脚本掌控(跨进程续跑),观测由 stream-json 提供(单进程内),两者互补。
把自愈 Agent 放到生产前,再做四件事:
1. 给会话起稳定名字,别只靠 UUID。脚本里用业务前缀命名,便于人肉排障:
claude -n "patrol-$(basename "$PWD")-$(date +%Y%m%d)" -p "..." --output-format json
2. 长 batch 用独立的配置目录,避开 30 天清理。CLAUDE_CONFIG_DIR 把所有会话状态挪到一个不会被清、且易备份的位置;cleanupPeriodDays 调大保留期:
export CLAUDE_CONFIG_DIR=/data/agent-state
// .claude/settings.json
{ "cleanupPeriodDays": 90 }
3. 大输入走文件,别走管道。stdin 上限 10MB(v2.1.128)。把大日志/大 diff 写文件,prompt 里给路径:
git diff main > /tmp/d.diff
claude -p "审查 /tmp/d.diff 这个 diff 里的拼写和明显 bug" --allowedTools Read
4. 后台任务别撑住进程。v2.1.163 起,claude -p 结束后约 5 秒会回收它启动的后台 Bash 任务(如 dev server),给了个收尾宽限期。但若你故意要保留一个长驻服务,别指望它活过 -p 调用——那是设计如此。
把全部能力串起来:对一组仓库逐个跑"升级 patch 依赖 → 测试 → 提交",每个仓库独立捕获 session_id,崩溃自动续跑,最后出一张状态表。
#!/usr/bin/env bash
# repo-upgrade-patrol.sh
set -uo pipefail
ROOT=$(pwd)
REPOS=(services/api services/auth services/billing web/frontend)
STATE="$ROOT/.patrol-state.tsv"
PERMISSIONS="Read,Edit,Bash(npm install),Bash(npm test),Bash(git *)"
printf 'repo\tsession_id\tstatus\tattempts\n' > "$STATE"
upgrade_one() {
local repo="$1" sid="" n=0
cd "$ROOT/$repo" || { printf '%s\t-\tSKIP\t-\n' "$repo" >> "$STATE"; return; }
while (( n < 3 )); do
n=$((n+1))
local out
if [[ -z "$sid" ]]; then
out=$(claude -n "patrol-$repo" -p \
"升级 package.json 中所有 patch 版本依赖到最新,npm install 后跑 npm test,全绿就 git commit。" \
--permission-mode acceptEdits --allowedTools "$PERMISSIONS" \
--output-format json)
else
out=$(claude -p "上次中断,从断点继续,不要重复已完成步骤。" \
--resume "$sid" --permission-mode acceptEdits \
--allowedTools "$PERMISSIONS" --output-format json)
fi
jq -e . >/dev/null 2>&1 <<<"$out" || { sleep 5; continue; }
sid=$(jq -r '.session_id // ""' <<<"$out")
if [[ "$(jq -r '.is_error // false' <<<"$out")" == "false" ]]; then
printf '%s\t%s\tOK\t%s\n' "$repo" "$sid" "$n" >> "$STATE"
cd "$ROOT"; return 0
fi
sleep $((n*5))
done
printf '%s\t%s\tFAILED\t%s\n' "$repo" "$sid" "$n" >> "$STATE"
cd "$ROOT"
}
for r in "${REPOS[@]}"; do upgrade_one "$r"; done
echo "===== 巡检结果 ====="; column -t -s$'\t' "$STATE"
FAILED 的仓库会留下 session_id,事后直接 cd services/billing && claude --resume <id> 人工介入即可——现场永远不丢。整体编排:
flowchart TD
S([巡检脚本启动]) --> L{"遍历仓库列表"}
L -->|每个 repo| R["cd repo; claude -p ... --output-format json"]
R --> J{解析 JSON}
J -->|is_error=false / 非 JSON 重试| OK["记 OK<br/>下一个仓库"]
J -->|is_error=true| CAP["记 session_id"]
CAP --> RT{重试<3?}
RT -->|是| RES["claude -p '继续' --resume SID"]
RES --> J
RT -->|否| FAIL["记 FAILED + SID<br/>留待人工 --resume"]
OK --> L
FAIL --> L
L -->|全部完成| T[[输出状态表]]
/rewind:在单个会话内做"代码 + 对话"检查点回滚,适合"试错了想倒回去"的场景,比 fork 更轻。defer 往返:AskUserQuestion/ExitPlanMode 在 -p 下会卡住;用 PreToolUse 钩子返回 permissionDecision: "defer"(v2.1.89+),进程以 stop_reason: "tool_deferred" 退出、保留挂起工具,外部 UI 收集答案后再 --resume 回灌——这是把 -p Agent 接进"需要人工确认"流程的标准范式。-p 基础关。-p/--bare/--output-format/--resume/--continue 与 stdin 上限、后台任务回收等行为权威说明。--resume/--from-pr、作用域规则、JSONL 转录存储路径与 cleanupPeriodDays 保留期。StopFailure/Stop 事件、matcher 规则、退出码(2 阻断/1 非阻塞)、--bare 跳过 hooks 的行为。-p 标志、输出格式与多轮会话的 18 条实践技巧。--continue/--resume 实操与会话历史检索方法。[!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 等场景全覆盖。
