Build a Claude API Code Review Assistant: Full Tutorial
What you'll build
This tutorial walks through building a working code review assistant powered by Claude: a script that takes a diff, sends it to the Claude API with a structured prompt, and returns actionable review comments you can post to a pull request. By the end you'll have a CLI tool you can run locally or wire into CI, plus guidance on running it reliably across a team.
If you searched for "claude api code review assistant tutorial," you're likely trying to answer one of two questions: how do I structure a prompt so Claude gives useful review feedback instead of generic praise, and how do I get a diff from Git into the API and back into a usable comment format. Both are covered below, with working code.
How Claude-based code review actually works
A code review assistant is not magic — it's three steps repeated for every pull request:
- Extract the diff. Pull the changed lines from Git (or your CI provider's webhook payload).
- Send it to Claude with context. A clear system prompt, the diff, and optionally the full file for context.
- Parse the response into actionable comments. Ask for structured output (line numbers, severity, explanation) so you can post it back to GitHub/GitLab automatically.
The trick that separates a toy script from something your team will actually trust is step 2: prompt structure. Claude is good at finding real issues — unhandled errors, off-by-one logic, SQL injection risks, missing null checks — but only if you tell it what to look for and in what format to answer.
Step 1: Get the diff
git diff main...feature-branch -- '*.ts' '*.js' > review.diff
For CI, most providers expose the diff directly in the webhook or via their API (e.g., GitHub's /repos/{owner}/{repo}/pulls/{pull_number}/files).
Step 2: Write a prompt that produces useful feedback
Vague prompts ("review this code") produce vague output. Be explicit about format and severity:
You are a senior software engineer performing a code review.
Review the following diff. For each issue found, output a JSON object with:
- "file": file path
- "line": approximate line number
- "severity": "blocker" | "warning" | "nit"
- "comment": a concise, actionable explanation
Only report real issues: bugs, security risks, performance problems,
or violations of the stated style guide. Do not comment on formatting
if a linter would catch it. If there are no issues, return an empty array.
Diff:
<<<DIFF>>>
Step 3: Call the API
Here's the implementation using SubToAPI, which exposes Claude through a standard HTTPS API with an application key (sub_live_...) instead of managing Anthropic credentials and billing directly:
import fs from "fs";
const diff = fs.readFileSync("review.diff", "utf8");
const prompt = `You are a senior software engineer performing a code review.
Review the following diff. For each issue found, output a JSON array of objects with:
- "file", "line", "severity" ("blocker"|"warning"|"nit"), "comment".
Only report real issues. Return [] if none.
Diff:
${diff}`;
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "claude-opus-4",
max_tokens: 2000,
messages: [{ role: "user", content: prompt }],
}),
});
const data = await res.json();
const reviewText = data.content[0].text;
let issues = [];
try {
issues = JSON.parse(reviewText);
} catch {
console.error("Could not parse model output as JSON:", reviewText);
}
issues.forEach((issue) => {
console.log(`[${issue.severity.toUpperCase()}] ${issue.file}:${issue.line} — ${issue.comment}`);
});
Setup for the above takes two steps: sign up at /signup and get a key, then follow /docs/quickstart to confirm your first request works before wiring it into CI.
Step 4: Post comments back to the PR
Once you have structured issues, map each one to a comment on the PR using your Git provider's API. For GitHub, that's POST /repos/{owner}/{repo}/pulls/{pull_number}/comments with path, line, and body fields pulled directly from the parsed JSON.
for (const issue of issues) {
await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${pr}/comments`, {
method: "POST",
headers: { Authorization: `token ${GITHUB_TOKEN}` },
body: JSON.stringify({
body: `**${issue.severity.toUpperCase()}**: ${issue.comment}`,
path: issue.file,
line: issue.line,
commit_id: headSha,
}),
});
}
Making it production-ready
A few things matter once this moves from a personal script to something your team relies on:
- Streaming for large diffs. Big diffs can produce long reviews; streaming the response avoids timeouts on the CI job. See /docs/streaming.
- Structured output reliability. If Claude occasionally wraps JSON in prose, tighten the prompt or use tool calling so the model is constrained to a schema instead of free text. See /docs/tools.
- Rate limits and concurrency. If every PR triggers a review on every push, you'll hit concurrency limits fast on a busy repo. Batch diffs per PR instead of per commit.
- Team-wide usage visibility. When multiple repos and multiple engineers are triggering reviews, a shared dashboard with per-key usage is worth it instead of each person holding their own credentials. SubToAPI's /pricing covers Solo, Team, and Scale tiers with seat-based billing for exactly this case.
- Request/response reference. For full parameter details (temperature, system prompts, stop sequences), see /docs/messages.
Common pitfalls
- Reviewing whole files instead of diffs. This wastes tokens and produces noisy feedback on unchanged code. Stick to the diff plus minimal surrounding context.
- No severity levels. Without a severity field, every comment looks equally urgent and engineers start ignoring the bot.
- No fallback when parsing fails. Models occasionally return malformed JSON — always wrap
JSON.parsein a try/catch and log the raw output for debugging. - Running it on every commit. Trigger on PR open/update events, not every push, to avoid redundant API calls and comment spam.
questions
Can Claude catch security vulnerabilities, not just style issues? Yes, if you ask explicitly. Claude performs well on injection risks, unsafe deserialization, and missing input validation when the prompt names these categories — generic "review this" prompts tend to focus on style instead.
Should I send the full file or just the diff? Send the diff for speed and cost, but include a few lines of surrounding context per hunk so Claude understands the change without needing the entire file.
How do I avoid rate limit errors when reviewing large PRs? Batch all changed files into a single request per PR instead of one request per file, and use streaming for large payloads so the connection doesn't time out before the response completes.