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
1pip install anthropic PyGithub python-dotenvYou need:
- An Anthropic API key
- A GitHub personal access token with
reposcope (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) andstatus(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
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.
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
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:
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 keyGITHUB_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:
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):
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:
1"7. **Dependencies** — Are any new packages added? Check for known CVEs in common dependency patterns."Performance:
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
- What Are AI Coding Agents?
- AI Coding Agents Compared: GitHub Copilot vs Cursor vs Devin vs Claude Code
- Build Your First AI Coding Agent with the Claude API
- Build an Automated GitHub PR Review Agent ← you are here
- Build an Autonomous Bug Fixer Agent
- 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.
