Artificial IntelligenceSoftware DevelopmentProjects

Build an Automated GitHub PR Review Agent with Claude

TT
TopicTrick Team
Build an Automated GitHub PR Review Agent with Claude

What Does This PR Review Agent Do?

This project builds a GitHub PR review agent that fetches pull request diffs via the GitHub API, sends each changed file to Claude with a structured review prompt, receives JSON-formatted findings categorised by severity, and posts a formatted review comment directly to the pull request — automatically triggered by GitHub Actions on every PR open or update.

Code review is one of the highest-leverage activities in software development — and one of the most time-consuming. Every PR that sits unreviewed blocks a developer. Every review that misses a security issue costs tenfold later. Most teams are perpetually behind on review.

A PR review agent does not replace your senior engineer's judgment on architecture and design. But it can reliably handle the first pass: checking for common bugs, security issues, missing tests, style violations, and documentation gaps — before a human even opens the diff. That frees human reviewers to focus on the things that cannot be automated.

In this project you will build a complete GitHub PR review agent that:

  • Reads a pull request diff from GitHub
  • Analyses it across five dimensions: functionality, security, test coverage, code quality, and documentation
  • Posts structured review comments back to the PR via the GitHub API
  • Runs automatically on every new PR via GitHub Actions

Prerequisites

bash
pip install anthropic PyGithub python-dotenv

You need:

  • An Anthropic API key
  • A GitHub personal access token with repo scope (or a GitHub App for production)
  • A GitHub repository to test against

How GitHub PR Reviews Work via API

Before building the agent, understand the GitHub objects involved:

  • Pull Request: has a title, description, base branch, and head branch
  • Files changed: each file has a patch (unified diff) and status (added/modified/removed)
  • Review: a top-level review object with overall verdict: APPROVE, REQUEST_CHANGES, or COMMENT
  • Review comments: inline comments attached to specific lines in the diff
  • Issue comments: general comments on the PR conversation thread

The agent reads the diff, passes it to Claude, Claude returns structured feedback, and the agent posts that feedback as a PR review with inline comments where appropriate.


Step 1: GitHub Client

python
# pr_reviewer/github_client.py
from dataclasses import dataclass
from github import Github
from github.PullRequest import PullRequest


@dataclass
class PRFile:
    filename: str
    status: str        # added, modified, removed, renamed
    additions: int
    deletions: int
    patch: str | None  # unified diff — None for binary files


@dataclass
class PRContext:
    number: int
    title: str
    description: str
    author: str
    base_branch: str
    head_branch: str
    files: list[PRFile]


class GitHubClient:
    def __init__(self, token: str, repo_name: str):
        self.gh = Github(token)
        self.repo = self.gh.get_repo(repo_name)

    def get_pr_context(self, pr_number: int) -> PRContext:
        pr: PullRequest = self.repo.get_pull(pr_number)

        files = []
        for f in pr.get_files():
            files.append(PRFile(
                filename=f.filename,
                status=f.status,
                additions=f.additions,
                deletions=f.deletions,
                patch=f.patch if hasattr(f, "patch") else None
            ))

        return PRContext(
            number=pr.number,
            title=pr.title,
            description=pr.body or "",
            author=pr.user.login,
            base_branch=pr.base.ref,
            head_branch=pr.head.ref,
            files=files,
        )

    def post_review(
        self,
        pr_number: int,
        body: str,
        verdict: str,  # "APPROVE", "REQUEST_CHANGES", "COMMENT"
        inline_comments: list[dict] | None = None,
    ) -> None:
        """
        Post a review on the PR.
        inline_comments: list of {'path': str, 'line': int, 'body': str}
        """
        pr = self.repo.get_pull(pr_number)
        comments = inline_comments or []

        # GitHub API requires position (line in diff) not line number.
        # For simplicity this implementation posts top-level comments.
        # See Step 4 for inline comment positioning.
        pr.create_review(body=body, event=verdict)

    def post_comment(self, pr_number: int, body: str) -> None:
        """Post a general conversation comment on the PR."""
        issue = self.repo.get_issue(pr_number)
        issue.create_comment(body)

