<!-- title: 三件套构建批量审查流水线 -->

把 Claude Code 的 `-p` 无头模式、Hooks 事件系统、Subagent 批量派活这三件套拧成一根绳，是规模化自动化最难也最值钱的一步。本教程不重复"单个钩子拦截一条命令"的入门玩法，而是聚焦 2026 年 Hooks 参考里真正改变批量任务形态的三个新机制：**PostToolBatch 聚合判定**、**agent-based hook（用模型做判断）**、**async hook（后台非阻塞）**。学完你将拥有一条可复现的批量代码审查流水线：用 `-p` 管道把成百上千个 diff 喂进去，靠聚合钩子在"一批并行工具调用落定、下一轮推理之前"这个唯一正确的时机把关，再用后台钩子跑测试，全程无人值守。适合已经会用 `claude -p` 跑单任务、想把审查从"一次一个"升级到"成批流水"的后端/平台/SRE 工程师。

## 前置条件

- **Claude Code ≥ v2.1.163**：本文的 `--bare` 行为、`stdin` 10MB 上限、后台任务宽限期都基于 v2.1.163 前后的约定，PostToolBatch/agent hook/async hook 是更近版本引入的，越新越全。
- **命令行与 JSON 熟练**：会读 `settings.json`、会用 `jq` 解析流式 JSON。
- **一个可审查的 Git 仓库**：能产出 `git diff`、有可跑的测试。
- **理解单个钩子**：建议先具备 PreToolUse（执行前拦截）与 PostToolUse（执行后处理）的基础认知，否则"为什么需要聚合"这一节的痛点会不明显。

## 一、为什么"单条护栏"管不住批量任务

早期 Hooks 教程几乎都停在两个事件上：

- `PreToolUse`：某条工具调用执行**之前**，用正则或脚本决定放行还是拦截（exit code 2 即阻断）。
- `PostToolUse`：某条工具调用执行**之后**，做格式化、日志、再校验。

这套"双钩护栏"对付"防止 `rm -rf`"这类单条高危命令非常有效。但一旦任务进入批量与并行，它就暴露三个盲区：

1. **并行工具调用各自独立**。Claude 在一轮推理里可以并行发出多条 `Bash`、`Edit`。每条单独看都合规：一条 `git add .`、一条 `git commit`、一条 `git push -f`。逐条过 PreToolUse 都不会拦——但拼起来就是一次覆盖远程的强推。单条钩子看不到"组合"。
2. **逐条触发太吵**。批量任务里工具调用密集，PostToolUse 每条都跑一遍脚本，要么阻塞主循环拖慢流水线，要么刷屏污染上下文。
3. **正则不够用**。"这批改动是否在语义上引入了密钥泄露风险"这种判断，写成正则既漏报又误报，需要的是模型而非模式匹配。

这三个盲区恰好对应三个新机制：组合盲区 → **PostToolBatch**（一批工具调用全部解析后、下一轮模型调用之前的唯一聚合点）；噪声盲区 → **async hook**（把不紧急的检查丢到后台）；语义盲区 → **agent-based hook**（让一个迷你 agent 做判断）。下面的实战就是围绕这三者展开。

## 二、第一件套：`-p` 模式如何"喂料"

无头模式的核心是 **stdin 即输入、stdout 即输出**，让它能像 `grep`、`sed` 一样塞进 Unix 管道。

### 2.1 管道喂料

最朴素的喂法是把内容 pipe 进去，结果重定向出来：

```bash
cat build-error.txt | claude -p 'concisely explain the root cause' > cause.txt
```

要点有三：

- **stdin 上限 10MB**（v2.1.128 起）。超过会非零退出并报错。大文件别 pipe，写盘后在 prompt 里引用路径。
- **管道即权限**。把 `git diff` 直接 pipe 进去，Claude 就不需要 `Bash` 权限去读 diff——这一点在锁权限的 CI 里极其重要。
- **结构化回收**。加 `--output-format json`，返回体里有 `result`（正文）、`session_id`，以及 `total_cost_usd` 和按模型的成本拆分。批量跑时靠它记账，不必再去后台查用量。

