Skip to content
Learn Agentic AI13 min read0 views

Building a Claude Code Review Agent: Automated PR Analysis and Suggestions

Build a code review agent that parses GitHub PR diffs, analyzes code changes with Claude, generates actionable suggestions, and posts review comments via the GitHub API.

Why Automate Code Reviews

Code reviews are critical for code quality, but they create bottlenecks. Reviewers miss subtle bugs when fatigued, junior developers wait days for feedback, and style issues consume review time that could be spent on logic and architecture. A Claude-powered code review agent handles the repetitive parts — style enforcement, bug pattern detection, security scanning, and documentation checks — letting human reviewers focus on design decisions and business logic.

The agent we will build fetches PR diffs from GitHub, analyzes each changed file with Claude, generates specific suggestions with line-level precision, and posts review comments back to the PR.

Fetching PR Diffs from GitHub

Use the GitHub API to get the pull request diff and file changes:

import requests
import os

GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]

def get_pr_diff(owner: str, repo: str, pr_number: int) -> dict:
    """Fetch PR details and file diffs."""
    headers = {
        "Authorization": f"token {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json",
    }

    # Get PR metadata
    pr_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
    pr_data = requests.get(pr_url, headers=headers).json()

    # Get changed files with patches
    files_url = f"{pr_url}/files"
    files = requests.get(files_url, headers=headers).json()

    return {
        "title": pr_data["title"],
        "description": pr_data.get("body", ""),
        "base_branch": pr_data["base"]["ref"],
        "head_branch": pr_data["head"]["ref"],
        "files": [
            {
                "filename": f["filename"],
                "status": f["status"],  # added, modified, removed
                "patch": f.get("patch", ""),
                "additions": f["additions"],
                "deletions": f["deletions"],
            }
            for f in files
            if f.get("patch")  # Skip binary files
        ]
    }

Analyzing Code Changes with Claude

Send each file's diff to Claude with structured instructions for what to look for:

import anthropic
import json

client = anthropic.Anthropic()

review_tool = {
    "name": "submit_review_comments",
    "description": "Submit code review comments for specific lines in the diff",
    "input_schema": {
        "type": "object",
        "properties": {
            "comments": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "file": {"type": "string", "description": "Filename"},
                        "line": {"type": "integer", "description": "Line number in the diff"},
                        "severity": {
                            "type": "string",
                            "enum": ["critical", "warning", "suggestion", "nitpick"]
                        },
                        "category": {
                            "type": "string",
                            "enum": ["bug", "security", "performance", "style", "logic", "documentation"]
                        },
                        "comment": {"type": "string", "description": "The review comment with explanation"},
                        "suggested_fix": {"type": "string", "description": "Suggested code replacement if applicable"}
                    },
                    "required": ["file", "line", "severity", "category", "comment"]
                }
            },
            "summary": {"type": "string", "description": "Overall review summary"}
        },
        "required": ["comments", "summary"]
    }
}

def review_file(filename: str, patch: str, pr_context: str) -> dict:
    """Review a single file's changes."""
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=4096,
        tools=[review_tool],
        tool_choice={"type": "tool", "name": "submit_review_comments"},
        system="""You are an expert code reviewer. Analyze the diff and provide specific,
actionable feedback. Focus on:
1. Bugs and logic errors (highest priority)
2. Security vulnerabilities (SQL injection, XSS, auth bypasses)
3. Performance issues (N+1 queries, missing indexes, memory leaks)
4. Error handling gaps (uncaught exceptions, missing validation)
5. Code style and readability issues (lowest priority)

Be specific — reference exact line numbers and explain WHY something is an issue,
not just WHAT the issue is. Only comment on changed lines (lines starting with +).
If the code looks good, say so with an empty comments array.""",
        messages=[{
            "role": "user",
            "content": f"PR Context: {pr_context}\n\nFile: {filename}\n\nDiff:\n{patch}"
        }]
    )

    for block in response.content:
        if block.type == "tool_use":
            return block.input
    return {"comments": [], "summary": "No issues found"}

The Complete Review Pipeline

Orchestrate the review across all changed files:

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

