<!-- title: 用-p模式构建CI代码审查闭环 -->
# 用 -p 模式构建 CI 代码审查闭环

## 概述

本教程聚焦 Claude Code 的 **Headless（无人值守）自动化能力**，目标是用 `-p`（`--print`）模式把 Claude Code 织进 GitHub Actions，搭建一条「PR 触发 → 拉取 diff → Claude 评审 → 回写评论 → 质量门禁」的端到端代码审查闭环。适合已经能在终端里用 Claude Code 完成单次任务、想把它升级为 CI 常驻评审员的工程师与 DevOps 同学。学完你将掌握：headless 三种输出格式的取舍、stream-json 的逐事件解析、Hooks 作为质量门禁的配置方法，以及一个可直接复制进仓库的完整 workflow。

## 前置条件

- 已安装 Claude Code（`claude --version` 可输出版本号），完成至少一次交互式会话
- 一个 GitHub 仓库的写权限，能配置 Actions secrets
- 基本的 YAML 与 shell 脚本阅读能力
- `jq` 已在本地可用（CI 的 `ubuntu-latest` 镜像默认自带）
- 一个可用的 `ANTHROPIC_API_KEY`（或 Claude 订阅账号对应的 OAuth token）

---

## 第一步：理解 Headless 模式的三个关键参数

交互式 Claude Code 会启动一个 TUI，等待你逐轮输入。而 headless 模式把这一切变成「一次 prompt 进、一次结果出」的命令，天然适配脚本与 CI。最小用法：

```bash
claude -p "用一句话总结当前目录的 README"
```

在 CI 里，你会反复用到三个参数的组合，先记住这张对照表：

| 参数 | 作用 | 典型取值 |
|------|------|----------|
| `-p` / `--print` | 进入 headless，不启动 TUI | 无值，开关型 |
| `--output-format` | 控制 stdout 输出形态 | `text` / `json` / `stream-json` |
| `--input-format` | 控制 stdin 输入形态 | `text` / `stream-json` |
| `--allowedTools` | 限定可用工具白名单 | `Read,Bash(git:*)` |
| `--max-turns` | 限制 agent 最大循环轮数 | `1`~`N`，防失控 |
| `--add-dir` | 授予额外目录访问权 | 绝对路径 |

**预期结果**：执行后终端直接打印模型回复，进程结束码为 0。如果出现 `interactive mode requires a TTY` 之类的报错，多半是少了 `-p`。

**常见问题**：CI 里直接 `claude -p` 偶尔会因缺少 TTY 而 hang。稳妥写法是同时显式声明输入输出格式，避免它去探测终端：

```bash
claude -p --output-format text --input-format text "你的 prompt"
```

---

## 第二步：三种输出格式，何时用哪个

headless 的灵魂在于「输出可被程序消费」。三种格式各有定位：

**`text`（默认）**——纯文本回复，适合人类阅读或 `>>` 追加进文件、PR 描述。最简单，但无法区分「模型在思考」还是「模型在调用工具」。

**`json`**——把整次会话压成**一个** JSON 对象返回，包含 `result`（最终文本）、`cost_usd`（花费）、`duration_ms`（耗时）、`is_error` 等字段。适合「跑完一次拿总结」的场景：

```bash
claude -p --output-format json "列出 src 下所有 TODO" \
  | jq '{todo: .result, cost: .cost_usd}'
```

**`stream-json`**——按**事件流**逐行（NDJSON）输出，每个事件一行 JSON。这是 CI 自动化的首选，因为你能在「工具调用发生时」就介入，而不必等整轮结束。典型事件包括 `system`（初始化）、`assistant`（模型消息）、`tool_use`（发起工具调用）、`tool_result`（工具返回）、`result`（终态汇总）。

一个最小的流式消费脚本，用 `jq` 边读边过滤：

```bash
claude -p --output-format stream-json "评审 git diff HEAD~1 的改动" \
  | jq -c 'select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text'
```

**常见问题**：`stream-json` 模式下若同时给了 `--input-format stream-json` 却没按协议逐行喂输入，进程会卡住等待（对应社区 issue #53584 的典型复现）。CI 里**只设 `--output-format stream-json`、`--input-format` 保持 `text`**，是最稳的组合。

---

## 第三步：用 Hooks 给 Headless 加质量门禁

headless 模式下没有人盯着「确认执行」按钮，所以**护栏必须前置到配置里**。Hooks 就是这套护栏——在特定事件点自动执行的 shell 命令，配置在 `.claude/settings.json`。

最常用的是两个事件：

- **`PreToolUse`**：工具调用**前**触发，可拦截危险操作
- **`PostToolUse`**：工具调用**后**触发，可跑 lint / 测试 / 格式化

