Build a Code Review AI Agent with Claude

Claude Opus 5.5 and Claude Sonnet 5.5 are the strongest code models in production in 2026. Building a code review agent on top of them is one of the highest-leverage AI integrations a dev team can ship: pull-request feedback within 30 seconds of pushing, catching bugs humans miss, enforcing team conventions without manual review bandwidth. This article walks through a working code review agent architecture end to end, with code you can adapt.

What a code review agent actually does

At minimum: receive a diff, analyze it, and post comments on the lines where issues exist. Beyond minimum, a good agent runs tests to check the diff actually works, searches related code to understand context, and classifies feedback by severity so reviewers can focus.

Our target architecture:

  1. GitHub Actions triggers on pull_request
  2. Fetch the diff and relevant file contents
  3. Pass to Claude with structured output asking for findings
  4. Post findings as PR comments with the file and line anchored
  5. Mark the review as passed, needs changes, or has advisories

The core review loop

Start with a single Python script that takes a diff and returns structured findings:

from anthropic import Anthropic
from pydantic import BaseModel

class Finding(BaseModel):
    file: str
    line: int
    severity: str  # "bug" | "security" | "style" | "nitpick"
    message: str
    suggested_fix: str | None = None

client = Anthropic()

def review_diff(diff_text, repo_context):
    response = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=4096,
        tools=[{
            "name": "report_findings",
            "description": "Report all findings from this review",
            "input_schema": {
                "type": "object",
                "properties": {
                    "findings": {
                        "type": "array",
                        "items": Finding.model_json_schema(),
                    },
                },
                "required": ["findings"],
            },
        }],
        tool_choice={"type": "tool", "name": "report_findings"},
        messages=[{
            "role": "user",
            "content": f"""You are a senior engineer reviewing a pull request.

Repository context:
{repo_context}

Diff to review:
{diff_text}

Review the diff for correctness, security, performance, and style issues.
Only flag real problems. Be specific: cite the file and line number.
Classify severity: bug (will break), security (vulnerability),
style (team convention), nitpick (opinion).
"""
        }],
    )
    tool_use = next(b for b in response.content if b.type == "tool_use")
    return [Finding(**f) for f in tool_use.input["findings"]]

This is 50 lines and produces structured findings. The structured-output constraint via tool-use means no parsing errors; the model has to return findings in the right shape.

Context: giving Claude what it needs

Reviewing a diff in isolation produces generic feedback. Give the model the surrounding file:

def build_context(pr_files):
    chunks = []
    for f in pr_files:
        full_file = fetch_file_content(f.filename, f.sha)
        chunks.append(f"--- {f.filename} (full file) ---\n{full_file}\n--- end {f.filename} ---")
    return "\n\n".join(chunks)

For larger PRs, cap at the most-changed 10 files or send diff hunks with 20 lines of context (git diff –unified=20). Claude’s 200K context window easily holds a typical PR; the real limit is latency and cost, not tokens.

For projects with coding conventions, add the team style guide to the prompt. Claude reads it and applies it to the review.

Enabling Claude to run tests and search

A stronger agent uses tools to verify its hypotheses. Add a run_tests tool and a search_code tool:

tools = [
    {
        "name": "search_code",
        "description": "Grep the codebase for a pattern",
        "input_schema": {
            "type": "object",
            "properties": {"pattern": {"type": "string"}},
            "required": ["pattern"],
        },
    },
    {
        "name": "run_tests",
        "description": "Run the test suite and return pass/fail output",
        "input_schema": {
            "type": "object",
            "properties": {"test_path": {"type": "string"}},
            "required": [],
        },
    },
    {
        "name": "report_findings",
        "description": "Final findings report",
        "input_schema": { ... },
    },
]

Loop the model, dispatching tool calls, until it calls report_findings:

messages = [{"role": "user", "content": initial_prompt}]
for step in range(10):  # cap at 10 steps
    response = client.messages.create(
        model="claude-opus-5-5", max_tokens=4096,
        tools=tools, messages=messages,
    )
    messages.append({"role": "assistant", "content": response.content})

    tool_results = []
    for block in response.content:
        if block.type != "tool_use":
            continue
        if block.name == "report_findings":
            return block.input["findings"]
        result = dispatch_tool(block.name, block.input)
        tool_results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": result,
        })

    if not tool_results:
        break  # model is done without reporting; treat as empty
    messages.append({"role": "user", "content": tool_results})

Capping steps prevents runaway loops. 10 is enough for most reviews; raise to 20 for complex multi-file diffs.

