Automated code review on Claude via API
Setting up Claude in GitHub Actions to comment on pull requests, without double-charging on job retries or blowing the budget on one huge diff.
4 min · · Updated · development · код-ревью
Reviewing pull requests with a bot boils down to one operation: take the diff, send it to a model with "find bugs and issues," get back a list of comments. The hard part isn't the prompt — it's making sure this doesn't break in CI or drain the budget on the first large PR.
Which endpoint and which model
Code review calls for /v1/messages (the Anthropic Messages API) — it gives more control over the system prompt than chat completions. Use claude-sonnet-5 for regular PRs and claude-opus-5-5 for critical branches like main or release/*, where review quality matters more than speed. Both carry a 1,000,000-token context window, so a diff of a couple thousand lines fits whole, no chunking required.
curl https://api.zukko.pro/v1/messages \
-H "Authorization: Bearer fetch_your_key" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-sonnet-5",
"max_tokens": 2000,
"system": "You are a code reviewer. Look for bugs, races, resource leaks, security issues. Skip formatting comments. Reply as a list: file, line, issue, suggested fix.",
"messages": [{"role": "user", "content": "'"$(git diff origin/main... | jq -Rs .)"'"}]
}'Idempotency is not optional in CI
GitHub Actions retries failed jobs. Without protection, a job retry on the same commit sends the same diff a second time and charges twice. The fix is an Idempotency-Key header built from the PR and commit hash:
IDEMPOTENCY_KEY="review-$(git rev-parse HEAD)-$PR_NUMBER"
curl https://api.zukko.pro/v1/messages \
-H "Authorization: Bearer fetch_your_key" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d @payload.jsonWithin 24 hours, a repeated request with the same key returns the same response without a second charge. This is exactly the case where a runner retry or a network timeout on the CI side shouldn't turn into a double bill.
Estimating cost before you run it
A 50-file diff can run into hundreds of thousands of tokens, and on a large monorepo that adds up per PR. Before sending it off for review, it's worth running the text through the free token count:
curl https://api.zukko.pro/v1/messages/count_tokens \
-H "Authorization: Bearer fetch_your_key" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model": "claude-sonnet-5", "messages": [{"role": "user", "content": "..."}]}'If the number clears a sane threshold — say, 300,000 tokens — it makes more sense to trim the diff down to changed functions plus their calling context, or to switch to per-file review instead of reviewing the whole PR at once.
Budget control through the key
The CI bot should run on its own API key, not a shared one. In the dashboard (https://zukko.pro/app/api-keys) a key can carry a daily or monthly dollar spend cap and a list of allowed models. The practical benefit: if a recursive call slips into the pipeline, or someone sets review-on-every-push instead of review-on-PR, the key hits key_daily_limit_reached (429) instead of draining the whole account balance overnight.
{"error": {"message": "Daily limit reached for this key", "code": "key_daily_limit_reached"}}Handle this in the script by not failing loudly but skipping the review gracefully, leaving a comment like "daily limit reached, review skipped" instead of a red X scaring developers.
What to log from response headers
Two response headers are useful for monitoring the bot: X-Zukko-Cost-Micros, the cost of that specific call in micro-dollars, and X-Fetch-Balance-Cents, the remaining balance (despite the name, in microdollars: 1,000,000 = $1; the exact balance in cents comes from GET /v1/balance, the balance_cents field). Both headers only come with regular, non-streaming responses. Logging the first per PR makes it easy to chart "review cost per month" and compare it against reviewer hours.
import requests
resp = requests.post(
"https://api.zukko.pro/v1/messages",
headers={
"Authorization": "Bearer fetch_your_key",
"anthropic-version": "2023-06-01",
},
json=payload,
)
cost_usd = int(resp.headers.get("X-Zukko-Cost-Micros", 0)) / 1_000_000
print(f"Review cost: ${cost_usd:.4f}")Caching for repeated runs
If the pipeline re-runs review every time a new comment is posted on a PR, and the diff hasn't changed since last time, it's worth enabling response caching with the X-Zukko-Cache: true header — without temperature: 0: claude-sonnet-5 and claude-opus-5-5 may reject a temperature other than 1 with a 400. A request with a fully identical body from the same user caches for up to 7 days, and a repeat call returns instantly and for free, with X-Zukko-Cache: hit in the response (such hits aren't written to /v1/usage); "almost identical" requests don't hit the cache. That doesn't fit review itself, since the prompt always carries the current diff, but it works well for re-analyzing an unchanged file if the bot gets invoked on demand several times in a row.
Errors you'll actually hit in production
503 model_disabled — the configured model has been switched off in the catalog (not a 404 and not model_retired); take a replacement from the current GET https://api.zukko.pro/v1/models list and log the fact for a later pipeline update. 503 provider_unavailable — the model is temporarily unavailable at the provider; for both cases it's worth keeping a fallback to another model, e.g. gpt-5.6-sol via /v1/chat/completions, so review doesn't fully block a merge. 422 idempotency_conflict shows up when the same Idempotency-Key is sent with a different request body — usually a bug in key generation (like forgetting to include the PR number), not a server problem.
Put together: a dedicated capped key for the bot, idempotency on every call, count_tokens before large diffs, and logging X-Zukko-Cost-Micros — that's enough to run code review on Claude in production for months without surprises on the bill.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Prompt Engineering Examples: 12 Ready-Made Templates for WorkTwelve prompt templates for everyday tasks: what each one says, why it is built that way, and what a single API call costs.