下面这段配置做两件事：① 禁止 headless 模式下 `git push --force`；② 每次 `Edit`/`Write` 之后自动跑 `eslint --fix`。

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' | grep -q 'push --force' && { echo '禁止 force push' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "npx eslint --fix $(jq -r '.tool_input.file_path')" }
        ]
      }
    ]
  }
}
```

**关键约定**：Hook 通过 **stdin 收到一段 JSON**（含 `session_id`、`tool_name`、`tool_input`）；**退出码 0** 放行，**退出码 2** 阻断并把 stderr 反馈给模型让其改道，其他非零码视为错误。CI 里务必用 `jq` 解析 stdin，别用 `grep` 硬切。

**预期结果**：当 headless Claude 试图执行 `git push --force` 时，命令被拦下，模型收到反馈后会改用普通 `git push`。

**常见问题**：PostToolUse 里直接 `npx eslint` 每次都冷启动很慢。优化方案是把它包成一个脚本，先 `test -f node_modules/.bin/eslint` 走本地、否则才 fallback 到 npx，CI 里通常已 `npm ci` 装好依赖。

---

## 第四步：把闭环装进 GitHub Actions

现在把前三步拼成一条 workflow。设计目标：每个 PR 打开或更新时，Claude 自动评审 diff，并以评论形式贴回 PR。

先在仓库 **Settings → Secrets and variables → Actions** 添加 `ANTHROPIC_API_KEY`。然后新建 `.github/workflows/claude-review.yml`：

```yaml
name: Claude Code Review

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: write   # 回写评论必须

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # 需要完整历史才能取 diff

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Fetch PR diff
        id: diff
        run: |
          git diff origin/${{ github.base_ref }}...HEAD > pr.diff
          echo "size=$(wc -c < pr.diff)" >> $GITHUB_OUTPUT

      - name: Run Claude review
        if: steps.diff.outputs.size != '0'
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          claude -p \
            --output-format json \
            --max-turns 3 \
            --allowedTools "Read" \
            "你是资深代码评审。阅读 pr.diff，针对每个文件给出：1) 风险等级(高/中/低) 2) 具体问题 3) 修改建议。用 Markdown 输出，不要客套话。" \
            | jq -r '.result' > review.md

      - name: Post review comment
        if: steps.diff.outputs.size != '0'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const body = '## 🤖 Claude Code 评审\n\n' + fs.readFileSync('review.md', 'utf8');
            await github.rest.issues.createComment({
              ...context.repo,
              issue_number: context.issue.number,
              body
            });
```

**预期结果**：PR 创建后约 30~60 秒，会自动出现一条带「风险等级/问题/建议」的评论。

**常见问题与对策**：

1. **评论过长被截断**——GitHub 单条评论上限约 65536 字符。在 `jq` 后加 `head -c 60000` 截断，或按文件拆成多条评论。
2. **大 diff 烧 token**——加 `if: steps.diff.outputs.size < 50000` 的前置判断，超大 PR 改为只评审变更过的关键文件。
3. **评审重复刷屏**——用 `github.rest.issues.listComments` 先查是否已有 bot 评论，有则 `updateComment` 而非新建。

---

## 第五步：批量任务编排——从单 PR 到多仓库

单条 workflow 跑通后，下一步是**批量**。两种典型形态：

**形态 A：一次评审多个文件**。把文件列表喂给一个 headless 进程，让它串行处理，输出用 `stream-json` 区分每个文件的结果：

```bash
printf '%s\n' src/a.ts src/b.ts src/c.ts | while read f; do
  claude -p --output-format json --allowedTools Read \
    "只评审 $f 的安全性问题" \
    | jq --arg f "$f" '{file: $f, result: .result}'
done > batch-review.jsonl
```

**形态 B：多仓库定时巡检**。用 `workflow_dispatch` + `schedule` 触发，遍历一组仓库清单。关键是用 `--max-turns` 兜底，防止某个仓库让 agent 陷入死循环：

```yaml
on:
  schedule:
    - cron: '17 2 * * 1'   # 每周一凌晨（避开整点高峰）
  workflow_dispatch:
```

**批量通用护栏**：

- 每条任务设 `--max-turns 8`，超时即停
- 用 `timeout 300 claude -p ...` 包一层硬超时
- 失败不中断：`set +e` + 逐条记录退出码到 `report.csv`
- 汇总阶段统一算账：`jq -s 'map(.cost_usd) | add'` 得出总花费

---

## 实战案例：可一键复现的「风险分级评审机器人」

把上面的零件组装成一个最小但完整的项目。目录结构：

```
.ai-reviewer/
├── settings.json          # Hooks 护栏
├── prompt.md              # 评审提示词
└── run.sh                 # 本地预演脚本
.github/workflows/
└── claude-review.yml      # CI 入口
```

**`.ai-reviewer/prompt.md`**（评审提示词，单文件维护便于迭代）：

```markdown
你是一名注重工程实效的代码评审。对给定 diff 按下列规则评审：

## 评审范围
- 只看本次变更（diff），不评审未改动代码
- 优先级：安全 > 正确性 > 可读性 > 风格

## 输出格式（严格遵守）
对每个文件输出一个区块：
### `文件路径`
- **风险**：高 | 中 | 低
- **问题**：（编号列表，每条 ≤ 2 行）
- **建议**：（对应修复，给最小可行代码片段）

