Build a Code Review Assistant for GitHub PRs with Claude

What Does This Code Review Assistant Do?
This project builds a GitHub PR review assistant that fetches changed files from a pull request, sends each file to Claude with full PR context, and receives structured feedback categorised by severity — critical, high, medium, low. Claude identifies security issues, performance problems, and best-practice violations, then posts the full review as a comment directly on the pull request.
Code review is one of the most valuable practices in software development — and one of the most time-consuming. Experienced reviewers catch bugs, spot security vulnerabilities, enforce best practices, and share knowledge. But reviewers cannot always respond quickly, and even the best reviewers have blind spots.
An AI code review assistant does not replace human reviewers — it supplements them. It provides an immediate first pass on every pull request, catching common issues, flagging security concerns, and giving the author quick feedback before a human reviewer even opens the PR. Human reviewers can then focus their energy on the architecture, design decisions, and domain-specific logic that genuinely requires human judgment.
This project builds a GitHub PR review assistant that analyses changed files, provides structured feedback by category, and posts a review comment directly to the pull request.
What We Are Building
The code review assistant:
- Fetches the PR diff from GitHub using the GitHub REST API
- Analyses each changed file with Claude for bugs, security issues, performance problems, and style violations
- Produces a structured review with severity-labelled findings
- Posts the review as a comment on the GitHub PR
Prerequisites
- Python 3.9 or later
- pip install anthropic requests
- An Anthropic API key set as ANTHROPIC_API_KEY
- A GitHub Personal Access Token with repo scope set as GITHUB_TOKEN
Complete Implementation
import anthropic
import requests
import json
import os
from dataclasses import dataclass
from typing import Optional
anthropic_client = anthropic.Anthropic()
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
# ─── Data Types ───────────────────────────────────────────────────────────────
@dataclass
class PRFile:
filename: str
status: str # added, modified, removed
additions: int
deletions: int
patch: str # The diff content
# ─── GitHub API ───────────────────────────────────────────────────────────────
def get_pr_files(owner: str, repo: str, pr_number: int) -> list[PRFile]:
"""Fetch changed files from a GitHub PR."""
url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}/files"
headers = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json"
}
response = requests.get(url, headers=headers)
response.raise_for_status()
files = []
for file_data in response.json():
files.append(PRFile(
filename=file_data["filename"],
status=file_data["status"],
additions=file_data.get("additions", 0),
deletions=file_data.get("deletions", 0),
patch=file_data.get("patch", "") # May be absent for binary files
))
return files
def get_pr_info(owner: str, repo: str, pr_number: int) -> dict:
"""Fetch PR metadata (title, description, branch)."""
url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pr_number}"
headers = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json"
}
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
def post_pr_comment(owner: str, repo: str, pr_number: int, comment: str) -> None:
"""Post a review comment to the PR."""
url = f"https://api.github.com/repos/{owner}/{repo}/issues/{pr_number}/comments"
headers = {
"Authorization": f"Bearer {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json"
}
response = requests.post(url, headers=headers, json={"body": comment})
response.raise_for_status()
print(f"Review posted to PR #{pr_number}")
# ─── Review Tool Definition ───────────────────────────────────────────────────
CODE_REVIEW_TOOL = {
"name": "create_code_review",
"description": "Create a structured code review for a changed file",
"input_schema": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "1-2 sentence overview of what this file change does"
},
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"severity": {
"type": "string",
"enum": ["critical", "high", "medium", "low", "info"],
"description": "critical=blocking security/data issue, high=likely bug, medium=performance/quality concern, low=style, info=suggestion"
},
"category": {
"type": "string",
"enum": ["security", "bug", "performance", "maintainability", "style", "testing"]
},
"line_reference": {
"type": "string",
"description": "Reference to the relevant line(s) in the diff if applicable"
},
"description": {
"type": "string",
"description": "Clear description of the issue"
},
"suggestion": {
"type": "string",
"description": "Specific, actionable improvement suggestion"
}
},
"required": ["severity", "category", "description", "suggestion"]
},
"description": "List of review findings. Empty array if no issues found."
},
"positive_observations": {
"type": "array",
"items": {"type": "string"},
"description": "Good practices or improvements noticed in this change"
},
"overall_verdict": {
"type": "string",
"enum": ["approve", "request_changes", "comment"],
"description": "approve=no significant issues, request_changes=has critical or high findings, comment=minor feedback only"
}
},
"required": ["summary", "findings", "overall_verdict"]
}
}
# ─── Claude Review Logic ──────────────────────────────────────────────────────
def review_file(pr_file: PRFile, pr_context: dict) -> dict:
"""Use Claude to review a single changed file."""
if not pr_file.patch:
return None # Skip binary files or files with no diff
response = anthropic_client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=[CODE_REVIEW_TOOL],
tool_choice={"type": "tool", "name": "create_code_review"},
system="""You are a thorough and constructive code reviewer. Your goal is to help developers write better code.
Focus on:
1. SECURITY: SQL injection, XSS, authentication bypass, sensitive data exposure, insecure dependencies
2. BUGS: Logic errors, off-by-one errors, null pointer dereferences, race conditions, incorrect error handling
3. PERFORMANCE: N+1 queries, unnecessary loops, missing indexes, memory leaks, blocking operations in async code
4. MAINTAINABILITY: Magic numbers, unclear naming, overly complex functions, missing error handling
5. TESTING: Missing test coverage for new code paths, edge cases not covered
Be specific and constructive. Every finding must have an actionable suggestion.
Do not flag stylistic preferences as high severity. Reserve critical only for genuine security vulnerabilities.
""",
messages=[
{
"role": "user",
"content": f"""Review this code change in the pull request context.
PR: {pr_context.get('title', 'Unknown')}
PR Description: {pr_context.get('body', 'No description')[:500]}
File: {pr_file.filename}
Status: {pr_file.status}
Changes: +{pr_file.additions} lines, -{pr_file.deletions} lines
Diff:
{pr_file.patch}"""
}
]
)
for block in response.content:
if block.type == "tool_use":
return {"filename": pr_file.filename, "review": block.input}
return None
# ─── Report Formatting ────────────────────────────────────────────────────────
def format_pr_review(file_reviews: list[dict], pr_info: dict) -> str:
"""Format all file reviews into a single PR comment."""
all_findings = []
for fr in file_reviews:
if fr and fr.get("review"):
for finding in fr["review"].get("findings", []):
finding["filename"] = fr["filename"]
all_findings.append(finding)
critical_count = sum(1 for f in all_findings if f["severity"] == "critical")
high_count = sum(1 for f in all_findings if f["severity"] == "high")
# Overall verdict
if critical_count > 0:
overall = "🔴 **REQUEST CHANGES** — Critical issues found"
elif high_count > 0:
overall = "🟡 **REQUEST CHANGES** — High severity issues found"
elif all_findings:
overall = "🟢 **APPROVE WITH COMMENTS** — Minor feedback only"
else:
overall = "✅ **APPROVED** — No significant issues found"
lines = [
"## 🤖 AI Code Review",
"",
overall,
"",
f"**Files reviewed:** {len(file_reviews)} | **Total findings:** {len(all_findings)} | **Critical:** {critical_count} | **High:** {high_count}",
"",
"---",
""
]
# Per-file summaries
for fr in file_reviews:
if not fr or not fr.get("review"):
continue
review = fr["review"]
file_findings = review.get("findings", [])
lines.append(f"### `{fr['filename']}`")
lines.append(f"*{review['summary']}*")
if file_findings:
lines.append("")
for finding in file_findings:
emoji = {"critical": "🔴", "high": "🟡", "medium": "🟠", "low": "🔵", "info": "ℹ️"}.get(finding["severity"], "•")
lines.append(f"{emoji} **[{finding['severity'].upper()}][{finding['category']}]** {finding['description']}")
lines.append(f" > 💡 {finding['suggestion']}")
if review.get("positive_observations"):
for obs in review["positive_observations"]:
lines.append(f"✅ {obs}")
lines.append("")
lines.append("---")
lines.append("*This review was generated automatically. Human review is still recommended for architectural and business logic decisions.*")
return "\n".join(lines)
# ─── Main Review Function ─────────────────────────────────────────────────────
def review_pull_request(
owner: str,
repo: str,
pr_number: int,
post_comment: bool = False,
skip_extensions: list = None
) -> str:
"""
Review a GitHub PR and optionally post the review as a comment.
Returns the formatted review as a string.
"""
skip_ext = skip_extensions or [".md", ".txt", ".json", ".lock", ".png", ".jpg", ".svg"]
print(f"Fetching PR #{pr_number} from {owner}/{repo}...")
pr_info = get_pr_info(owner, repo, pr_number)
pr_files = get_pr_files(owner, repo, pr_number)
# Filter to reviewable files
reviewable = [f for f in pr_files if not any(f.filename.endswith(ext) for ext in skip_ext)]
print(f"Reviewing {len(reviewable)} of {len(pr_files)} changed files...")
file_reviews = []
for pr_file in reviewable:
print(f" Reviewing: {pr_file.filename}")
review = review_file(pr_file, pr_info)
if review:
file_reviews.append(review)
formatted_review = format_pr_review(file_reviews, pr_info)
if post_comment:
post_pr_comment(owner, repo, pr_number, formatted_review)
return formatted_review
# ─── Example Usage ────────────────────────────────────────────────────────────
if __name__ == "__main__":
# Review a PR without posting (dry run)
review = review_pull_request(
owner="your-org",
repo="your-repo",
pr_number=42,
post_comment=False # Set to True to post to GitHub
)
print(review)Set post_comment=False During Testing
Always test your review with post_comment=False first. This prints the formatted review to your terminal without posting to GitHub, letting you verify the output quality before it becomes visible to your team. Only set post_comment=True in your production GitHub Actions workflow once you are confident in the review quality.
Deploying as a GitHub Action
Create .github/workflows/ai-review.yml to run automatically on every PR:
name: AI Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install dependencies
run: pip install anthropic requests
- name: Run AI review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python review.py \
--owner ${{ github.repository_owner }} \
--repo ${{ github.event.repository.name }} \
--pr ${{ github.event.pull_request.number }} \
--postSummary
This project demonstrates Claude's ability to reason about code structure and quality across multiple files simultaneously. The critical design elements:
- Structured tool output with severity levels — makes it easy to filter critical vs cosmetic findings
- Context from the PR title and description — Claude understands the intent of the change, not just the code
- Skipping non-reviewable files (documentation, images, lock files) — keeps focus on actual code
- Posting to GitHub — makes the review visible in the existing developer workflow without requiring a separate tool
Next project: Project: Build a Multi-Language Translator App with Claude.
To deploy this as a full automated pipeline, see AI Coding Agents in CI/CD and Build a GitHub PR Review Agent. For the structured output concepts behind the review schema, see Claude Structured Outputs and JSON.
External Resources
- GitHub REST API: Pull Request Reviews — official docs for the API endpoints used to post review comments.
- Anthropic Tool Use documentation — reference for the structured tool schema that powers the review output.
This post is part of the Anthropic AI Tutorial Series. Previous post: Project: Build an Automated Meeting Notes Summariser.