Posting findings as GitHub comments

Use the GitHub REST API’s review-comment endpoint, which lets you anchor a comment to a specific file and line in the diff:

import requests

def post_review(repo, pr_number, findings, github_token):
    comments = []
    for f in findings:
        body = f"**{f.severity.upper()}**: {f.message}"
        if f.suggested_fix:
            body += f"\n\n```suggestion\n{f.suggested_fix}\n```"
        comments.append({
            "path": f.file,
            "line": f.line,
            "body": body,
        })

    event = "REQUEST_CHANGES" if any(x.severity == "bug" for x in findings) else "COMMENT"
    requests.post(
        f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews",
        headers={"Authorization": f"token {github_token}"},
        json={"event": event, "comments": comments},
    )

The suggestion code block renders as a one-click commit in GitHub’s UI. Train your model to produce these fixes when it is confident, so reviewers can accept them in one click.

GitHub Actions wiring

# .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
      - uses: actions/setup-python@v5
      - run: pip install anthropic requests pydantic
      - run: python .github/scripts/claude_review.py
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REPO: ${{ github.repository }}

Keeping cost under control

Code review calls are not cheap. Average cost per PR with Claude Opus 5.5 and a 10K-line context: $0.15 to $0.60 per review. At 200 PRs per month that is $30 to $120. Mitigate:

  • Use Claude Sonnet 5.5 ($3/M input vs $15 for Opus) for most reviews; escalate to Opus only on diffs flagged as high-risk (contain SQL, auth, payments).
  • Enable prompt caching on the system prompt and style guide (which do not change per PR). Cuts input cost 90% on cached portions.
  • Skip reviews on docs-only PRs and dependency bumps (detect via diff file extensions).
  • Cache the review for a given commit SHA. If the PR is force-pushed, re-review; if just a branch rename, reuse the cached review.

Common failure modes and fixes

  • Nitpick flood: the model finds 50 “could be cleaner” comments. Fix: tune the prompt to only flag real bugs; set a cap on findings per PR.
  • False positives on standard patterns: the model flags idiomatic framework code. Fix: feed the framework docs in the context, or use a few-shot example of “do not flag this pattern”.
  • Missing cross-file bugs: the model misses a bug caused by a change in another file. Fix: expand context to include referenced files (resolve imports).
  • Line numbers off by one: the model occasionally gets line numbers wrong. Fix: pass line-numbered file content (“1: def foo():\n2: pass\n”); accuracy jumps.

Frequently Asked Questions

Should I use Claude or GPT-5 for code review?

Claude Opus 5.5 and Claude Sonnet 5.5 outperform GPT-5 on most code review benchmarks in 2026, especially for long diffs and multi-file reasoning. GPT-5 is competitive on single-file reviews and may be faster. Benchmark both on your actual PR corpus before deciding.

Can the agent fix bugs automatically or just flag them?

Both. Return a suggested_fix field in each finding and format it as a GitHub suggestion block; reviewers can accept the fix in one click. For simple bugs this works often; for complex ones the fix is wrong 20-40% of the time, so always show it as a suggestion for a human to confirm, never auto-commit.

How do I prevent the model from being too harsh or too lenient?

Tune the prompt with concrete examples: “This is a nitpick we would ignore. This is a style issue worth flagging. This is a bug.” Few-shot calibration beats temperature tuning. Measure the ratio of findings-to-false-positives weekly and adjust.

What about security-sensitive code (auth, crypto)?

Add a security_sensitive flag that detects files or patterns touching auth, crypto, or secrets. Route these PRs to the stronger model (Opus) with an expanded context and a security-focused system prompt. Pair with a human security review for very high-risk changes.

Can I run the agent locally before pushing?

Yes. Wrap the review script as a git pre-push hook or a claude-review CLI command. Faster iteration and keeps API costs local to the engineer who ran it.

Does the agent see secret environment variables or private branches?

Only the ones you give it. GitHub Actions passes secrets as env vars; be careful not to log them in review output. Do not send branches containing private customer data to the LLM without consent.

Angel Jude Suarez

Full-Stack Developer at PIES IT Solution

Focuses on Python development, machine learning, and AI integration. Has built production AI systems including OpenAI Whisper integration for medical transcription and GPT-4o-powered diagnosis assistance. Strong background in pandas, scikit-learn, and TensorFlow.

Expertise: Python · PHP · Java · VB.NET · ASP.NET · Machine Learning · AI Integration · OpenCV · Django · CodeIgniter  · View all posts by Angel Jude Suarez →

Leave a Comment