<!-- title: 非交互Agent实战：-p模式配事件钩子 -->

## 概述

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

## 前置条件

- 已安装 Claude Code（`claude --version` 能输出版本号，建议 v2.1.160+，本教程用到的 `--bare`、PostCompact、Stop decision control 等特性在 2.1.x 系列稳定）
- 已完成首次认证（`ANTHROPIC_API_KEY` 或已登录的 OAuth 凭证）
- 命令行能调用 `jq`（解析 JSON 输出）、`git`（实战案例用）
- 理解 stdin/stdout 管道、退出码、shell 变量传递

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

交互模式下，Claude Code 是一个 TUI 应用：它读你敲的字符、渲染彩色界面、在工具调用前等你按 `y`。这套机制对人类友好，但**对脚本完全不可用**——脚本没法"按回车"，也没法解析混在 ANSI 转义码里的输出。

`-p`（等价 `--print`）切换的是范式：Claude Code 变成一个普通的 Unix 命令行工具，**输入来自命令行参数或 stdin，输出写到 stdout，退出码反映成败**。于是它就能被 shell 脚本、make、cron、CI 流水线像调用 `grep`、`curl` 一样调用。

```bash
# 交互模式：进入 TUI
claude

# 无头模式：一条命令，一个回答，结束
claude -p "这个仓库的入口文件是哪个？"
```

`-p` 模式完整继承了所有 CLI 选项，这意味着交互模式能做的事，无头模式基本都能做——只是去掉了人机对话的循环。**关键认知：无头不是阉割版，而是同一个 agent 内核暴露给程序的接口。**

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

直接 `claude -p` 时，Claude Code 会加载和交互模式一样多的上下文：工作目录与 `~/.claude` 下的 hooks、skills、plugins、MCP 服务器、auto memory、CLAUDE.md。这在本地很方便，但在脚本里是**可复现性的头号杀手**——同事机器上恰好装了一个拦截 Bash 的 hook，你的脚本在他那里就跑挂了。

`--bare`（裸模式）跳过所有自动发现，**只认你显式传进去的东西**：

```bash
# 纯净环境跑一次性任务，预批准 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`（默认）**：纯文本回答，最适合人类阅读或简单拼接。

```bash
# 把构建错误喂进去，结果写到文件
cat build-error.txt | claude -p '简明指出这个构建错误的根因' > output.txt
```

**`json`**：单条结构化 JSON，包含 `result`（文本回答）、`session_id`、`usage`、`total_cost_usd`（每模型成本拆分）。这是脚本化编排的基石——`session_id` 让你能续跑同一对话。

```bash
# 用 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 的实时输出。长任务用它做实时仪表盘，而不是傻等几十秒后一次性拿结果。

```bash
# 只过滤出文本增量，连续打印出流式回答
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` 字段，可被程序零歧义解析：

```bash
# 提取 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` 事件是脚本的金矿：

- **`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 的进度条。

```bash
# 把整条流存档，同时实时挑出重试事件打日志
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` 调用间延续：

```bash
# 第一轮：开启审查
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 事件体系远不止于此，按触发节奏分三类：

- **一次会话**：`SessionStart`、`SessionEnd`
- **一次 turn**：`UserPromptSubmit`、`Stop`、`StopFailure`（API 错误结束）
- **每次工具调用**：`PreToolUse`、`PostToolUse`
- **上下文压缩**：`PreCompact`、`PostCompact`
- **子代理 / 任务**：`SubagentStart`、`SubagentStop`、`TaskCreated`、`TaskCompleted`
- **通知与展示**：`Notification`、`MessageDisplay`
- **MCP elicitation**：`Elicitation`、`ElicitationResult`

所有钩子都通过 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，就能给所有自动化调用建一份"谁、什么时候、在哪个目录、跑了什么"的台账：

```json
{
  "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 里再把压缩后的精简状态写一笔，就形成了"压缩前/压缩后"的对照记录：

```json
{
  "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：

```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 的钩子，长跑任务结束你就能收到通知，而不必守在终端前：

```json
{
  "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 行核心）**：

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

**运行与续跑**：

```bash
./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` 一一对应的状态机，这正是与"并行扇出"完全不同的工程权衡。

## 整体工作流程

```mermaid
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，必须在同一目录下跑。
- **stdin 报错 10MB 上限**：v2.1.128 起 piped stdin 上限 10MB，超了非零退出。大输入写到文件，在 prompt 里引用路径。
- **Stop 钩子无限循环**：忘了检查 `stop_hook_active`。递归触发时它为 true，钩子必须放行，否则 agent 反复"被要求继续"。
- **后台任务让 `-p` 挂住**：v2.1.182 起 background subagent 等待上限默认 10 分钟，用 `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS` 调整，设 `0` 为无限等待。
- **审计钩子拖慢启动**：SessionStart 钩子在关键路径上。保持轻量（只 append 一行），重活异步化或挪到 SessionEnd。

## 延伸阅读

- 官方 Headless 文档（本教程命令参数的权威来源）：`code.claude.com/docs/en/headless`
- 官方 Hooks reference（事件体系、JSON schema、decision control 全表）：`code.claude.com/docs/en/hooks`
- CLI reference（全部 flag 速查）：`code.claude.com/docs/en/cli-reference`
- Agent SDK（Python/TypeScript 里获得回调、工具审批、原生消息对象）：`code.claude.com/docs/en/agent-sdk/overview`
- Permission modes 与 `--permission-mode`：`acceptEdits` / `dontAsk` / `plan` 的取舍

## 训练营适用性评估

- **适用类型**：中阶"Agent 自动化"训练营、DevOps/平台工程方向、CI 工程化方向。不适合零基础营（需 shell 与 JSON 解析基础）。
- **前置能力**：能独立写出含管道与退出码判断的 shell 脚本，会用 `jq`。
- **预计教学时长**：引导讲解 45 分钟 + 动手实现 run.sh 与三类钩子 90 分钟 + 调试断点续传 30 分钟，合计约 3.5 小时。
- **可裁剪路径**：若时间紧，跳过第十一步 Notification 与 system 事件流，聚焦 `-p` + `session_id` 续跑 + Stop 校验即可完成最小闭环。
- **考核建议**：人为中断后重启 `run.sh`，验证已完成任务不重复、未完成任务从断点续跑、`review/*.json` 数量与源文件一致。

## 参考来源

1. [Run Claude Code programmatically — Claude Code Docs](https://code.claude.com/docs/en/headless) — 官方 headless 文档，`-p`/`--bare`/`--output-format`/`--json-schema`/`--resume`/system 事件流的权威定义
2. [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks) — 官方钩子参考，含 SessionStart/End、PreCompact/PostCompact、Stop decision control、async/HTTP/prompt hooks 的完整事件表与 JSON schema
3. [CLI Output & Headless Mode — DeepWiki](https://deepwiki.com/kdxsydq/ClaudeCode/14.2-cli-output-and-headless-mode) — 非交互输出管线与 stream-json 事件解析的社区深度解析
4. [Claude Code Headless Mode: The Definitive Guide to claude -p — buildthisnow](https://www.buildthisnow.com/blog/guide/development/claude-code-headless-mode) — 无头模式从脚本到 CI 的实践指南，含批量调用模式
5. [Headless mode and CI/CD command reference — SFEIR Institute](https://institute.sfeir.com/en/claude-code/claude-code-headless-mode-and-ci-cd/command-reference/) — 把 `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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
