Artificial IntelligenceSoftware DevelopmentProjects

Build an Automated GitHub PR Review Agent with Claude

TT
TopicTrick
Build an Automated GitHub PR Review Agent with Claude

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
1pip 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
1# pr_reviewer/github_client.py 2from dataclasses import dataclass 3from github import Github 4from github.PullRequest import PullRequest 5 6 7@dataclass 8class PRFile: 9 filename: str 10 status: str # added, modified, removed, renamed 11 additions: int 12 deletions: int 13 patch: str | None # unified diff — None for binary files 14 15 16@dataclass 17class PRContext: 18 number: int 19 title: str 20 description: str 21 author: str 22 base_branch: str 23 head_branch: str 24 files: list[PRFile] 25 26 27class GitHubClient: 28 def __init__(self, token: str, repo_name: str): 29 self.gh = Github(token) 30 self.repo = self.gh.get_repo(repo_name) 31 32 def get_pr_context(self, pr_number: int) -> PRContext: 33 pr: PullRequest = self.repo.get_pull(pr_number) 34 35 files = [] 36 for f in pr.get_files(): 37 files.append(PRFile( 38 filename=f.filename, 39 status=f.status, 40 additions=f.additions, 41 deletions=f.deletions, 42 patch=f.patch if hasattr(f, "patch") else None 43 )) 44 45 return PRContext( 46 number=pr.number, 47 title=pr.title, 48 description=pr.body or "", 49 author=pr.user.login, 50 base_branch=pr.base.ref, 51 head_branch=pr.head.ref, 52 files=files, 53 ) 54 55 def post_review( 56 self, 57 pr_number: int, 58 body: str, 59 verdict: str, # "APPROVE", "REQUEST_CHANGES", "COMMENT" 60 inline_comments: list[dict] | None = None, 61 ) -> None: 62 """ 63 Post a review on the PR. 64 inline_comments: list of {'path': str, 'line': int, 'body': str} 65 """ 66 pr = self.repo.get_pull(pr_number) 67 comments = inline_comments or [] 68 69 # GitHub API requires position (line in diff) not line number. 70 # For simplicity this implementation posts top-level comments. 71 # See Step 4 for inline comment positioning. 72 pr.create_review(body=body, event=verdict) 73 74 def post_comment(self, pr_number: int, body: str) -> None: 75 """Post a general conversation comment on the PR.""" 76 issue = self.repo.get_issue(pr_number) 77 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
1# pr_reviewer/prompt.py 2 3def build_review_prompt(pr: "PRContext") -> str: 4 """Build the review prompt from PR context.""" 5 6 # Summarise the diff — cap per-file patch length to avoid overflow 7 MAX_PATCH_CHARS = 3000 8 file_sections = [] 9 10 for f in pr.files: 11 if f.patch is None: 12 file_sections.append(f"### {f.filename} ({f.status}) — binary file, skipped") 13 continue 14 15 patch = f.patch 16 if len(patch) > MAX_PATCH_CHARS: 17 patch = patch[:MAX_PATCH_CHARS] + f"\n... [patch truncated — {len(f.patch)} chars total]" 18 19 file_sections.append( 20 f"### {f.filename} ({f.status}, +{f.additions} -{f.deletions})\n" 21 f"```diff\n{patch}\n```" 22 ) 23 24 files_content = "\n\n".join(file_sections) 25 26 return f"""You are a senior software engineer conducting a code review. 27 28## Pull Request: #{pr.number}{pr.title} 29 30**Author:** {pr.author} 31**Branch:** {pr.head_branch}{pr.base_branch} 32 33**Description:** 34{pr.description or '(no description provided)'} 35 36## Files Changed 37 38{files_content} 39 40--- 41 42Provide a thorough code review covering these five dimensions: 43 441. **Functionality** — Does the code do what it claims? Any obvious bugs or logic errors? 452. **Security** — SQL injection, XSS, insecure deserialization, hardcoded secrets, missing input validation, auth/authz issues 463. **Test Coverage** — Are new code paths tested? Are edge cases covered? Any tests that need to be added? 474. **Code Quality** — Naming, readability, complexity, duplication, adherence to existing patterns 485. **Documentation** — Missing docstrings, README updates needed, unclear variable names 49 50## Output Format 51 52Respond with a JSON object with this exact structure: 53{{ 54 "verdict": "APPROVE" | "REQUEST_CHANGES" | "COMMENT", 55 "summary": "2-4 sentence overall summary of the PR and your assessment", 56 "highlights": ["thing that was done well 1", "thing done well 2"], 57 "issues": [ 58 {{ 59 "severity": "critical" | "major" | "minor" | "suggestion", 60 "category": "security" | "functionality" | "tests" | "quality" | "documentation", 61 "file": "filename or null if general", 62 "description": "Clear description of the issue", 63 "suggestion": "Specific fix or improvement recommendation" 64 }} 65 ], 66 "overall_comment": "Full markdown-formatted review body suitable for posting to GitHub" 67}} 68 69Verdict rules: 70- APPROVE: no critical or major issues 71- REQUEST_CHANGES: any critical issue, or multiple major issues 72- COMMENT: minor issues only, or no issues but useful feedback to share 73 74Return only the JSON object — no markdown fence, no preamble."""