### 2.2 `--bare`：可复现的批量前提

裸跑批量的第一条铁律：**用 `--bare`**。

不加 `--bare`，`claude -p` 会加载交互式会话的全部上下文——你或同事 `~/.claude` 里的某个 hook、项目 `.mcp.json` 里的某个 MCP 服务器，都会被捎带执行。结果就是"我本机能跑、CI 上结果不一样"。

```bash
claude --bare -p "Summarize this file" --allowedTools "Read"
```

`--bare` 跳过 hooks、skills、plugins、MCP、auto memory、`CLAUDE.md` 的自动发现，只认你显式传的 flag。需要什么就用 flag 显式带进来：`--append-system-prompt-file`、`--settings`、`--mcp-config`、`--agents`、`--plugin-dir`。官方明确：`--bare` 是脚本与 SDK 调用的推荐模式，未来会成为 `-p` 的默认。批量流水线里它等价于"确定性保证"。

### 2.3 结构化输出：让结果可被程序消费

批量任务的产物必须是机器可解析的。两种姿势：

**纯 JSON 回收正文**：

```bash
claude -p "Summarize this project" --output-format json | jq -r '.result'
```

**强 schema 约束**：用 `--json-schema` 让输出落在 `structured_output` 字段，字段名和类型完全由你定义：

```bash
claude -p "Extract main function names from auth.py" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
  | jq '.structured_output'
```

### 2.4 批量循环：jsonl 是王道

把"一个 diff → 一次审查"封装成一次 `-p` 调用，循环遍历，每行一条结果写进 `.jsonl`，后续用 `jq` 聚合：

```bash
#!/usr/bin/env bash
set -euo pipefail

# 遍历当前分支相对 main 的每个已改文件，逐个喂给 -p
mapfile -t files < <(git diff --name-only main)

for f in "${files[@]}"; do
  [ -f "$f" ] || continue
  git diff main -- "$f" | claude --bare -p \
    --append-system-prompt "你是安全审查员，只报告真实风险，按 schema 输出" \
    --output-format json \
    --json-schema '{"type":"object","properties":{"risk":{"type":"string","enum":["high","medium","low","none"]},"issues":{"type":"array","items":{"type":"string"}}},"required":["risk","issues"]}' \
  | jq -c '{file: "'"$f"'", result: .structured_output, cost: .total_cost_usd}' \
  >> reviews.jsonl
done

# 汇总高危项
jq -c 'select(.result.risk=="high")' reviews.jsonl
```

这条循环本身已经是一条流水线。但它的审查全靠 prompt 里的"安全审查员"人设，**没有确定性护栏**。如果 `-p` 内部的 agent 决定顺手 `git commit` 或删个文件，循环就翻了车。这就是为什么要在喂料之外，再加第二、第三件套。

**常见问题**

| 现象 | 原因 | 解决 |
| --- | --- | --- |
| `claude -p` 卡住不退出 | 起了后台 dev server / watch 进程 | v2.1.163+ 后 5 秒宽限即终止；subagent/workflow 豁免但默认 10 分钟封顶，用 `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS` 调 |
| stdin 报错非零退出 | 输入超 10MB | 写盘后 prompt 里引用路径，别 pipe |
| CI 上结果与本地不一致 | 没用 `--bare`，捎带了本地配置 | 批量一律 `--bare` + 显式 flag |
| `structured_output` 为空 | 没同时给 `--output-format json` | schema 必须配 json 输出格式 |

## 三、第二件套：PostToolBatch——批量任务的唯一聚合点

### 3.1 它在生命周期里的位置

Hooks 事件按节奏分三档：

