1. 为什么要把 Claude Code 塞进 git hook 和 CI/CD
claude -p这个参数,是我最近用得最顺手的一个开关。它让 Claude Code 从「一个要坐在终端前聊天的工具」变成「一个能被脚本调用的命令行程序」。你写代码、提交、推送,它自己跑完 review,把结果写进日志,你有空再看。整个过程不需要你盯着屏幕。
先说清楚它是什么。Claude Code headless mode,中文一般叫无头模式或非交互模式,核心就是-p(print)参数:你直接把提示词当参数传进去,它执行完输出结果,进程退出。没有交互界面,不等你输入,不占终端。这意味着任何能执行 shell 命令的地方都能触发它——git hook、CI 流水线、cron 定时任务、Makefile,甚至另一个脚本里的一行。
它适合谁?三类人最该关注。第一类是每天要手动跑好几遍 code review 的开发者,重复劳动最容易被自动化吃掉。第二类是想在 CI 里加一道 AI 检查但不想引入复杂平台的人,一条命令就能接进现有流水线。第三类是已经在用 Claude Code 但只把它当聊天窗口的人,headless mode 能把它变成你工具箱里的一个普通命令。
我试过的第一个场景就是 pre-push hook。以前每次推送前,我要么忘了 review,要么手动开一个会话粘代码进去。现在 hook 里一行claude -p,推送时自动跑,结果落到.claude-review.log,推送照常进行,我事后翻日志就行。这个改动很小,但省掉的是「记得去做」这件事本身的认知负担。
不过要跑通,光有-p不够。你得解决三个问题:Claude Code 怎么在无界面环境里拿到模型能力、git hook 里怎么传参和落盘、CI 里怎么保证它稳定触发而不是随机失败。下面按顺序拆。
2. 前置准备:让 headless 模式拿到稳定的模型通道
Claude Code 本身是个客户端,headless 模式下它依然要调用背后的模型服务。本地交互时你可能已经登录过,但 CI 环境是干净的容器,没有登录态,也没有浏览器可以走 OAuth。这时候你需要一个能用 API Key 直接调用的通道,把 Base URL、Key、Model ID 三件套配好。
我用的方式是走 TaoToken 的 API 通道。它的接口地址是https://taotoken.net/api,兼容 Anthropic 的调用格式,Claude Code 可以直接指过去。先去控制台建一个 API Key,地址是https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite,建完复制出来,后面配置要用。
配置有两种方式,本地和 CI 通用。第一种是环境变量,最省事:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="sk-你的key" export ANTHROPIC_MODEL="claude-sonnet-4-5-20250929"第二种是写进 Claude Code 的 settings 文件,路径是~/.claude/settings.json,适合本地长期使用:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的key", "ANTHROPIC_MODEL": "claude-sonnet-4-5-20250929" } }注意 Model ID 要写全,别只写claude-sonnet,headless 模式下模型名不完整会直接报错退出,而且因为是非交互,你看不到友好提示,只有一个非零退出码。这一点在 CI 里特别坑,后面排障章节会细说。
如果你用的是 Codex 那套,配置在~/.codex/auth.json,结构不一样,但同样是 Base URL + Key + Model ID 三件套,缺一不可。Cline 的 MCP 配置也是同理,mcp.json里把这三项填全。不管哪个客户端,headless 场景下「配置完整」比「配置好看」重要得多。
配好之后先别急着写 hook,手动验证一次:
claude -p "回复 ok" --output-format text能打印出ok就说明通道通了。这一步花两分钟,能省掉后面在 CI 里瞎猜的半小时。
3. 可复制配置:git hook 脚本与 CI 流水线片段
配置通了,接下来把claude -p真正接进自动化链路。分两块:本地 git hook 和 CI 流水线。
先说 git hook。在项目根目录建.git/hooks/pre-push,注意这个文件默认不存在,要自己创建,并且给执行权限:
touch .git/hooks/pre-push chmod +x .git/hooks/pre-push内容这样写:
#!/bin/bash # pre-push hook:推送前自动跑一次 review set -e LOG_FILE=".claude-review.log" DIFF_RANGE="origin/main...HEAD" echo "[claude-review] 开始检查 $(date)" >> "$LOG_FILE" git diff "$DIFF_RANGE" | claude -p "根据这个 diff 做 code review,重点检查边界条件、错误处理和潜在的 null 引用。用简洁的中文列出问题,每条一行。" \ --output-format text >> "$LOG_FILE" 2>&1 echo "[claude-review] 检查结束 $(date)" >> "$LOG_FILE" exit 0几个关键点。set -e让脚本遇到错误就停,避免半途而废还继续推送。exit 0很重要——review 只是提示,不该阻断推送,否则你改个错别字都被卡住会疯。日志追加而不是覆盖,方便对比多次结果。2>&1把错误也收进日志,不然 headless 模式下报错你根本看不到。
再说 CI。以 GitHub Actions 为例,在.github/workflows/claude-review.yml:
name: Claude Code Review on: pull_request: types: [opened, synchronize] jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: 安装 Claude Code run: npm install -g @anthropic-ai/claude-code - name: 跑 headless review env: ANTHROPIC_BASE_URL: https://taotoken.net/api ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_MODEL: claude-sonnet-4-5-20250929 run: | git diff origin/${{ github.base_ref }}...HEAD | \ claude -p "review 这个 PR 的改动,列出三个最需要关注的问题" \ --output-format text > review.txt - name: 上传结果 uses: actions/upload-artifact@v4 with: name: claude-review path: review.txtfetch-depth: 0必须加,否则 checkout 是浅克隆,git diff拿不到完整历史,diff 会是空的,Claude 收到空输入只会回一句「没有改动」,你还以为它坏了。API Key 放 secrets,别硬编码进 yml。
如果你要更结构化的输出,把--output-format text换成json,CI 里可以用jq解析:
claude -p "review 这个 diff" --output-format json > review.json jq -r '.result' review.jsonjson格式返回的是一个对象,正文在result字段里。这个格式在需要程序化处理结果时更好用,比如把问题数量统计出来当流水线指标。
4. 双端验证:本地跑通再到流水线确认
配置写完不代表能跑。我习惯先在本地验证,再推到 CI,这样出问题定位范围小。
本地验证分两步。第一步,手动触发 hook,不用真的推送:
echo "test" | .git/hooks/pre-push cat .claude-review.log如果日志里出现了 review 内容,说明 hook 逻辑和模型通道都通了。如果日志是空的,先看claude -p "回复 ok"单独跑通没有,再检查 hook 里的 diff 范围是不是空的。
第二步,做一次真实的推送验证。改一行代码,git add、git commit、git push,推送过程中你应该能看到 hook 在跑(如果没加--quiet),推送完成后日志里多了一段新内容。这一步确认的是 hook 在真实 git 流程里能被触发,而不是只能手动调用。
本地通了,再验证 CI。把改动推到一个分支,开一个 PR,去 Actions 页面看流水线。成功的标志是 job 变绿,artifact 里能下载到review.txt并且有实际内容。
这里有个容易忽略的点:CI 里的 diff 范围和本地不一样。本地origin/main...HEAD在 CI 里要换成origin/${{ github.base_ref }}...HEAD,因为 CI 的 base 分支名是动态的。我第一次接的时候就是照抄本地命令,结果 CI 里 diff 为空,review 输出「没有改动」,排查了半天才发现是分支名写死了。
双端都跑通后,建议在日志里加一行时间戳和 commit hash,方便回溯:
echo "[claude-review] commit=$(git rev-parse --short HEAD) at $(date)" >> "$LOG_FILE"这样以后翻日志,能直接对应到是哪次提交触发的 review。
5. 常见报错排查:401、local proxy failed、reading choices
headless 模式最难受的地方是报错不友好。交互模式下它会告诉你哪里错了,非交互模式下经常只有一个退出码。下面是我踩过的几个典型错误和对应解法。
401 Unauthorized。最常见,基本是 Key 的问题。三种可能:Key 没设进环境变量(CI 里 secrets 名字写错)、Key 过期或被删、Base URL 和 Key 不匹配(比如 Key 是 A 平台的,URL 指向 B 平台)。排查方法是在同一环境里手动跑curl测一下:
curl -s -o /dev/null -w "%{http_code}" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ https://taotoken.net/api/v1/messages返回 401 就是 Key 的问题,返回 400 说明 Key 有效只是请求体不完整,返回 200 或 405 说明通道没问题。
local proxy failed。这个报错通常出现在 CI 容器里,原因是 Claude Code 尝试走本地代理但环境里没有。解法是显式设置NO_PROXY或者清掉HTTP_PROXY/HTTPS_PROXY:
unset HTTP_PROXY HTTPS_PROXY export NO_PROXY="localhost,127.0.0.1"如果 CI 平台本身要求走代理,那就得把 TaoToken 的域名加进NO_PROXY白名单,别让请求被代理拦掉。
Error reading choices / unexpected response format。这个多半是--output-format json时返回体不是预期结构,常见于模型名写错导致服务端返回了错误对象而不是正常结果。检查ANTHROPIC_MODEL是不是完整 ID。另一个可能是输出被截断,长 diff 加上长回复超过了 token 上限,解法是把 diff 分段传,或者用--max-tokens控制输出长度。
OAuth 相关报错。CI 里如果 Claude Code 尝试走 OAuth 登录流程,会卡住然后超时。这是因为环境变量没设全,它 fallback 到了交互登录。确保ANTHROPIC_API_KEY和ANTHROPIC_BASE_URL都设了,它就不会去尝试 OAuth。
hook 不执行。检查三件事:文件有没有执行权限(chmod +x)、shebang 是不是#!/bin/bash、文件路径对不对(.git/hooks/pre-push不是.git/hook/)。还有一个隐蔽的:如果你用了 husky 之类的 hook 管理工具,它可能覆盖了原生 hook 目录,要去 husky 的配置里加。
排查通用思路:把claude -p单独拎出来在目标环境跑一次,能复现就说明是配置问题,不能复现就说明是 hook 或 CI 的上下文问题。二分法定位,比盯着日志猜快得多。
6. 把自动化链路固定下来
跑通之后,我建议做两件收尾的事。
第一件,把 review 的提示词抽成独立文件,别硬编码在 hook 里。建一个.claude/prompts/review.md,hook 里用cat读进来:
PROMPT=$(cat .claude/prompts/review.md) git diff "$DIFF_RANGE" | claude -p "$PROMPT" --output-format text >> "$LOG_FILE"这样调整 review 重点时改一个文件就行,不用动 hook 逻辑,团队里其他人也能贡献提示词。
第二件,给自动化加个开关。不是每次推送都想跑 review,加个环境变量控制:
if [ "$SKIP_CLAUDE_REVIEW" = "1" ]; then echo "跳过 review" exit 0 fi紧急推送时SKIP_CLAUDE_REVIEW=1 git push就能绕过,不用临时改脚本。
如果你想把这条链路用得更重,比如在 CI 里跑多轮分析、接 Agent 做自动修复,可以考虑 Coding Plan 这类长期方案,地址是https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite。只是单纯验证模型输出效果,用模型对话页面更快:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite。API Key 管理和接入文档分别在https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite和https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite。
最后留一个我自己的习惯:每次改完 hook 或 CI 配置,先在本地用echo "test diff" | claude -p "回复收到"确认通道没断,再推。通道断了的时候,hook 会静默失败,日志里只有时间戳没有内容,很容易被忽略。多花十秒确认,比事后翻半天日志划算。