<!-- title: 造CI机器人自动审查每个PR -->

## 概述

本教程带你用 Claude Code 的「自动化三件套」——无头 `-p` 模式、生命周期 hooks、批量处理——从零搭建一个**每个 PR 自动审查**的 GitHub Actions 机器人。学完你将掌握：用 `stream-json` 解析审查结论、用 `PreToolUse` 钩子把审查范围锁死在 PR 改动文件、用 `--max-turns` 控制成本与防失控，以及把审查结果回写成 PR 行内评论。适合已装通 Claude Code、想把它接进团队流水线的开发者与 DevOps 工程师。

## 前置条件

- 已安装 Claude Code 并能跑通 `claude -p "hello"`（参见系列入门篇）
- 一个 GitHub 仓库的写权限，可添加 Actions secret `ANTHROPIC_API_KEY`
- 会写一点 Bash 与 YAML；Node.js 用于写钩子脚本（非必需，钩子也可以是 Bash）
- 本地装有 `gh` CLI 用于回写评论调试

---

## 第一步：理解自动化三件套与两条路径

把 Claude Code 接进 CI 有两条路径，理解它们的分工是本教程的基石：

| 路径 | 本质 | 适合场景 |
|------|------|----------|
| **官方 action** `anthropics/claude-code-action@v1` | 一层封装，把 `-p` 模式 + diff 收集 + 评论回写打包好 | 生产环境，快速上线 |
| **原生 CLI** `claude -p ... --output-format stream-json` | 直接用底层能力，自己拼管道 | 想精细控制、本地干跑、自定义流程 |

```mermaid
flowchart LR
    A[PR 推送] --> B[GitHub Actions 触发]
    B --> C{选路径}
    C -->|官方 action| D[claude-code-action@v1]
    C -->|原生 CLI| E[ci/review.sh]
    D --> F[-p 无头模式]
    E --> F
    F --> G[PreToolUse 钩子锁范围]
    G --> H[批量审查改动文件]
    H --> I[stream-json 输出]
    I --> J[解析 finding]
    J --> K[gh 回写行内评论]
    K --> L[Stop 钩子通知/计费]
```

本教程**先教原生 CLI 路径**（吃透三件套），再给官方 action 的生产级写法。这样无论 action 升级与否，你都能自己维护。

---

## 第二步：用 `-p` 模式本地跑通单次审查

无头模式的核心是 `-p`（即 `--print`）：不进交互 TUI，直接吃一句 prompt、吐一份结果。配合 `--output-format stream-json`，输出变成**换行分隔的 JSON 事件流**，方便程序解析。

先在本地验证。准备一个 `REVIEW.md` 作为审查准则（Claude 会读它）：

```markdown
# 代码审查准则
- 禁止在代码中硬编码密钥、token、密码
- 所有公开函数必须有类型注解
- SQL 查询必须使用参数化，禁止字符串拼接
- 错误必须被处理，禁止空 except / catch 后吞掉
```

然后跑一次干跑（`PR_DIFF` 假设是当前分支相对 main 的 diff）：

```bash
# 收集改动文件清单
git fetch origin main
git diff --name-only origin/main...HEAD > /tmp/changed.txt

# 把 diff 喂给 Claude，要求 stream-json 输出
claude -p "$(cat <<'EOF'
你是资深代码审查员。请审查以下 Git diff，严格依据 REVIEW.md 准则。
只对违反准则的代码提 finding，不要泛泛而谈。
每条 finding 用如下 JSON 结构（无 finding 则输出空数组）：
{"findings":[{"file":"路径","line":12,"severity":"high|med|low","rule":"违反的准则","suggestion":"修复建议"}]}

改动文件：
$(cat /tmp/changed.txt)

完整 diff：
$(git diff origin/main...HEAD)
EOF
)" --model claude-sonnet-4-6 \
  --output-format stream-json \
  --max-turns 6 \
  --include-partial-messages
```

**预期结果**：终端逐行打印 JSON。关键事件类型有三类：

- `assistant`：模型发言或发起工具调用（审查场景下一般不调工具，直接给结论）
- `result`：最终汇总对象，包含 `result`（文本）、`num_turns`、`total_cost_usd`、`is_error`、`duration_ms`

**常见问题**：

