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
pip 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
# 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.
# 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
# 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:
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 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:
# 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):
"6. **Django-specific** — Are views using the ORM safely (parameterised queries, select_related)? "
"Are new views covered by permission_required decorators?"Dependency security:
"7. **Dependencies** — Are any new packages added? Check for known CVEs in common dependency patterns."Performance:
"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_TOKENKey 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.
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
- GitHub REST API: Pull Requests — official reference for the endpoints used to fetch PR diffs and post review comments.
- PyGithub documentation — the Python library used to interact with the GitHub API in this project.