Step 2: Build the Review Prompt

The key to a good review agent is a well-structured prompt. You want Claude to produce consistent, actionable output that maps cleanly to GitHub's review format.

python
# pr_reviewer/prompt.py

def build_review_prompt(pr: "PRContext") -> str:
    """Build the review prompt from PR context."""

    # Summarise the diff — cap per-file patch length to avoid overflow
    MAX_PATCH_CHARS = 3000
    file_sections = []

    for f in pr.files:
        if f.patch is None:
            file_sections.append(f"### {f.filename} ({f.status}) — binary file, skipped")
            continue

        patch = f.patch
        if len(patch) > MAX_PATCH_CHARS:
            patch = patch[:MAX_PATCH_CHARS] + f"\n... [patch truncated — {len(f.patch)} chars total]"

        file_sections.append(
            f"### {f.filename} ({f.status}, +{f.additions} -{f.deletions})\n"
            f"```diff\n{patch}\n```"
        )

    files_content = "\n\n".join(file_sections)

    return f"""You are a senior software engineer conducting a code review.

## Pull Request: #{pr.number} — {pr.title}

**Author:** {pr.author}
**Branch:** {pr.head_branch} → {pr.base_branch}

**Description:**
{pr.description or '(no description provided)'}

## Files Changed

{files_content}

---

Provide a thorough code review covering these five dimensions:

1. **Functionality** — Does the code do what it claims? Any obvious bugs or logic errors?
2. **Security** — SQL injection, XSS, insecure deserialization, hardcoded secrets, missing input validation, auth/authz issues
3. **Test Coverage** — Are new code paths tested? Are edge cases covered? Any tests that need to be added?
4. **Code Quality** — Naming, readability, complexity, duplication, adherence to existing patterns
5. **Documentation** — Missing docstrings, README updates needed, unclear variable names

## Output Format

Respond with a JSON object with this exact structure:
{{
  "verdict": "APPROVE" | "REQUEST_CHANGES" | "COMMENT",
  "summary": "2-4 sentence overall summary of the PR and your assessment",
  "highlights": ["thing that was done well 1", "thing done well 2"],
  "issues": [
    {{
      "severity": "critical" | "major" | "minor" | "suggestion",
      "category": "security" | "functionality" | "tests" | "quality" | "documentation",
      "file": "filename or null if general",
      "description": "Clear description of the issue",
      "suggestion": "Specific fix or improvement recommendation"
    }}
  ],
  "overall_comment": "Full markdown-formatted review body suitable for posting to GitHub"
}}

Verdict rules:
- APPROVE: no critical or major issues
- REQUEST_CHANGES: any critical issue, or multiple major issues
- COMMENT: minor issues only, or no issues but useful feedback to share

Return only the JSON object — no markdown fence, no preamble."""

Step 3: The Review Agent

python
# pr_reviewer/agent.py
import json
import anthropic
from .github_client import GitHubClient, PRContext
from .prompt import build_review_prompt