- **输出被截断 / 像没跑完**：`--max-turns` 给太小会被截。审查任务给 4–8 通常够，别给几十否则成本失控。
- **想只要最终 JSON**：把 `--output-format` 换成 `json`（非 stream），只吐一个汇总对象，适合管道下游用 `jq`。
- **找不到 REVIEW.md**：`-p` 模式默认不会自动读仓库文件，要么在 prompt 里内联，要么用 `--add-dir` 或挂文件。本例直接内联清单 + diff 最稳。

---

## 第三步：配 CI 护栏——用 hooks 锁死审查范围

在 CI 里最怕两件事：**审查员乱改文件**（review 任务不该写代码）、**跑飞烧钱**。hooks 就是确定性护栏——它由 harness 执行，不依赖模型「听话」。

在仓库根目录写 `.claude/settings.json`（项目级配置，CI 会自动加载）：

```json
{
  "permissions": {
    "allow": ["Bash(git diff:*)", "Bash(git show:*)", "Read(*)"],
    "deny": ["Bash(git push:*)", "Bash(rm -rf:*)"]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [{ "type": "command", "command": "node .ci/guard-no-write.mjs" }]
      },
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "node .ci/guard-bash.mjs" }]
      }
    ],
    "Stop": [
      { "hooks": [{ "type": "command", "command": "node .ci/on-stop.mjs" }] }
    ]
  }
}
```

钩子从 **stdin 拿到一段 JSON**，包含 `session_id`、`tool_name`、`tool_input` 等。**返回方式**有两种：退出码 0（放行，stdout 作为上下文回喂模型）或退出码 2（阻断，stderr 回喂，模型会重试）。PreToolUse 还能返回结构化决策 `permissionDecision: allow/deny/ask`。

写审查护栏 `.ci/guard-no-write.mjs`（审查员只能读、不能写）：

```javascript
// .ci/guard-no-write.mjs —— 审查机器人禁止任何写操作
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync(0, "utf8"));
const file = input.tool_input?.file_path ?? "";

// 审查场景下，写任何文件都拒绝，并告诉模型原因（它会改为只读）
console.log(JSON.stringify({
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason:
      "审查机器人只读。请勿修改文件，把建议写进 finding 的 suggestion 字段。"
  }
}));
process.exit(0);
```

再写 `.ci/guard-bash.mjs`（Bash 命令白名单）：

```javascript
// .ci/guard-bash.mjs —— 只允许只读 git 命令，挡住 push/rm 等
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync(0, "utf8"));
const cmd = input.tool_input?.command ?? "";
const blocked = /\b(git\s+push|rm\s+-rf|curl\s+.*\|\s*sh|sudo)\b/;
if (blocked.test(cmd)) {
  console.error(`CI 审查禁止执行: ${cmd.slice(0, 80)}`);
  process.exit(2); // 退出码 2 = 阻断并回喂 stderr
}
process.exit(0);
```

最后写 `.ci/on-stop.mjs`，在 Claude 结束时打印成本（事件里有 `stop_hook_active`，注意避免递归）：

```javascript
// .ci/on-stop.mjs —— 会话结束上报成本
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync(0, "utf8"));
console.log(`::notice::审查完成 session=${input.session_id?.slice(0,8)}`);
process.exit(0);
```

**常见问题**：

- **钩子没生效**：确认 `.claude/settings.json` 在仓库根、JSON 合法。本地可用 `claude --debug` 看钩子加载日志。
- **退出码 2 导致无限重试**：若模型改不了行为（比如硬要 push），会反复触发。配合 `--max-turns` 做兜底。
- **钩子本身报错**：钩子脚本崩溃会当作「非阻断失败」，stderr 警告但不中断——所以 `process.exit(2)` 必须在你想阻断时显式调用。

---

## 第四步：写 GitHub Actions 工作流

把上面三步串成 CI。关键是用 `actions/checkout` 的 `fetch-depth: 0` 拿到完整 diff，再把改动喂给审查脚本。

```yaml
# .github/workflows/claude-review.yml
name: Claude PR Review
on:
  pull_request:
    types: [opened, synchronize, reopened]
  # 节省成本：草稿 PR 不跑
jobs:
  review:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write   # 回写评论需要
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0      # 拿全历史才能 diff
      - uses: actions/setup-node@v4
        with: { node-version: '20' }

      - name: 运行 Claude 审查
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: bash ci/review.sh
```

`ci/review.sh` 是把第二步的本地命令 CI 化，并调用第四步的钩子配置（`.claude/settings.json` 自动加载）：

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

