本教程聚焦 Claude Code 的 Headless(无人值守)自动化能力,目标是用 -p(--print)模式把 Claude Code 织进 GitHub Actions,搭建一条「PR 触发 → 拉取 diff → Claude 评审 → 回写评论 → 质量门禁」的端到端代码审查闭环。适合已经能在终端里用 Claude Code 完成单次任务、想把它升级为 CI 常驻评审员的工程师与 DevOps 同学。学完你将掌握:headless 三种输出格式的取舍、stream-json 的逐事件解析、Hooks 作为质量门禁的配置方法,以及一个可直接复制进仓库的完整 workflow。
claude --version 可输出版本号),完成至少一次交互式会话jq 已在本地可用(CI 的 ubuntu-latest 镜像默认自带)ANTHROPIC_API_KEY(或 Claude 订阅账号对应的 OAuth token)交互式 Claude Code 会启动一个 TUI,等待你逐轮输入。而 headless 模式把这一切变成「一次 prompt 进、一次结果出」的命令,天然适配脚本与 CI。最小用法:
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。稳妥写法是同时显式声明输入输出格式,避免它去探测终端:
claude -p --output-format text --input-format text "你的 prompt"
headless 的灵魂在于「输出可被程序消费」。三种格式各有定位:
text(默认)——纯文本回复,适合人类阅读或 >> 追加进文件、PR 描述。最简单,但无法区分「模型在思考」还是「模型在调用工具」。
json——把整次会话压成一个 JSON 对象返回,包含 result(最终文本)、cost_usd(花费)、duration_ms(耗时)、is_error 等字段。适合「跑完一次拿总结」的场景:
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 边读边过滤:
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,是最稳的组合。
headless 模式下没有人盯着「确认执行」按钮,所以护栏必须前置到配置里。Hooks 就是这套护栏——在特定事件点自动执行的 shell 命令,配置在 .claude/settings.json。
最常用的是两个事件:
PreToolUse:工具调用前触发,可拦截危险操作PostToolUse:工具调用后触发,可跑 lint / 测试 / 格式化下面这段配置做两件事:① 禁止 headless 模式下 git push --force;② 每次 Edit/Write 之后自动跑 eslint --fix。
{
"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 装好依赖。
现在把前三步拼成一条 workflow。设计目标:每个 PR 打开或更新时,Claude 自动评审 diff,并以评论形式贴回 PR。
先在仓库 Settings → Secrets and variables → Actions 添加 ANTHROPIC_API_KEY。然后新建 .github/workflows/claude-review.yml:
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 秒,会自动出现一条带「风险等级/问题/建议」的评论。
常见问题与对策:
jq 后加 head -c 60000 截断,或按文件拆成多条评论。if: steps.diff.outputs.size < 50000 的前置判断,超大 PR 改为只评审变更过的关键文件。github.rest.issues.listComments 先查是否已有 bot 评论,有则 updateComment 而非新建。单条 workflow 跑通后,下一步是批量。两种典型形态:
形态 A:一次评审多个文件。把文件列表喂给一个 headless 进程,让它串行处理,输出用 stream-json 区分每个文件的结果:
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 陷入死循环:
on:
schedule:
- cron: '17 2 * * 1' # 每周一凌晨(避开整点高峰)
workflow_dispatch:
批量通用护栏:
--max-turns 8,超时即停timeout 300 claude -p ... 包一层硬超时set +e + 逐条记录退出码到 report.csvjq -s 'map(.cost_usd) | add' 得出总花费把上面的零件组装成一个最小但完整的项目。目录结构:
.ai-reviewer/
├── settings.json # Hooks 护栏
├── prompt.md # 评审提示词
└── run.sh # 本地预演脚本
.github/workflows/
└── claude-review.yml # CI 入口
.ai-reviewer/prompt.md(评审提示词,单文件维护便于迭代):
你是一名注重工程实效的代码评审。对给定 diff 按下列规则评审:
## 评审范围
- 只看本次变更(diff),不评审未改动代码
- 优先级:安全 > 正确性 > 可读性 > 风格
## 输出格式(严格遵守)
对每个文件输出一个区块:
### `文件路径`
- **风险**:高 | 中 | 低
- **问题**:(编号列表,每条 ≤ 2 行)
- **建议**:(对应修复,给最小可行代码片段)
## 边界
- 不输出"整体看起来不错"等无信息量的话
- 风格类问题合并为一条,不逐条罗列
.ai-reviewer/run.sh(本地预演,CI 前 dry-run):
#!/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 同源」的关键,避免两边提示词漂移。
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 字段做账,超预算阈值时在评论里标注当次花费。
--print 标志、管道输入、CI/CD 自动化与批处理的实践指南[!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 等场景全覆盖。