class PRReviewAgent:
    def __init__(
        self,
        github_token: str,
        repo_name: str,
        model: str = "claude-sonnet-4-6",
    ):
        self.gh = GitHubClient(github_token, repo_name)
        self.claude = anthropic.Anthropic()
        self.model = model

    def review_pr(self, pr_number: int, post_to_github: bool = True) -> dict:
        """
        Review a pull request.
        Returns the structured review dict.
        If post_to_github=True, posts the review to GitHub.
        """
        print(f"Fetching PR #{pr_number}...")
        pr_context = self.gh.get_pr_context(pr_number)
        print(f"  → {len(pr_context.files)} files changed")

        # Skip PRs that are too large
        total_changes = sum(f.additions + f.deletions for f in pr_context.files)
        if total_changes > 5000:
            print(f"  → Skipping: {total_changes} lines changed exceeds the 5000-line limit")
            return {"verdict": "COMMENT", "summary": "PR too large for automated review."}

        print("Calling Claude for review...")
        prompt = build_review_prompt(pr_context)

        response = self.claude.messages.create(
            model=self.model,
            max_tokens=4096,
            messages=[{"role": "user", "content": prompt}]
        )

        raw = response.content[0].text.strip()

        # Parse the JSON response
        try:
            review = json.loads(raw)
        except json.JSONDecodeError:
            # Claude returned something other than pure JSON — extract it
            import re
            match = re.search(r'\{.*\}', raw, re.DOTALL)
            if match:
                review = json.loads(match.group())
            else:
                raise ValueError(f"Could not parse Claude's response as JSON:\n{raw[:500]}")

        print(f"  → Verdict: {review['verdict']}")
        print(f"  → Issues: {len(review.get('issues', []))}")

        if post_to_github:
            self._post_review(pr_number, review)

        return review

    def _post_review(self, pr_number: int, review: dict) -> None:
        """Format and post the review to GitHub."""
        body = self._format_review_body(review)
        verdict = review.get("verdict", "COMMENT")

        self.gh.post_review(
            pr_number=pr_number,
            body=body,
            verdict=verdict,
        )
        print(f"  → Posted {verdict} review to PR #{pr_number}")

    def _format_review_body(self, review: dict) -> str:
        """Convert the review dict into a formatted GitHub review body."""
        lines = []

        # Summary
        lines.append(f"## AI Code Review\n")
        lines.append(f"{review.get('summary', '')}\n")

        # Highlights
        highlights = review.get("highlights", [])
        if highlights:
            lines.append("### What's Good")
            for h in highlights:
                lines.append(f"- ✅ {h}")
            lines.append("")

        # Issues by severity
        issues = review.get("issues", [])
        if issues:
            lines.append("### Issues Found\n")

            severity_icons = {
                "critical": "🚨 **Critical**",
                "major": "⚠️ **Major**",
                "minor": "💡 Minor",
                "suggestion": "💬 Suggestion",
            }

            for severity in ["critical", "major", "minor", "suggestion"]:
                severity_issues = [i for i in issues if i.get("severity") == severity]
                for issue in severity_issues:
                    icon = severity_icons.get(severity, "•")
                    file_ref = f" (`{issue['file']}`)" if issue.get("file") else ""
                    lines.append(f"**{icon} — {issue.get('category', '').title()}{file_ref}**")
                    lines.append(f"{issue.get('description', '')}")
                    if issue.get("suggestion"):
                        lines.append(f"> 💡 **Suggestion:** {issue['suggestion']}")
                    lines.append("")

        # Footer
        lines.append("---")
        lines.append("*This review was generated by an AI coding agent powered by Claude. "
                     "It covers common code quality, security, and testing concerns. "
                     "Human review is still required before merging.*")

        return "\n".join(lines)

Step 4: GitHub Actions Integration

Create .github/workflows/ai-pr-review.yml in your repository to run the agent automatically on every PR:

yaml
name: AI PR Review

on:
  pull_request:
    types: [opened, synchronize]
    # Optionally restrict to specific branches
    # branches: [main, develop]

jobs:
  ai-review:
    runs-on: ubuntu-latest
    # Only run on PRs from branches (not forks, for security)
    if: github.event.pull_request.head.repo.full_name == github.repository

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: pip install anthropic PyGithub

      - name: Run AI PR 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 -c "
          import os
          from pr_reviewer.agent import PRReviewAgent

          agent = PRReviewAgent(
              github_token=os.environ['GITHUB_TOKEN'],
              repo_name=os.environ['REPO_NAME'],
          )
          agent.review_pr(int(os.environ['PR_NUMBER']), post_to_github=True)
          "

