GitHub PR Review Bot with Claude: Step-by-Step (2026)
Build a PR review agent that reads diffs, flags security issues, and posts comments back to GitHub. Runs on every PR via Actions. Python code. 2026.

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.
Part of the Claude AI Masterclass.