- **每会话一次**：`SessionStart` / `SessionEnd`
- **每轮一次**：`UserPromptSubmit` / `Stop` / `StopFailure`
- **代理循环内每次工具调用**：`PreToolUse` / `PostToolUse`

`PostToolBatch` 是第三档里的特殊存在：它在一**整批并行工具调用全部解析完毕、下一轮模型推理开始之前**触发。这是你能在"一批改动"这个粒度上做判断的唯一时机——既看得到这批每条调用的输入与结果，又能赶在模型继续推理前拦下整批。

### 3.2 配置形态

它**不支持 matcher**，永远每次都触发（与 `UserPromptSubmit`、`Stop`、`TaskCreated/Completed` 等同列）。配置直接挂在事件名下：

```jsonc
{
  "hooks": {
    "PostToolBatch": [
      {
        "hooks": [
          { "type": "command", "command": ".claude/hooks/guard-batch.sh" }
        ]
      }
    ]
  }
}
```

handler 类型有四类，恰好覆盖从"确定性"到"概率性"的频谱：`command`（shell）、`http`（POST 端点）、`prompt`（用 LLM 判条件）、`agent`（用迷你 agent 判）。

### 3.3 判定与阻断：exit code 2 + JSON decision

钩子通过两种方式回流决策：

- **exit code 2**：直接阻断，stderr 反馈给模型。
- **stdout 输出 JSON**：更精细的 decision control，例如 `{"decision":"block","reason":"..."}`、或为某些事件注入额外上下文。

### 3.4 实战脚本：抓"组合型"危险序列

下面这个钩子专门抓"单条合规、组合危险"的情形——一批里同时出现 `git commit` 与 `git push -f`：

```bash
#!/usr/bin/env bash
# .claude/hooks/guard-batch.sh
input="$(cat)"
# PostToolBatch 的 input 包含本批所有工具调用的输入与结果（字段以官方 reference 为准）
cmds=$(printf '%s' "$input" | jq -r '.. | .command? // empty' | tr '\n' '|')

has_commit=$(printf '%s' "$cmds" | grep -c 'git commit' || true)
has_force=$(printf '%s' "$cmds"  | grep -c 'push.*-f\|--force' || true)

if [ "$has_commit" -gt 0 ] && [ "$has_force" -gt 0 ]; then
  jq -nc '{decision:"block", reason:"同批出现 commit + push -f，疑似覆盖远程，已拦截整批"}' >&2
  exit 2
fi
```

放进 `.claude/settings.json` 后，无论 `-p` 还是交互模式，只要一轮里这两条命令同时出现，整批就被拦在下一轮推理之前。这是单条 PreToolUse 永远做不到的。

## 四、第三件套（上）：agent-based hook——把判断从正则交给模型

确定性钩子（exit code 2、正则）快、便宜、可复现，但只懂模式不懂语义。"这批改动是否在语义上像硬编码了凭据"，正则会漏掉 `"AKIA" 开头被切片拼接` 这种变体。

agent-based hook 让你用**一个迷你 Claude agent**来判这件事。它接收钩子输入，跑一次轻量推理，按 schema 返回判定。配置上挂在同一事件名下，`type` 取 agent 类（具体字段以官方 "agent hook configuration" 为准）。配合 PostToolBatch，等于在"聚合点"上叠加一个"概率判断层"：

```jsonc
{
  "hooks": {
    "PostToolBatch": [
      { "hooks": [
        { "type": "command", "command": ".claude/hooks/guard-batch.sh" }   // 确定性：组合序列
      ]},
      { "hooks": [
        { "type": "agent", /* model/prompt/schema 见官方字段 */ }          // 概率性：语义风险
      ]}
    ]
  }
}
```

分工原则：**确定能写成正则的，绝不上模型；只有语义判断才交给 agent**。这样既保住批量速度，又补上语义盲区。判定结果回流为 decision，命中则阻断，未命中则放行。

## 五、第三件套（下）：async hook——后台跑测试不阻塞

