本教程带你用 Claude Code 的「自动化三件套」——无头 -p 模式、生命周期 hooks、批量处理——从零搭建一个每个 PR 自动审查的 GitHub Actions 机器人。学完你将掌握:用 stream-json 解析审查结论、用 PreToolUse 钩子把审查范围锁死在 PR 改动文件、用 --max-turns 控制成本与防失控,以及把审查结果回写成 PR 行内评论。适合已装通 Claude Code、想把它接进团队流水线的开发者与 DevOps 工程师。
claude -p "hello"(参见系列入门篇)ANTHROPIC_API_KEYgh CLI 用于回写评论调试把 Claude Code 接进 CI 有两条路径,理解它们的分工是本教程的基石:
| 路径 | 本质 | 适合场景 |
|---|---|---|
官方 action anthropics/claude-code-action@v1 |
一层封装,把 -p 模式 + diff 收集 + 评论回写打包好 |
生产环境,快速上线 |
原生 CLI claude -p ... --output-format stream-json |
直接用底层能力,自己拼管道 | 想精细控制、本地干跑、自定义流程 |
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 会读它):
# 代码审查准则
- 禁止在代码中硬编码密钥、token、密码
- 所有公开函数必须有类型注解
- SQL 查询必须使用参数化,禁止字符串拼接
- 错误必须被处理,禁止空 except / catch 后吞掉
然后跑一次干跑(PR_DIFF 假设是当前分支相对 main 的 diff):
# 收集改动文件清单
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 通常够,别给几十否则成本失控。--output-format 换成 json(非 stream),只吐一个汇总对象,适合管道下游用 jq。-p 模式默认不会自动读仓库文件,要么在 prompt 里内联,要么用 --add-dir 或挂文件。本例直接内联清单 + diff 最稳。在 CI 里最怕两件事:审查员乱改文件(review 任务不该写代码)、跑飞烧钱。hooks 就是确定性护栏——它由 harness 执行,不依赖模型「听话」。
在仓库根目录写 .claude/settings.json(项目级配置,CI 会自动加载):
{
"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(审查员只能读、不能写):
// .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 命令白名单):
// .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,注意避免递归):
// .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 看钩子加载日志。--max-turns 做兜底。process.exit(2) 必须在你想阻断时显式调用。把上面三步串成 CI。关键是用 actions/checkout 的 fetch-depth: 0 拿到完整 diff,再把改动喂给审查脚本。
# .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 自动加载):
#!/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
审查的价值在于精确到行。.ci/post-comments.mjs 用 GitHub 的 line-position 评论 API,把每条 finding 钉到具体行:
// .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,含文件、行号、级别、违反准则、修复建议。无违规时不出评论(或出 ✅)。
常见问题:
gh pr comment。line 是 diff 里的新文件行号;若要对到精确行,建议优先用「文件 + 准则 + 关键代码片段」定位,行号仅作参考。permissions: pull-requests: write 且 GH_TOKEN 用的是默认 GITHUB_TOKEN。当 PR 改动几十个文件时,三个手段保住账单:
grep 只挑代码文件,过滤 *.lock、dist/、生成代码(见第四步)。-p 进程并行跑,用 xargs -P: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"
'
--max-turns 是第一道闸(单次会话轮次);再叠环境变量限输出 token,以及 workflow 级 timeout-minutes:- 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):若不想自己维护脚本,直接用官方封装,等价于把第二~五步打包好:
- 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。
对每个违规项在对应代码行留行内评论,标明级别与修复建议。
无违规则留一条 ✅ 评论。
验证清单:
bash ci/review.sh(先 export PR_NUMBER=1)应输出 findings.jsonhigh findingPreToolUse 钩子加载、::notice::审查完成 上报| 维度 | 评估 |
|---|---|
| 适合训练营 | DevOps / SRE / 平台工程 / 全栈进阶营 |
| 学员基础 | 装通 Claude Code + 会 Git/GitHub Actions 基础 |
| 预计教学时长 | 引导式 3–4 小时(含动手跑通);自学 6–8 小时 |
| 难点 | hooks 的 JSON 协议、stream-json 解析、行内评论定位 |
| 可裁剪 | 原生 CLI 路径(重点教三件套)或官方 action 路径(快速落地)二选一 |
code.claude.com/docs/en/hooks——hook 事件、JSON 输入输出、退出码完整规范code.claude.com/docs/en/github-actions——官方 action 用法与 @claude 触发机制anthropics/claude-code-action——claude_args、prompt、权限与示例JacobFV/2c4a75bc6a835d2c1f6c863cfcbdfa5a——-p + stream-json 深度用法claude-code-action@v1 自主审查 YAML 与 claude_args 模型选择实践permissionDecision 协议-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 等场景全覆盖。
