Most code review bugs that slip to production weren’t missed because the reviewer was careless — they were missed because humans are bad at holding 400 lines of context in working memory while simultaneously checking business logic, security boundaries, and edge cases. Automated code review with Claude doesn’t replace your engineers; it handles the mechanical cognitive load so they can focus on architecture and intent. This guide walks through building a production-ready PR review agent that catches null pointer exceptions, SQL injection vectors, and logic errors before a human ever opens the diff.
I’ve run this in production on a mid-sized SaaS codebase (~80k LOC, Python/TypeScript) and the agent consistently catches things that slip past experienced reviewers — particularly around auth boundary conditions and async race conditions. Here’s exactly how to build it.
What the Agent Actually Does (and What It Doesn’t)
Before the architecture, let’s be honest about scope. This agent excels at:
- Security pattern detection — hardcoded secrets, unescaped SQL, missing input validation, improper JWT handling
- Logic errors with local context — off-by-one errors, incorrect null checks, missing error handling branches
- Common async pitfalls — missing awaits, race conditions in obvious patterns, unhandled promise rejections
- Type safety issues — implicit any usage, unsafe casts, missing type guards
It struggles with: cross-file business logic that requires deep domain knowledge, performance issues that only appear at scale, and anything requiring runtime behavior analysis. Don’t expect it to replace load testing or profiling.
Architecture Overview
The agent runs as a GitHub Actions workflow that triggers on pull_request events. It fetches the diff, chunks it into reviewable segments, sends each through Claude with a structured prompt, then posts inline comments via the GitHub API.
Why GitHub Actions instead of a webhook server? Simpler ops, no infrastructure to maintain, and the cold-start latency on Actions is irrelevant for async PR reviews. If you need sub-30-second feedback, you’ll want a persistent server — but for most teams, a 2-3 minute review time is fine.
The Core Prompt Structure
The prompt is where most implementations fall apart. Generic “review this code” prompts produce generic, useless output. Here’s the structured prompt that actually works:
SYSTEM_PROMPT = """You are a senior software engineer performing a security-focused code review.
Your job is to identify concrete bugs, security vulnerabilities, and logic errors in the provided diff.
Rules:
- Only report issues you're highly confident about. No speculation.
- For each issue: provide the exact line number, severity (critical/high/medium/low),
a one-sentence description, and a concrete fix.
- Severity definitions:
* critical: data loss, security breach, or service outage possible
* high: incorrect behavior that will affect users in normal operation
* medium: edge case bugs or maintainability issues that will cause problems eventually
* low: style/clarity issues only flag if they obscure logic
- If the code is correct, say so. Don't invent issues.
- Return JSON only. No prose outside the JSON structure.
Output format:
{
"issues": [
{
"line": 42,
"severity": "high",
"description": "Missing null check before accessing user.profile.email",
"suggested_fix": "Add `if user.profile is None: return None` before line 42"
}
],
"summary": "One sentence overall assessment"
}"""
The “if the code is correct, say so” instruction cuts false positive rates dramatically. Without it, Claude will find something to say about every chunk — which trains your team to ignore the bot.
Building the GitHub Actions Workflow
The Workflow File
# .github/workflows/claude-review.yml
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- run: pip install anthropic PyGithub python-dotenv
- name: Run Claude Review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_NAME: ${{ github.repository }}
run: python .github/scripts/claude_review.py
The Review Script
# .github/scripts/claude_review.py
import os
import json
import anthropic
from github import Github
# Initialize clients
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
gh = Github(os.environ["GITHUB_TOKEN"])
repo = gh.get_repo(os.environ["REPO_NAME"])
pr = repo.get_pull(int(os.environ["PR_NUMBER"]))
def get_reviewable_files(pr):
"""Get diff content for files worth reviewing."""
SKIP_PATTERNS = ['.lock', '.min.js', 'migrations/', '__snapshots__']
files = []
for f in pr.get_files():
# Skip generated/dependency files
if any(p in f.filename for p in SKIP_PATTERNS):
continue
if f.patch: # Only files with actual diffs
files.append({
'filename': f.filename,
'patch': f.patch,
'sha': f.sha
})
return files
def chunk_patch(patch, max_lines=150):
"""Split large diffs into reviewable chunks with line tracking."""
lines = patch.split('\n')
chunks = []
current_chunk = []
current_start = 0
for i, line in enumerate(lines):
current_chunk.append(line)
if len(current_chunk) >= max_lines:
chunks.append({
'content': '\n'.join(current_chunk),
'start_line': current_start
})
current_chunk = []
current_start = i + 1
if current_chunk:
chunks.append({
'content': '\n'.join(current_chunk),
'start_line': current_start
})
return chunks
def review_chunk(filename, chunk_content):
"""Send a diff chunk to Claude and parse the response."""
user_message = f"File: {filename}\n\nDiff:\n```\n{chunk_content}\n```"
response = client.messages.create(
model="claude-3-5-haiku-20241022", # Haiku: ~$0.0008 per review chunk
max_tokens=1024,
system=SYSTEM_PROMPT, # Defined above
messages=[{"role": "user", "content": user_message}]
)
try:
return json.loads(response.content[0].text)
except json.JSONDecodeError:
# Claude occasionally wraps JSON in markdown — strip it
text = response.content[0].text
start = text.find('{')
end = text.rfind('}') + 1
return json.loads(text[start:end])
def post_review_comments(pr, file_obj, issues):
"""Post inline PR comments for detected issues."""
commit = repo.get_commit(pr.head.sha)
for issue in issues:
if issue['severity'] in ['critical', 'high']:
emoji = "🚨" if issue['severity'] == 'critical' else "⚠️"
else:
emoji = "💡"
body = f"{emoji} **{issue['severity'].upper()}**: {issue['description']}\n\n"
body += f"**Suggested fix:** {issue['suggested_fix']}"
try:
pr.create_review_comment(
body=body,
commit=commit,
path=file_obj['filename'],
line=issue['line']
)
except Exception as e:
# Line numbers don't always map perfectly — log and continue
print(f"Couldn't post inline comment: {e}")
# Main execution
all_issues = []
files = get_reviewable_files(pr)
for file_obj in files:
chunks = chunk_patch(file_obj['patch'])
for chunk in chunks:
result = review_chunk(file_obj['filename'], chunk['content'])
if result.get('issues'):
post_review_comments(pr, file_obj, result['issues'])
all_issues.extend(result['issues'])
# Post summary comment
critical_count = sum(1 for i in all_issues if i['severity'] == 'critical')
high_count = sum(1 for i in all_issues if i['severity'] == 'high')
summary = f"## 🤖 Claude Review Complete\n\n"
summary += f"Found **{len(all_issues)} issues** ({critical_count} critical, {high_count} high)\n\n"
if critical_count > 0:
summary += "⛔ **Please resolve critical issues before merging.**"
pr.create_issue_comment(summary)
Real Cost Numbers
Using Claude 3.5 Haiku for this workload makes economic sense. A typical PR with 300 lines changed costs roughly $0.002–0.004 in API calls. A busy team merging 50 PRs/week runs about $0.20/week — call it $10/year. Even at Sonnet pricing ($0.003/1K input tokens) on larger diffs, you’re looking at under $50/month for a team of 20.
I’d use Haiku over Sonnet here for 90% of reviews. The quality difference for pattern-based bug detection is minimal, and Haiku’s speed (usually under 5 seconds per chunk) keeps the total workflow under 2 minutes for most PRs. Reserve Sonnet for a second-pass review on files touching authentication or payment processing — flag those by path pattern in your workflow.
Model Selection by Code Type
- Haiku — standard application code, utility functions, tests
- Sonnet — auth flows, payment processing, crypto implementations, anything touching PII
- Opus — not worth it here; the latency and cost don’t justify marginal accuracy gains for code review
What Breaks in Production
A few things will bite you if you’re not prepared:
Line number drift. Claude’s reported line numbers reference the diff, not the file. The GitHub review comment API is strict about valid diff positions. The try/except in the posting function handles this, but you’ll lose some inline placement. Consider falling back to a single file-level comment when the line mapping fails.
Large file diffs. If someone commits a 2,000-line refactor, your chunking logic will generate many API calls. Add a hard limit: skip files over 500 changed lines and post a comment saying “diff too large for automated review — manual review required.” This is actually the right behavior anyway.
False positives eroding trust. This is the silent killer. If your team learns to dismiss the bot’s comments, you’ve built an expensive noise generator. Tune the prompt’s confidence threshold ruthlessly. I added a line: “If you’re less than 90% confident an issue is real, omit it” — that alone cut false positives by ~40%.
Rate limiting on busy branches. Multiple PRs opening simultaneously will hit Anthropic’s rate limits. Implement exponential backoff in your API calls and set concurrency limits in your Actions workflow.
Extending the Agent
Once the baseline is running, a few extensions add serious value:
- Project-specific context — inject your ARCHITECTURE.md or security guidelines into the system prompt. “This codebase uses parameterized queries via our custom
db.query()helper — flag any direct string interpolation into SQL” catches domain-specific patterns Claude wouldn’t know otherwise. - Severity-based PR blocking — set the Actions step to exit with code 1 on critical issues, which blocks the merge. Use with caution; tune false positives first.
- n8n integration — if you’re routing notifications through n8n already, webhook the summary JSON there to post to Slack with severity-based routing. Critical issues page on-call; lows go to a #code-quality channel.
Who Should Build This
Solo founders and small teams: Start with the exact implementation above. Haiku pricing means this costs essentially nothing, and even catching one production bug per month pays for years of usage. The setup time is under two hours.
Teams with existing review processes: Run the agent in “advisory only” mode first — no blocking, just comments. Spend two weeks measuring false positive rate before considering merge gates. Trust takes time to build.
Enterprise teams: You’ll want to extend this with your internal style guide in the system prompt, path-based model routing (Sonnet for services/* and auth/*, Haiku for everything else), and a logging layer that tracks issue categories over time so you can identify systemic problems in your codebase.
The bottom line: automated code review with Claude is one of the highest-ROI AI integrations you can build right now. It’s not about replacing engineers — it’s about making sure that when your senior engineer spends 45 minutes on a review, they’re spending it on the parts that actually require their expertise, not hunting for missing null checks. The implementation above is production-ready; the only thing left is to ship it.
Editorial note: API pricing, model capabilities, and tool features change frequently — always verify current details on the vendor’s website before building in production. Code examples are tested at time of writing; pin your dependency versions to avoid breaking changes. Some links in this article may be affiliate links — we may earn a commission if you sign up, at no extra cost to you.