文件改动后想跑测试，又不想让 PostToolUse 每条都阻塞主循环——这就是 async hook 的场景。它把钩子丢到后台执行，主代理循环继续推进，结果异步回流（典型用于"跑测试/发通知/写日志"这类不必立即阻塞的事）。

> async hook 的输出不会像同步钩子那样即时回流进当前推理，因此**不要用它做必须当场阻断的检查**。需要阻断的（组合拦截、语义拦截）仍走同步 PostToolBatch + exit code 2。async 适合"事后核验"：测试挂了就再触发一轮修复，而不是卡住当前批。

限制也要记牢：后台任务在 `-p` 退出时有 5 秒宽限；subagent/workflow 豁免该宽限但默认 10 分钟封顶。所以 async 测试要么自包含、要么走子 agent。

## 六、实战：把三件套拼成一条流水线

目标：对当前分支相对 `main` 的全部改动，自动跑一遍"确定性护栏 + 语义护栏 + 后台测试"。

**目录结构**

```
.claude/
├── settings.json            # 注册 PostToolBatch 钩子（command + agent）
├── hooks/
│   └── guard-batch.sh       # 确定性：组合序列拦截
└── prompts/
    └── secret-review.md     # 语义：硬编码凭据审查（供 agent hook 用）
```

**`.claude/settings.json`**

```jsonc
{
  "hooks": {
    "PostToolBatch": [
      { "hooks": [
        { "type": "command", "command": ".claude/hooks/guard-batch.sh" }
      ]},
      { "hooks": [
        {
          "type": "agent",
          "prompt": "审查本批所有 Bash/Edit 改动是否引入硬编码凭据（API key、token、私钥）。仅当确信时返回 block。",
          "model": "haiku"
        }
      ]}
    ],
    "FileChanged": [
      { "matcher": "*.py", "hooks": [
        { "type": "command", "command": "pytest -q &", "_note": "async：后台跑测试，不阻塞" }
      ]}
    ]
  }
}
```

**入口脚本** `batch-review.sh`

```bash
#!/usr/bin/env bash
set -euo pipefail
: > reviews.jsonl

mapfile -t files < <(git diff --name-only main)
for f in "${files[@]}"; do
  [ -f "$f" ] || continue
  # --bare 保证 CI 可复现；--permission-mode 锁住可写范围
  git diff main -- "$f" | claude --bare -p \
    --append-system-prompt-file .claude/prompts/secret-review.md \
    --permission-mode acceptEdits \
    --allowedTools "Read,Edit,Bash(git diff *),Bash(git add *),Bash(git commit *)" \
    --output-format json \
    --json-schema '{"type":"object","properties":{"risk":{"type":"string"},"issues":{"type":"array","items":{"type":"string"}}},"required":["risk","issues"]}' \
    | jq -c '{file:"'"$f"'", r:.structured_output, cost:.total_cost_usd}' \
    >> reviews.jsonl
done

echo "== 高危 ==";  jq -c 'select(.r.risk=="high")' reviews.jsonl
echo "== 总成本 =="; jq '[.cost] | add' reviews.jsonl
```

**运行效果**：每个文件一次 `-p`；PostToolBatch 的 command 钩子拦组合危险序列、agent 钩子判语义密钥风险、FileChanged 的 async 钩子在后台对改动过的 `.py` 跑 `pytest`。最终 `reviews.jsonl` 一行一文件，可被 `jq` 聚合成高危清单与总成本。整条链路无人值守、可复现、有护栏、可记账。

### 流水线全景