## 边界
- 不输出"整体看起来不错"等无信息量的话
- 风格类问题合并为一条，不逐条罗列
```

**`.ai-reviewer/run.sh`**（本地预演，CI 前 dry-run）：

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

DIFF_FILE="${1:-pr.diff}"
PROMPT="$(cat .ai-reviewer/prompt.md)"

# 本地预演：用 HEAD~1 的 diff 当样本
[ -f "$DIFF_FILE" ] || git diff HEAD~1 > "$DIFF_FILE"

claude -p \
  --output-format json \
  --max-turns 4 \
  --allowedTools "Read,Bash(cat:*)" \
  --setting .ai-reviewer/settings.json \
  "$(printf '%s\n\n待评审 diff：\n```diff\n%s\n```' "$PROMPT" "$(cat "$DIFF_FILE")")" \
  | jq -r '.result'
```

**预期结果**：本地执行 `bash .ai-reviewer/run.sh` 即可在终端看到结构化的风险分级评审，无需推送。确认效果满意后，CI 直接复用同一段 prompt——这是「本地与 CI 同源」的关键，避免两边提示词漂移。

---

## 工作流程图

```mermaid
flowchart TD
    A[PR 打开/更新] --> B[Actions 触发]
    B --> C[checkout + 取 diff]
    C --> D{diff 为空?}
    D -- 是 --> Z[跳过评审]
    D -- 否 --> E[headless Claude -p]
    E --> F{PreToolUse Hook}
    F -- 命中护栏 --> G[阻断+反馈]
    F -- 放行 --> H[模型调用 Read 等工具]
    H --> I[PostToolUse: eslint --fix]
    I --> J[生成评审结果]
    J --> K[output-format json 解析]
    K --> L[jq 提取 .result]
    L --> M[回写 PR 评论]
    G --> E
    M --> N[人工 review + merge]
```

---

## 关键设计决策与权衡

**为什么 CI 里优先 `json` 而非 `stream-json`？** 评审是一次性总结任务，不需要在工具调用中途介入；`json` 拿到完整对象后一次 `jq` 取 `.result` 最干净。`stream-json` 留给需要「边跑边消费」的管道，比如把工具调用实时转成进度日志。

**为什么用 `--allowedTools` 而不是全开？** CI 环境里 Claude 拥有的能力越窄越安全。评审场景只需 `Read`（顶多加 `Bash(cat:*)`），不给 `Edit`/`Write` 就从物理上杜绝了「评审机器人顺手改了线上代码」的风险。

**为什么 Hooks 配在仓库内而不是用户级？** 仓库级 `.claude/settings.json` 会随代码版本化，团队所有人——包括 CI——共享同一套护栏。用户级配置只在你的本机生效，CI 里读不到。

**成本控制三件套**：① `--max-turns` 限轮数；② diff 体积预判跳过超大 PR；③ `json` 输出里的 `cost_usd` 字段做账，超预算阈值时在评论里标注当次花费。

---

## 延伸阅读

- [Claude Code Headless Mode 官方文档](https://code.claude.com/docs/en/headless) —— Agent SDK 在 CLI/Python/TypeScript 三种语言下的编程式调用
- [Hooks 参考手册](https://code.claude.com/docs/en/hooks) —— 全部事件类型、JSON 输入输出协议、退出码语义
- [Output Styles](https://code.claude.com/docs/en/output-styles) —— 用输出样式固定角色与格式，比每次在 prompt 里重复要求更省 token
- [Headless stream-json hang issue #53584](https://github.com/anthropics/claude-code/issues/53584) —— 官方 issue，记录 stream-json 配 input 时的卡死复现与规避

---

## 训练营适用性评估

- **适合类型**：中高级工程师训练营、DevOps/SRE 自动化专题、企业内部「AI 辅助研发」落地工作坊
- **前置衔接**：建议放在「Skills 开发」「MCP 服务器」之后，作为「把 Claude 接入真实交付流水线」的收口环节
- **预计教学时长**：理论讲授 45 分钟 + 动手实操 90 分钟（含本地预演与 CI 接入）+ 排坑答疑 30 分钟，合计约 2.5~3 课时
- **评估产出**：学员各自仓库出现一条真实可用的 bot 评审评论，且能口述三个护栏的设计理由，即视为通过

## 参考来源

1. [Claude Code Headless Mode: Non-Interactive & CI Usage](https://claude-code-examples.vercel.app/claude-code-headless-mode/) —— `--print` 标志、管道输入、CI/CD 自动化与批处理的实践指南
2. [Run Claude Code programmatically（官方）](https://code.claude.com/docs/en/headless) —— Agent SDK 在脚本与 CI/CD 中的编程式调用权威说明
3. [Hooks reference（官方）](https://code.claude.com/docs/en/hooks) —— Hook 事件、配置 schema、JSON 输入输出与退出码的完整参考
4. [Headless --print --output-format=stream-json Issue #53584](https://github.com/anthropics/claude-code/issues/53584) —— 社区对 stream-json 卡死问题的复现与根因讨论
5. [Building Headless Automation with Claude Code](https://lilys.ai/en/notes/claude-code-20251028/building-headless-automation-claude-code) —— 基于 SDK 与官方 GitHub Action 搭建 headless 自动化的实战笔记

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