Step 3: The Review Agent

python
1# pr_reviewer/agent.py 2import json 3import anthropic 4from .github_client import GitHubClient, PRContext 5from .prompt import build_review_prompt 6 7 8class PRReviewAgent: 9 def __init__( 10 self, 11 github_token: str, 12 repo_name: str, 13 model: str = "claude-sonnet-4-6", 14 ): 15 self.gh = GitHubClient(github_token, repo_name) 16 self.claude = anthropic.Anthropic() 17 self.model = model 18 19 def review_pr(self, pr_number: int, post_to_github: bool = True) -> dict: 20 """ 21 Review a pull request. 22 Returns the structured review dict. 23 If post_to_github=True, posts the review to GitHub. 24 """ 25 print(f"Fetching PR #{pr_number}...") 26 pr_context = self.gh.get_pr_context(pr_number) 27 print(f" → {len(pr_context.files)} files changed") 28 29 # Skip PRs that are too large 30 total_changes = sum(f.additions + f.deletions for f in pr_context.files) 31 if total_changes > 5000: 32 print(f" → Skipping: {total_changes} lines changed exceeds the 5000-line limit") 33 return {"verdict": "COMMENT", "summary": "PR too large for automated review."} 34 35 print("Calling Claude for review...") 36 prompt = build_review_prompt(pr_context) 37 38 response = self.claude.messages.create( 39 model=self.model, 40 max_tokens=4096, 41 messages=[{"role": "user", "content": prompt}] 42 ) 43 44 raw = response.content[0].text.strip() 45 46 # Parse the JSON response 47 try: 48 review = json.loads(raw) 49 except json.JSONDecodeError: 50 # Claude returned something other than pure JSON — extract it 51 import re 52 match = re.search(r'\{.*\}', raw, re.DOTALL) 53 if match: 54 review = json.loads(match.group()) 55 else: 56 raise ValueError(f"Could not parse Claude's response as JSON:\n{raw[:500]}") 57 58 print(f" → Verdict: {review['verdict']}") 59 print(f" → Issues: {len(review.get('issues', []))}") 60 61 if post_to_github: 62 self._post_review(pr_number, review) 63 64 return review 65 66 def _post_review(self, pr_number: int, review: dict) -> None: 67 """Format and post the review to GitHub.""" 68 body = self._format_review_body(review) 69 verdict = review.get("verdict", "COMMENT") 70 71 self.gh.post_review( 72 pr_number=pr_number, 73 body=body, 74 verdict=verdict, 75 ) 76 print(f" → Posted {verdict} review to PR #{pr_number}") 77 78 def _format_review_body(self, review: dict) -> str: 79 """Convert the review dict into a formatted GitHub review body.""" 80 lines = [] 81 82 # Summary 83 lines.append(f"## AI Code Review\n") 84 lines.append(f"{review.get('summary', '')}\n") 85 86 # Highlights 87 highlights = review.get("highlights", []) 88 if highlights: 89 lines.append("### What's Good") 90 for h in highlights: 91 lines.append(f"- ✅ {h}") 92 lines.append("") 93 94 # Issues by severity 95 issues = review.get("issues", []) 96 if issues: 97 lines.append("### Issues Found\n") 98 99 severity_icons = { 100 "critical": "🚨 **Critical**", 101 "major": "⚠️ **Major**", 102 "minor": "💡 Minor", 103 "suggestion": "💬 Suggestion", 104 } 105 106 for severity in ["critical", "major", "minor", "suggestion"]: 107 severity_issues = [i for i in issues if i.get("severity") == severity] 108 for issue in severity_issues: 109 icon = severity_icons.get(severity, "•") 110 file_ref = f" (`{issue['file']}`)" if issue.get("file") else "" 111 lines.append(f"**{icon}{issue.get('category', '').title()}{file_ref}**") 112 lines.append(f"{issue.get('description', '')}") 113 if issue.get("suggestion"): 114 lines.append(f"> 💡 **Suggestion:** {issue['suggestion']}") 115 lines.append("") 116 117 # Footer 118 lines.append("---") 119 lines.append("*This review was generated by an AI coding agent powered by Claude. " 120 "It covers common code quality, security, and testing concerns. " 121 "Human review is still required before merging.*") 122 123 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
1name: AI PR Review 2 3on: 4 pull_request: 5 types: [opened, synchronize] 6 # Optionally restrict to specific branches 7 # branches: [main, develop] 8 9jobs: 10 ai-review: 11 runs-on: ubuntu-latest 12 # Only run on PRs from branches (not forks, for security) 13 if: github.event.pull_request.head.repo.full_name == github.repository 14 15 steps: 16 - name: Checkout 17 uses: actions/checkout@v4 18 19 - name: Set up Python 20 uses: actions/setup-python@v5 21 with: 22 python-version: "3.11" 23 24 - name: Install dependencies 25 run: pip install anthropic PyGithub 26 27 - name: Run AI PR Review 28 env: 29 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} 30 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 PR_NUMBER: ${{ github.event.pull_request.number }} 32 REPO_NAME: ${{ github.repository }} 33 run: | 34 python -c " 35 import os 36 from pr_reviewer.agent import PRReviewAgent 37 38 agent = PRReviewAgent( 39 github_token=os.environ['GITHUB_TOKEN'], 40 repo_name=os.environ['REPO_NAME'], 41 ) 42 agent.review_pr(int(os.environ['PR_NUMBER']), post_to_github=True) 43 "

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
    1# test_review.py 2import os 3from dotenv import load_dotenv 4from pr_reviewer.agent import PRReviewAgent 5 6load_dotenv() 7 8agent = PRReviewAgent( 9 github_token=os.environ["GITHUB_TOKEN"], 10 repo_name="your-org/your-repo", # replace with your repo 11) 12 13# Dry run — don't post to GitHub yet 14review = agent.review_pr(pr_number=42, post_to_github=False) 15 16print(f"\nVerdict: {review['verdict']}") 17print(f"Summary: {review['summary']}") 18print(f"\nIssues ({len(review['issues'])}):") 19for issue in review["issues"]: 20 print(f" [{issue['severity'].upper()}] {issue['category']}: {issue['description'][:100]}") 21 22print("\n--- Formatted Review Body ---") 23print(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
    1"6. **Django-specific** — Are views using the ORM safely (parameterised queries, select_related)? " 2"Are new views covered by permission_required decorators?"

    Dependency security:

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

    Performance:

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

    Project File Structure

    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.