本教程把 Claude Code 从一个需要人盯着的交互式助手,改造成一个可编程、可观测、可断点续传的命令行自动化引擎。你将掌握三件套的深度联动:-p(--print)无头调用与 --bare 纯净启动、--output-format 结构化输出与 system 事件流的脚本消费、以及 hooks 事件系统中非护栏类的会话生命周期 / 上下文压缩 / Stop 决策控制钩子。最终用约 150 行 shell 构建一个能处理成百上千个任务、自动快照上下文、失败可续跑的批量 Agent 编排器。适合已经写过一两个 Claude Code 项目、想把 Agent 嵌入到脚本与定时任务里的中阶开发者。
claude --version 能输出版本号,建议 v2.1.160+,本教程用到的 --bare、PostCompact、Stop decision control 等特性在 2.1.x 系列稳定)ANTHROPIC_API_KEY 或已登录的 OAuth 凭证)jq(解析 JSON 输出)、git(实战案例用)交互模式下,Claude Code 是一个 TUI 应用:它读你敲的字符、渲染彩色界面、在工具调用前等你按 y。这套机制对人类友好,但对脚本完全不可用——脚本没法"按回车",也没法解析混在 ANSI 转义码里的输出。
-p(等价 --print)切换的是范式:Claude Code 变成一个普通的 Unix 命令行工具,输入来自命令行参数或 stdin,输出写到 stdout,退出码反映成败。于是它就能被 shell 脚本、make、cron、CI 流水线像调用 grep、curl 一样调用。
# 交互模式:进入 TUI
claude
# 无头模式:一条命令,一个回答,结束
claude -p "这个仓库的入口文件是哪个?"
-p 模式完整继承了所有 CLI 选项,这意味着交互模式能做的事,无头模式基本都能做——只是去掉了人机对话的循环。关键认知:无头不是阉割版,而是同一个 agent 内核暴露给程序的接口。
直接 claude -p 时,Claude Code 会加载和交互模式一样多的上下文:工作目录与 ~/.claude 下的 hooks、skills、plugins、MCP 服务器、auto memory、CLAUDE.md。这在本地很方便,但在脚本里是可复现性的头号杀手——同事机器上恰好装了一个拦截 Bash 的 hook,你的脚本在他那里就跑挂了。
--bare(裸模式)跳过所有自动发现,只认你显式传进去的东西:
# 纯净环境跑一次性任务,预批准 Read 工具,避免权限弹窗
claude --bare -p "总结这个文件" --allowedTools "Read"
裸模式下 Claude 默认只有 Bash、文件读、文件编辑三类工具。任何额外上下文都要用 flag 显式喂入:
| 想加载什么 | 用哪个 flag |
|---|---|
| 追加系统提示 | --append-system-prompt / --append-system-prompt-file |
| 自定义 settings | --settings <file> |
| MCP 服务器 | --mcp-config <file> |
| 自定义子代理 | --agents <file> |
| 插件 | --plugin-dir / --plugin-url |
裸模式跳过 OAuth 和钥匙串读取,认证只能来自 ANTHROPIC_API_KEY 或 --settings 里的 apiKeyHelper。官方明确:--bare 是脚本和 SDK 调用的推荐模式,未来会成为 -p 的默认行为。从第一天就用 --bare 写脚本,等于锁定了一份随时可迁移到任何机器的执行契约。
常见坑:脚本里忘加 --bare,在 A 机器能跑、B 机器报错。排查时先 diff 两台机器的 ~/.claude/settings.json 与项目 .claude/ 目录。养成肌肉记忆:脚本里的 claude -p 一律改成 claude --bare -p。
--output-format 决定 stdout 里到底是什么,直接决定你的脚本怎么解析:
text(默认):纯文本回答,最适合人类阅读或简单拼接。
# 把构建错误喂进去,结果写到文件
cat build-error.txt | claude -p '简明指出这个构建错误的根因' > output.txt
json:单条结构化 JSON,包含 result(文本回答)、session_id、usage、total_cost_usd(每模型成本拆分)。这是脚本化编排的基石——session_id 让你能续跑同一对话。
# 用 jq 取出文本部分
claude -p "总结这个项目" --output-format json | jq -r '.result'
# 用 jq 取出会话 ID,留给后续 --resume 用
session_id=$(claude -p "开始审查" --output-format json | jq -r '.session_id')
stream-json:换行分隔的 JSON 流(NDJSON),每行一个事件,配合 --verbose 与 --include-partial-messages 能拿到逐 token 的实时输出。长任务用它做实时仪表盘,而不是傻等几十秒后一次性拿结果。
# 只过滤出文本增量,连续打印出流式回答
claude -p "解释递归" --output-format stream-json --verbose --include-partial-messages \
| jq -rj 'select(.type=="stream_event" and .event.delta.type?=="text_delta") | .event.delta.text'
脚本要稳定消费 Agent 输出,光靠 JSON 不够——result 字段是自由文本,模型可能哪天换个措辞就解析失败。--json-schema 让 Claude 把回答严格塞进你定义的 JSON Schema,结果落在 structured_output 字段,可被程序零歧义解析:
# 提取 auth.py 的函数名,返回字符串数组
claude -p "提取 auth.py 里的主要函数名" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
| jq '.structured_output'
这条命令是把 LLM 变成确定性数据提取管道的关键。批量任务里,让 Agent 对每个输入文件产出统一 schema 的结构化结果,下游 jq、pandas、数据库入库都能稳定衔接。
stream-json 模式下,除了文本增量,还有一类 system 事件是脚本的金矿:
system/init:流里第一条事件,报告本会话的 model、tools、MCP 服务器、plugins、plugin_errors。用它在 CI 里检测插件加载失败——plugin_errors 非空就让流水线挂掉。system/api_retry:API 请求遇到可重试错误(限流、过载等)时触发,字段含 attempt、max_retries、retry_delay_ms、error(如 rate_limit、overloaded)。用它给监控大盘加一条"Agent 正在重试"的指标,或在长时间限流时主动切换模型。system/plugin_install:marketplace 插件安装进度,用于自研 UI 的进度条。# 把整条流存档,同时实时挑出重试事件打日志
claude -p "重构这个模块" --output-format stream-json --verbose \
| tee run.jsonl \
| jq -rc 'select(.type=="system" and .subtype=="api_retry") | "retry #\(.attempt)/\(.max_retries) in \(.retry_delay_ms)ms: \(.error)"'
一个完整可观测的 Agent 调用,外壳只需上面这一行:原始事件落盘 run.jsonl 供事后复盘,关键事件实时投影到终端。
无头模式默认是"一问一答就结束"。但 --continue(续最近一次对话)和 --resume <id>(续指定会话)能打破单次边界,让多步推理在多次 -p 调用间延续:
# 第一轮:开启审查
session_id=$(claude -p "审查这个代码库的性能问题" --output-format json | jq -r '.session_id')
# 第二轮:在同一会话里深入
claude -p "现在聚焦到数据库查询" --resume "$session_id"
# 第三轮:生成汇总
claude -p "生成所有发现问题的摘要" --resume "$session_id"
注意:会话 ID 的查找限定在当前项目目录及其 git worktree。链式调用的两次 claude 必须在同一个目录下跑。把 session_id 写进状态文件,就能实现"今天干到一半,明天接着干"的断点续传——这正是后面实战案例的核心机制。
很多人对 hooks 的认知停留在 PreToolUse 拦截危险命令(那是"造确定性护栏")。但 2026 版的 hooks 事件体系远不止于此,按触发节奏分三类:
SessionStart、SessionEndUserPromptSubmit、Stop、StopFailure(API 错误结束)PreToolUse、PostToolUsePreCompact、PostCompactSubagentStart、SubagentStop、TaskCreated、TaskCompletedNotification、MessageDisplayElicitation、ElicitationResult所有钩子都通过 stdin 收到一份统一 JSON,含 session_id、transcript_path(对话 JSON 路径)、cwd、permission_mode、effort.level 等字段。钩子类型也不再只是 shell:type 可以是 "command"(shell)、"http"(打 HTTP 端点)、"prompt"(让 LLM 跑一段提示)。下面挑护栏之外、最适合自动化编排的几类讲透。
每跑一次 claude -p 都会触发一次 SessionStart 和 SessionEnd。把审计逻辑挂在这两个事件上,无需改动任何 prompt,就能给所有自动化调用建一份"谁、什么时候、在哪个目录、跑了什么"的台账:
{
"hooks": {
"SessionStart": [{
"hooks": [{
"type": "command",
"command": "jq -rc '{ts:now|todateiso8601, sid:.session_id, cwd:.cwd, source:.source}' >> ~/.claude/audit.log"
}]
}],
"SessionEnd": [{
"hooks": [{
"type": "command",
"command": "echo \"end $(jq -r .session_id)\" >> ~/.claude/audit.log"
}]
}]
}
}
SessionStart 的输入含 source 字段(标明是 cli、cli-wrapper 还是 SDK 等来源),便于区分交互调用与脚本调用。注意:审计钩子要轻、要快——它们串在会话关键路径上,慢钩子会拖慢每一次启动。
长任务里上下文必然触发压缩(compact)。PreCompact 在压缩前触发,PostCompact 在压缩后触发。这是被严重低估的一对事件:压缩意味着细节会丢,而 PreCompact 是你"抢救"完整对话的最后机会。
把 transcript 备份挂上去,等于给每次压缩留一份原始档案;PostCompact 里再把压缩后的精简状态写一笔,就形成了"压缩前/压缩后"的对照记录:
{
"hooks": {
"PreCompact": [{
"hooks": [{
"type": "command",
"command": "cp \"$(jq -r .transcript_path)\" \"~/.claude/snapshots/$(jq -r .session_id)-pre.json\" 2>/dev/null || true"
}]
}]
}
}
在批量任务里,这个钩子让你事后能回答"任务在第 3 次压缩后行为变了,当时它看到了什么"——这对调试 Agent 漂移、定位"为什么同一批任务有的成功有的失败"极有价值。
Stop 钩子最强大的不是"记录结束",而是 decision control:钩子可以让 Claude 不要停下来,继续干。Stop 事件的输入里有 stop_hook_active(防递归标志)、last_assistant_message、background_tasks、session_crons 等字段。钩子通过 stdout 输出决策 JSON:
{
"hooks": {
"Stop": [{
"hooks": [{
"type": "command",
"command": "my-stop-checker.sh"
}]
}]
}
}
my-stop-checker.sh 读取 last_assistant_message,若发现回答里含"TODO:"或"待补全"这类未完成标记,就输出 {"decision":"block","reason":"检测到未完成项,请继续"},Claude 便会续跑下一轮,直到真正干完。stop_hook_active 字段用于防止无限循环——递归触发时它为 true,钩子应直接放行。SubagentStop 同理,可对子代理施加同样的"做完才算完"约束。
这条机制把"任务是否完成"的判定权从模型手里挪到你写的确定性脚本手里,是自动化任务质量的保险栓。
Notification 在 Claude Code 发通知(如等待输入、任务完成)时触发。无头模式下没有 TUI,所有"该提醒人"的信号都走这里。挂一个推送到手机或 IM 的钩子,长跑任务结束你就能收到通知,而不必守在终端前:
{
"hooks": {
"Notification": [{
"hooks": [{
"type": "command",
"command": "jq -r .message | notify-send 'Claude Code' $(cat) || curl -s -X POST $BOT_WEBHOOK -d @-"
}]
}]
}
}
--bare 模式不加载这些钩子,所以Notification 等钩子只在非 bare 的 -p 调用里生效;这是选 --bare(可复现)还是非 bare(享受钩子生态)时要权衡的取舍点。
把上述拼成一个真实可跑的系统:对一个仓库的每个 .py 文件做安全审查,串行处理、失败可续跑、自动快照上下文、任务完成自校验、跑完推送通知。
项目结构:
batch-review/
├── run.sh # 编排主脚本
├── state.tsv # 进度状态:文件名 \t session_id \t 状态
├── review/ # 每个文件一个结果 JSON
└── .claude/settings.json # hooks 配置
.claude/settings.json 配好 PreCompact 快照 + Stop 自校验 + SessionStart 审计 + Notification 推送(即第八到十一步的钩子)。
run.sh 编排逻辑(约 60 行核心):
#!/usr/bin/env bash
set -euo pipefail
MODEL="claude-sonnet-4-6"
# 1. 待审文件清单(第一次跑生成;后续跑复用,断点续传的基础)
if [[ ! -f state.tsv ]]; then
for f in $(git ls-files '*.py'); do
printf '%s\t\tPENDING\n' "$f" >> state.tsv
done
fi
# 2. 串行处理每个 PENDING 文件
while IFS=$'\t' read -r file sid status; do
[[ "$status" == "DONE" ]] && continue
# 3. 该文件首次处理:发起审查,捕获 session_id
if [[ -z "$sid" ]]; then
sid=$(claude --bare -p "审查 $file 的安全漏洞,只输出问题清单" \
--allowedTools "Read" --output-format json \
--json-schema '{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"line":{"type":"integer"},"severity":{"type":"string"},"desc":{"type":"string"}},"required":["line","severity","desc"]}}},"required":["issues"]}' \
| jq -r '.session_id') || { echo "FAIL init $file"; continue; }
# 回写 session_id,崩溃后能 --resume
sed -i "s|^${file}\t\t|${file}\t${sid}\t|" state.tsv
fi
# 4. 用 session_id 续跑:让模型把结果落盘(Stop 钩子会校验是否真写完)
if claude -p "把审查结果写入 review/$(basename $file).json,写完回复 DONE" \
--resume "$sid" --allowedTools "Write,Read"; then
sed -i "s|^${file}\t${sid}\t.*|${file}\t${sid}\tDONE|" state.tsv
echo "OK $file"
else
echo "RETRY $file (下次重跑会 --resume $sid)"
fi
done < state.tsv
运行与续跑:
./run.sh # 首跑
# Ctrl-C 中断,或网络中断
./run.sh # 再次运行:从 state.tsv 的 PENDING 行续跑,已 DONE 的跳过
这个案例把十一步全部串了起来:--bare 保证每台机器一致;--json-schema 让每个文件产出统一格式便于汇总;session_id 写进 state.tsv 实现任务级断点续传;PreCompact 钩子在每次压缩前备份 transcript,事后可追溯;Stop 钩子校验 review/*.json 真正落盘才允许结束;Notification 在全部完成时推送。串行而非并行是有意为之——它换来可预测的 token 消耗、清晰的失败定位、与 state.tsv 一一对应的状态机,这正是与"并行扇出"完全不同的工程权衡。
flowchart TD
A[run.sh 读取 state.tsv] --> B{行状态?}
B -->|DONE| Z[跳过]
B -->|PENDING| C[claude --bare -p 首审<br/>--json-schema 锁结构]
C --> D[捕获 session_id 回写 state.tsv]
D --> E[claude -p --resume 续跑落盘]
E --> F{Stop 钩子校验<br/>结果文件真落盘?}
F -->|否| E
F -->|是| G[标记该行 DONE]
G --> H{还有 PENDING?}
H -->|是| B
H -->|否| I[Notification 推送完成]
C -.压缩时.-> PC[PreCompact 钩子<br/>备份 transcript 快照]
C -.会话起止.-> SS[SessionStart/End<br/>写入 audit.log]
--bare,本地 hook/skill/MCP 被加载。统一加 --bare,需要的能力用 flag 显式传。--resume 找不到会话:两次 claude 不在同一目录。会话查找限定当前项目目录与 git worktree,必须在同一目录下跑。stop_hook_active。递归触发时它为 true,钩子必须放行,否则 agent 反复"被要求继续"。-p 挂住:v2.1.182 起 background subagent 等待上限默认 10 分钟,用 CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS 调整,设 0 为无限等待。code.claude.com/docs/en/headlesscode.claude.com/docs/en/hookscode.claude.com/docs/en/cli-referencecode.claude.com/docs/en/agent-sdk/overview--permission-mode:acceptEdits / dontAsk / plan 的取舍jq。-p + session_id 续跑 + Stop 校验即可完成最小闭环。run.sh,验证已完成任务不重复、未完成任务从断点续跑、review/*.json 数量与源文件一致。-p/--bare/--output-format/--json-schema/--resume/system 事件流的权威定义claude -p 嵌入 CI/CD 的命令参考与编排示例[!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 等场景全覆盖。