def review_pull_request(owner: str, repo: str, pr_number: int) -> dict:
    """Run a complete code review on a pull request."""
    pr_data = get_pr_diff(owner, repo, pr_number)

    pr_context = f"PR Title: {pr_data['title']}\nDescription: {pr_data['description']}"

    all_comments = []
    file_summaries = []

    for file_info in pr_data["files"]:
        if file_info["status"] == "removed":
            continue  # Skip deleted files

        print(f"Reviewing {file_info['filename']}...")
        review = review_file(
            file_info["filename"],
            file_info["patch"],
            pr_context,
        )

        for comment in review.get("comments", []):
            comment["file"] = file_info["filename"]
            all_comments.append(comment)

        file_summaries.append({
            "file": file_info["filename"],
            "summary": review.get("summary", ""),
        })

    # Sort by severity
    severity_order = {"critical": 0, "warning": 1, "suggestion": 2, "nitpick": 3}
    all_comments.sort(key=lambda c: severity_order.get(c["severity"], 99))

    return {
        "pr_number": pr_number,
        "total_comments": len(all_comments),
        "critical_count": sum(1 for c in all_comments if c["severity"] == "critical"),
        "comments": all_comments,
        "file_summaries": file_summaries,
    }

Posting Review Comments to GitHub

Post the agent's findings as a GitHub PR review:

def post_review_to_github(owner: str, repo: str, pr_number: int,
                           review_data: dict, commit_sha: str):
    """Post review comments to GitHub PR."""
    headers = {
        "Authorization": f"token {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json",
    }

    # Build GitHub review comments
    gh_comments = []
    for comment in review_data["comments"]:
        severity_emoji = {
            "critical": "[CRITICAL]",
            "warning": "[WARNING]",
            "suggestion": "[SUGGESTION]",
            "nitpick": "[NITPICK]",
        }
        prefix = severity_emoji.get(comment["severity"], "")
        body = f"**{prefix} {comment['category'].upper()}**\n\n{comment['comment']}"

        if comment.get("suggested_fix"):
            body += f"\n\n**Suggested fix:**\n```suggestion\n{comment['suggested_fix']}\n```"

        gh_comments.append({
            "path": comment["file"],
            "line": comment["line"],
            "body": body,
        })

    # Determine review action based on findings
    if review_data["critical_count"] > 0:
        event = "REQUEST_CHANGES"
    elif review_data["total_comments"] > 0:
        event = "COMMENT"
    else:
        event = "APPROVE"

    # Create the review
    review_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
    review_body = {
        "commit_id": commit_sha,
        "body": generate_review_summary(review_data),
        "event": event,
        "comments": gh_comments,
    }

    response = requests.post(review_url, headers=headers, json=review_body)
    return response.json()

def generate_review_summary(review_data: dict) -> str:
    critical = review_data["critical_count"]
    total = review_data["total_comments"]
    summary = f"## Automated Code Review\n\n"
    summary += f"Found **{total}** issues ({critical} critical).\n\n"

    for fs in review_data["file_summaries"]:
        summary += f"- **{fs['file']}**: {fs['summary']}\n"

    return summary

Running as a GitHub Action

Trigger the review agent on every PR:

# .github/workflows/code-review.yml
name: AI Code Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install anthropic requests
      - run: python scripts/review_pr.py
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REPO_OWNER: ${{ github.repository_owner }}
          REPO_NAME: ${{ github.event.repository.name }}

FAQ

How do I prevent the agent from being too noisy with nitpick comments?

Add a severity filter in your review pipeline — only post comments with severity "critical" or "warning" by default. Store nitpicks separately for developers who want detailed feedback. You can also instruct Claude to limit total comments to the 10 most important findings, forcing it to prioritize.

Can the agent understand context beyond the diff?

Yes. You can fetch the full file content (not just the diff) from GitHub and include it in the prompt. This helps Claude understand the broader code context — what functions the changed code calls, what patterns the rest of the file follows, and whether the changes are consistent with existing style.

How much does it cost to review a typical PR?

A PR with 500 lines changed across 10 files typically uses 30,000-50,000 input tokens and 3,000-5,000 output tokens per file review. With Claude Sonnet, this costs roughly $0.50-$1.50 per PR. Using prompt caching for the system prompt reduces this by 20-30% for subsequent reviews. Batch processing non-urgent reviews saves an additional 50%.


#Claude #CodeReview #GitHub #PullRequests #Python #AgenticAI #LearnAI #AIEngineering

Share this article
C

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.