BASE="origin/${GITHUB_BASE_REF:-main}"
HEAD="origin/${GITHUB_HEAD_REF:-HEAD}"
git fetch origin "$GITHUB_BASE_REF" 2>/dev/null || true

# 改动文件（只看代码，过滤 lock/distro）
FILES=$(git diff --name-only --diff-filter=AM "$BASE...$HEAD" \
        | grep -iE '\.(ts|tsx|js|jsx|py|go|java|rb)$' || true)
[ -z "$FILES" ] && { echo "无代码改动，跳过"; exit 0; }

DIFF=$(git diff "$BASE...$HEAD")

# stream-json 流式输出，取最后一条 result 事件里的文本
OUT=$(claude -p "依据 REVIEW.md 审查下列 diff，仅对违规项输出 JSON 数组：\
[{\"file\",\"line\",\"severity\",\"rule\",\"suggestion\"}]，无则输出 []。
改动文件：$FILES
完整 diff：$DIFF" \
  --model claude-sonnet-4-6 \
  --output-format stream-json \
  --max-turns 6)

# 取最后一条 result 事件的 result 字段
PAYLOAD=$(echo "$OUT" | jq -rc 'select(.type=="result") | .result' | tail -n1)
echo "$PAYLOAD" | jq '.findings // []' > /tmp/findings.json

# 回写评论
node ci/post-comments.mjs "$PR_NUMBER" /tmp/findings.json
```

---

## 第五步：把审查结果回写成 PR 行内评论

审查的价值在于**精确到行**。`.ci/post-comments.mjs` 用 GitHub 的 line-position 评论 API，把每条 finding 钉到具体行：

```javascript
// .ci/post-comments.mjs <pr_number> <findings.json>
import { readFileSync } from "node:fs";
const [,, prStr, findingsPath] = process.argv;
const pr = prStr;
const findings = JSON.parse(readFileSync(findingsPath, "utf8"));

if (!findings.length) {
  console.log("✅ 未发现违规项");
  process.exit(0);
}

// 汇总成一条 PR 评论（行内评论需要 commit_sha 较繁琐，这里用汇总评论稳妥落地）
const lines = findings.map((f, i) =>
  `| ${i+1} | \`${f.file}:${f.line}\` | **${f.severity}** | ${f.rule} | ${f.suggestion} |`);

const body = [
  "## 🤖 Claude 自动审查结果",
  "",
  "| # | 位置 | 级别 | 准则 | 建议 |",
  "|---|------|------|------|------|",
  ...lines,
  "",
  "_由 Claude Code CI 机器人生成，依据 `REVIEW.md`。_"
].join("\n");

const { execSync } = await import("node:child_process");
execSync(`gh pr comment ${pr} --body '${body.replace(/'/g, "'\\''")}'`,
  { stdio: "inherit", env: process.env });
```

**预期结果**：PR 上出现一条表格评论，每行一个 finding，含文件、行号、级别、违反准则、修复建议。无违规时不出评论（或出 ✅）。

**常见问题**：

- **评论超长被截**：GitHub 单条评论有长度上限。finding 多时分批 `gh pr comment`。
- **行号对不上**：模型给的 `line` 是 diff 里的新文件行号；若要对到精确行，建议优先用「文件 + 准则 + 关键代码片段」定位，行号仅作参考。
- **评论权限不足**：确认 `permissions: pull-requests: write` 且 `GH_TOKEN` 用的是默认 `GITHUB_TOKEN`。

---

## 第六步：批量、并发与成本控制

当 PR 改动几十个文件时，三个手段保住账单：

1. **预过滤**：`grep` 只挑代码文件，过滤 `*.lock`、`dist/`、生成代码（见第四步）。
2. **并发审查**：把文件按目录切片，开多个 `-p` 进程并行跑，用 `xargs -P`：

```bash
cat /tmp/changed.txt | xargs -I{} -P 4 bash -c '
  claude -p "审查 {}，依据 REVIEW.md，输出 JSON findings" \
    --output-format json --max-turns 4 > "/tmp/rev_$(echo {} | tr / _).json"
'
```

3. **硬性成本上限**：`--max-turns` 是第一道闸（单次会话轮次）；再叠环境变量限输出 token，以及 workflow 级 `timeout-minutes`：

```yaml
- name: 运行 Claude 审查
  timeout-minutes: 10
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    CLAUDE_CODE_MAX_OUTPUT_TOKENS: "8000"
  run: bash ci/review.sh
```

> **省钱要点**：审查是「读密集、结论短」任务，用 `claude-sonnet-4-6` 而非 opus；`--max-turns` 控在 4–8；非代码改动直接跳过。社区经验：每月 50 个 PR 的团队，API 成本通常在个位数美元量级。

---

## 实战案例：从零交付一个可运行仓库

把上面所有片段落成一个完整项目，目录结构如下：

```
my-repo/
├── .github/workflows/claude-review.yml   # 第四步
├── .claude/settings.json                  # 第三步
├── ci/
│   ├── guard-no-write.mjs                 # 第三步钩子
│   ├── guard-bash.mjs                     # 第三步钩子
│   ├── on-stop.mjs                        # 第三步钩子
│   ├── review.sh                          # 第四步主脚本
│   └── post-comments.mjs                  # 第五步
└── REVIEW.md                              # 第二步审查准则
```

**生产化替代（官方 action）**：若不想自己维护脚本，直接用官方封装，等价于把第二~五步打包好：

```yaml
- uses: anthropics/claude-code-action@v1
  with:
    anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
    claude_args: '--model claude-sonnet-4-6 --max-turns 20'
    prompt: |
      审查本 PR 改动文件，严格依据 REVIEW.md。
      对每个违规项在对应代码行留行内评论，标明级别与修复建议。
      无违规则留一条 ✅ 评论。
```

**验证清单**：

1. 本地干跑：`bash ci/review.sh`（先 `export PR_NUMBER=1`）应输出 findings.json
2. 推一个含硬编码 token 的测试 PR → 应被标 `high` finding
3. 推一个纯文档改动 PR → 应跳过（无代码文件）
4. 在 CI 日志确认 `PreToolUse` 钩子加载、`::notice::审查完成` 上报

---

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| 适合训练营 | DevOps / SRE / 平台工程 / 全栈进阶营 |
| 学员基础 | 装通 Claude Code + 会 Git/GitHub Actions 基础 |
| 预计教学时长 | 引导式 3–4 小时（含动手跑通）；自学 6–8 小时 |
| 难点 | hooks 的 JSON 协议、stream-json 解析、行内评论定位 |
| 可裁剪 | 原生 CLI 路径（重点教三件套）或官方 action 路径（快速落地）二选一 |

## 延伸阅读

- Claude Code 官方 Hooks 参考：`code.claude.com/docs/en/hooks`——hook 事件、JSON 输入输出、退出码完整规范
- Claude Code GitHub Actions：`code.claude.com/docs/en/github-actions`——官方 action 用法与 `@claude` 触发机制
- 官方 action 仓库 `anthropics/claude-code-action`——`claude_args`、`prompt`、权限与示例
- Headless 编程化调用完整指南：gist `JacobFV/2c4a75bc6a835d2c1f6c863cfcbdfa5a`——`-p` + `stream-json` 深度用法

## 参考来源

1. [How to Run Claude Code in a GitHub Action for Autonomous PR Review](https://startdebugging.net/2026/05/how-to-run-claude-code-in-a-github-action-for-autonomous-pr-review/) — `claude-code-action@v1` 自主审查 YAML 与 `claude_args` 模型选择实践
2. [Claude Code in GitHub Actions: A Complete Guide to Automated PR Fixes](https://groundy.com/articles/how-to-run-claude-code-as-a-github-actions-agent-for-automated-pr-fixes/) — 自动修 PR、CI 失败修复、成本与失控护栏
3. [Hooks reference - Claude Code Docs](https://code.claude.com/docs/en/hooks) — hook 事件、JSON schema、退出码 0/2 与 `permissionDecision` 协议
4. [claude-code-hooks-schemas (Gist)](https://gist.github.com/FrancisBourre/50dca37124ecc43eaf08328cdcccdb34) — 各 hook 的 stdin 输入与结构化输出实现级参考
5. [Using Claude Code Programmatically (Gist)](https://gist.github.com/JacobFV/2c4a75bc6a835d2c1f6c863cfcbdfa5a) — `-p` / `stream-json` 程序化调用与事件类型解析

---

> 本文由微信公众号「51学通信」站长爱卫生（微信：gprshome201101）整理输出。掌握 Claude Code 的 `-p` 模式、hooks 与批量处理三件套，你就能把 AI 审查固化进团队流水线，让每个 PR 都被 deterministic 的护栏与高质量模型双重把关。

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