```mermaid
flowchart TD
    A[git diff main 文件清单] --> B{遍历每个文件}
    B --> C[git diff 单文件 管道喂料]
    C --> D["claude --bare -p\n--permission-mode acceptEdits\n--json-schema 结构化输出"]
    D --> E[agent 循环]
    E -->|每条工具调用| F[PreToolUse/PostToolUse 单条护栏]
    F --> G{"一批并行调用落定\nPostToolBatch 触发"}
    G -->|command 钩子| H[组合序列拦截 commit+push -f]
    G -->|agent 钩子| I[语义判断 硬编码凭据]
    H -->|命中| J[exit 2 阻断整批]
    I -->|命中| J
    H -->|放行| K[下一轮模型推理]
    I -->|放行| K
    E -->|文件落盘| L[FileChanged async 钩子]
    L -.后台非阻塞.-> M[pytest 测试]
    K --> N[Stop 输出结构化 JSON]
    N --> O[jq 聚合进 reviews.jsonl]
    O --> B
```

## 七、踩坑清单

- **钩子不触发**：先 `/hooks` 看 PostToolBatch 计数是否 > 0；确认没有写 `matcher`（写了会被静默忽略，但不会报错，最容易骗人）。
- **JSON 校验失败**：decision 必须是合法 JSON 且字段名匹配；非法 JSON 会被丢弃，钩子形同虚设。调试时把脚本输出重定向到文件逐行看。
- **agent hook 太慢/太贵**：把模型降到 `haiku`，且只在 PostToolBatch（每批一次）触发，不要挂在 PostToolUse（每条一次）。
- **async hook"没生效"**：它的输出不即时回流，所以"测试挂了却没阻断当前批"是正常的——它靠下一轮或下次运行补救。需要当场阻断的别用 async。
- **批量越跑越慢**：逐文件 `-p` 适合几十个文件；上百个时改成按目录分批，或用 Subagent 并行派活（每个 subagent 独立上下文窗口，不互相污染），主会话只做汇总。
- **CI 上被本地配置污染**：忘加 `--bare` 是头号元凶。养成"批量即 bare"的肌肉记忆。

## 训练营适用性评估

- **平台工程 / DevOps 训练营**：高度契合。PostToolBatch 组合拦截 + agent 语义审查 + async 测试，几乎就是一条本地化 CI 的雏形。建议教学时长 **2.5 小时**：1 小时讲三件套机制与生命周期定位、1 小时动手拼流水线、0.5 小时踩坑与调优。
- **安全审计训练营**：作为"AI 辅助代码审查"模块切入点，agent hook 判密钥泄露很有代入感。
- **零基础入门营**：**不推荐直接用**。需先掌握 `-p` 单任务、PreToolUse/PostToolUse 基础，否则"聚合点"的动机讲不透。可作为进阶周课程。
- **企业内训**：组合序列拦截（`commit`+`push -f`）是最能打动工程负责人的演示点，5 分钟 demo 即可建立价值认知。

## 参考来源

1. [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks) — 官方钩子参考，含全部事件（PostToolBatch、SubagentStop、FileChanged 等）的 input schema、decision control 与 agent/prompt/async/HTTP hook 配置。
2. [Automate actions with hooks — Claude Code Docs](https://code.claude.com/docs/en/hooks-guide) — 官方实战指南，第一个钩子、matcher/if 过滤、组合多钩子结果的写法。
3. [Run Claude Code programmatically（headless / -p）— Claude Code Docs](https://code.claude.com/docs/en/headless) — 官方 `-p` 与 `--bare` 模式、管道喂料、`--json-schema` 结构化输出、stdin 上限与后台任务宽限期。
4. [Claude Code Hooks: 13 Lifecycle Events, Examples, and Automation (2026) — knightli](https://knightli.com/en/2026/05/01/claude-code-hooks-mastery-guide/) — 钩子生命周期分档、配置落点、单文件脚本与"钩子别太重"的工程纪律。
5. [Create custom subagents — Claude Code Docs](https://code.claude.com/docs/en/sub-agents) — 自定义子代理 frontmatter 与 scope，供上百文件并行派活时参考。

---

更多 Claude Code 实战内容，欢迎关注公众号「51学通信」，联系站长爱卫生（微信：gprshome201101）交流。

> [!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)
