首页 › tutorials › 三件套构建批量审查流水线

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

前置条件

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

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

这套"双钩护栏"对付"防止 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 进去,结果重定向出来:

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

要点有三:

2.2 --bare:可复现的批量前提

裸跑批量的第一条铁律:用 --bare。

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

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 回收正文:

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

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

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 聚合:

#!/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 事件按节奏分三档:

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

3.2 配置形态

它不支持 matcher,永远每次都触发(与 UserPromptSubmit、Stop、TaskCreated/Completed 等同列)。配置直接挂在事件名下:

{
  "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

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

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

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

#!/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,等于在"聚合点"上叠加一个"概率判断层":

{
  "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

{
  "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

#!/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 聚合成高危清单与总成本。整条链路无人值守、可复现、有护栏、可记账。

流水线全景

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

七、踩坑清单

训练营适用性评估

参考来源

  1. Hooks reference — Claude Code Docs — 官方钩子参考,含全部事件(PostToolBatch、SubagentStop、FileChanged 等)的 input schema、decision control 与 agent/prompt/async/HTTP hook 配置。
  2. Automate actions with hooks — Claude Code Docs — 官方实战指南,第一个钩子、matcher/if 过滤、组合多钩子结果的写法。
  3. Run Claude Code programmatically(headless / -p)— Claude Code Docs — 官方 -p 与 --bare 模式、管道喂料、--json-schema 结构化输出、stdin 上限与后台任务宽限期。
  4. Claude Code Hooks: 13 Lifecycle Events, Examples, and Automation (2026) — knightli — 钩子生命周期分档、配置落点、单文件脚本与"钩子别太重"的工程纪律。
  5. Create custom subagents — Claude Code Docs — 自定义子代理 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学通信知识星球海报-扫码加入