首页 › tutorials › 非交互Agent实战:-p模式配事件钩子

概述

本教程把 Claude Code 从一个需要人盯着的交互式助手,改造成一个可编程、可观测、可断点续传的命令行自动化引擎。你将掌握三件套的深度联动:-p(--print)无头调用与 --bare 纯净启动、--output-format 结构化输出与 system 事件流的脚本消费、以及 hooks 事件系统中非护栏类的会话生命周期 / 上下文压缩 / Stop 决策控制钩子。最终用约 150 行 shell 构建一个能处理成百上千个任务、自动快照上下文、失败可续跑的批量 Agent 编排器。适合已经写过一两个 Claude Code 项目、想把 Agent 嵌入到脚本与定时任务里的中阶开发者。

前置条件

第一步:理解交互模式与无头模式的根本区别

交互模式下,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 内核暴露给程序的接口。

第二步:用 --bare 锁定可复现的执行环境

直接 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'

第四步:用 --json-schema 锁死结构化输出

脚本要稳定消费 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、数据库入库都能稳定衔接。

第五步:用 system 事件流做实时遥测

stream-json 模式下,除了文本增量,还有一类 system 事件是脚本的金矿:

# 把整条流存档,同时实时挑出重试事件打日志
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 供事后复盘,关键事件实时投影到终端。

第六步:session_id 传递——把单次调用串成链

无头模式默认是"一问一答就结束"。但 --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 事件系统——护栏之外的全景

很多人对 hooks 的认知停留在 PreToolUse 拦截危险命令(那是"造确定性护栏")。但 2026 版的 hooks 事件体系远不止于此,按触发节奏分三类:

所有钩子都通过 stdin 收到一份统一 JSON,含 session_id、transcript_path(对话 JSON 路径)、cwd、permission_mode、effort.level 等字段。钩子类型也不再只是 shell:type 可以是 "command"(shell)、"http"(打 HTTP 端点)、"prompt"(让 LLM 跑一段提示)。下面挑护栏之外、最适合自动化编排的几类讲透。

第八步:SessionStart/SessionEnd——零侵入的审计遥测

每跑一次 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 等来源),便于区分交互调用与脚本调用。注意:审计钩子要轻、要快——它们串在会话关键路径上,慢钩子会拖慢每一次启动。

第九步:PreCompact/PostCompact——自动保存上下文快照

长任务里上下文必然触发压缩(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——让 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——给无头任务接上提醒通道

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]

常见问题与排查

延伸阅读

训练营适用性评估

参考来源

  1. Run Claude Code programmatically — Claude Code Docs — 官方 headless 文档,-p/--bare/--output-format/--json-schema/--resume/system 事件流的权威定义
  2. Hooks reference — Claude Code Docs — 官方钩子参考,含 SessionStart/End、PreCompact/PostCompact、Stop decision control、async/HTTP/prompt hooks 的完整事件表与 JSON schema
  3. CLI Output & Headless Mode — DeepWiki — 非交互输出管线与 stream-json 事件解析的社区深度解析
  4. Claude Code Headless Mode: The Definitive Guide to claude -p — buildthisnow — 无头模式从脚本到 CI 的实践指南,含批量调用模式
  5. Headless mode and CI/CD command reference — SFEIR Institute — 把 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 等场景全覆盖。

51学通信知识星球海报-扫码加入