Add your secrets in GitHub repository settings:

  • ANTHROPIC_API_KEY: your Anthropic API key
  • GITHUB_TOKEN: this is automatically provided by GitHub Actions — no manual setup needed

Fork PR Security

The GitHub Actions workflow above includes `if: github.event.pull_request.head.repo.full_name == github.repository` to prevent external forks from triggering the workflow and potentially leaking your API key or abusing your quota. Never remove this check for public repositories.


    Step 5: Local Testing

    Test the agent locally before deploying to GitHub Actions:

    python
    # test_review.py
    import os
    from dotenv import load_dotenv
    from pr_reviewer.agent import PRReviewAgent
    
    load_dotenv()
    
    agent = PRReviewAgent(
        github_token=os.environ["GITHUB_TOKEN"],
        repo_name="your-org/your-repo",  # replace with your repo
    )
    
    # Dry run — don't post to GitHub yet
    review = agent.review_pr(pr_number=42, post_to_github=False)
    
    print(f"\nVerdict: {review['verdict']}")
    print(f"Summary: {review['summary']}")
    print(f"\nIssues ({len(review['issues'])}):")
    for issue in review["issues"]:
        print(f"  [{issue['severity'].upper()}] {issue['category']}: {issue['description'][:100]}")
    
    print("\n--- Formatted Review Body ---")
    print(agent._format_review_body(review))

    Customisation: Adding Review Focus Areas

    You can extend the agent to focus on your team's specific concerns by modifying the prompt. Common additions:

    Framework-specific checks (e.g., Django):

    python
    "6. **Django-specific** — Are views using the ORM safely (parameterised queries, select_related)? "
    "Are new views covered by permission_required decorators?"

    Dependency security:

    python
    "7. **Dependencies** — Are any new packages added? Check for known CVEs in common dependency patterns."

    Performance:

    python
    "8. **Performance** — Any N+1 query patterns? Expensive operations in hot paths? Missing database indexes for new filter conditions?"

    Project File Structure

    text
    pr_reviewer/
    ├── __init__.py
    ├── github_client.py   ← GitHub API wrapper
    ├── prompt.py          ← Review prompt builder
    └── agent.py           ← Review agent orchestrator
    
    .github/
    └── workflows/
        └── ai-pr-review.yml   ← GitHub Actions workflow
    
    test_review.py             ← Local test script
    .env                       ← ANTHROPIC_API_KEY, GITHUB_TOKEN

    Key Takeaways

    • A PR review agent reads the unified diff from GitHub's API and passes it to Claude with a structured review prompt
    • Using a JSON output format from Claude makes it straightforward to parse the review and format it for different output targets
    • Always truncate large diffs — PR review quality drops on diffs over 5,000 lines and costs escalate fast
    • GitHub Actions integration makes this zero-maintenance — the agent runs on every PR automatically
    • Block fork PRs from triggering the action to prevent API key abuse
    • The agent is most valuable on security, tests, and code quality — invest in customising the prompt for your stack and team conventions

    What's Next in the AI Coding Agents Series

    1. What Are AI Coding Agents?
    2. AI Coding Agents Compared: GitHub Copilot vs Cursor vs Devin vs Claude Code
    3. Build Your First AI Coding Agent with the Claude API
    4. Build an Automated GitHub PR Review Agent ← you are here
    5. Build an Autonomous Bug Fixer Agent
    6. AI Coding Agents in CI/CD: Automate Code Reviews and Fixes in Production

    This post is part of the AI Coding Agents Series. Previous post: Build Your First AI Coding Agent with the Claude API.

    To deploy this agent in a full CI/CD pipeline with cost controls and human approval gates, see AI Coding Agents in CI/CD. For a simplified standalone code review assistant (without the full agentic loop), see Build a Code Review Assistant for GitHub PRs.

    External Resources