Code review is one of the highest-leverage activities in software development โ and one of the most time-consuming. A well-built PR review agent doesn't replace human review; it surfaces the obvious issues automatically so humans can focus on the subtler architectural decisions.
This tutorial builds a production agent that: receives GitHub webhooks, fetches the PR diff, analyzes it with Claude Opus 4, and posts a structured review comment โ all in under 60 seconds.
Set up your environment
pip install anthropic PyGithub fastapi uvicorn python-dotenv
# .env
ANTHROPIC_API_KEY=sk-ant-xxx
GITHUB_TOKEN=ghp_xxx
GITHUB_WEBHOOK_SECRET=your_webhook_secret
Create the webhook server
from fastapi import FastAPI, Request, HTTPException
import hmac, hashlib, os
from github import Github
app = FastAPI()
gh = Github(os.getenv("GITHUB_TOKEN"))
def verify_signature(payload: bytes, signature: str) -> bool:
"""Verify GitHub webhook HMAC signature."""
secret = os.getenv("GITHUB_WEBHOOK_SECRET", "").encode()
expected = "sha256=" + hmac.new(secret, payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/webhook/github")
async def github_webhook(request: Request):
payload = await request.body()
sig = request.headers.get("X-Hub-Signature-256", "")
if not verify_signature(payload, sig):
raise HTTPException(status_code=401, detail="Invalid signature")
event = request.headers.get("X-GitHub-Event")
data = await request.json()
if event == "pull_request" and data["action"] in ["opened", "synchronize"]:
pr_number = data["pull_request"]["number"]
repo_name = data["repository"]["full_name"]
await review_pr(repo_name, pr_number)
return {"status": "ok"}
Fetch the PR diff
async def get_pr_diff(repo_name: str, pr_number: int) -> dict:
"""Fetch PR metadata and diff from GitHub."""
repo = gh.get_repo(repo_name)
pr = repo.get_pull(pr_number)
# Get diff (limit to 8000 chars to fit in context)
files = list(pr.get_files())
diff_text = ""
for f in files[:20]: # Max 20 files
diff_text += f"\n--- {f.filename} (+{f.additions}/-{f.deletions}) ---\n"
if f.patch:
diff_text += f.patch[:2000] # Max 2000 chars per file
return {
"title": pr.title,
"body": pr.body or "",
"author": pr.user.login,
"files_changed": len(files),
"additions": pr.additions,
"deletions": pr.deletions,
"diff": diff_text[:8000]
}
Build the review agent
import anthropic
client = anthropic.Anthropic()
REVIEW_PROMPT = """You are a senior software engineer performing a thorough code review.
Analyze this pull request and provide a structured review covering:
1. **Security** โ SQL injection, XSS, auth issues, exposed secrets
2. **Performance** โ N+1 queries, missing indexes, inefficient algorithms
3. **Test Coverage** โ Missing tests for edge cases, untested branches
4. **Code Quality** โ Readability, naming, DRY violations
5. **Summary** โ Overall assessment: APPROVE / REQUEST_CHANGES / COMMENT
Format your review as Markdown. Be specific โ quote exact lines when noting issues.
Be constructive, not harsh. Acknowledge good patterns you see."""
async def run_review_agent(pr_data: dict) -> str:
"""Run Claude on the PR diff and return review text."""
message = client.messages.create(
model="claude-opus-4-5",
max_tokens=2048,
system=REVIEW_PROMPT,
messages=[{
"role": "user",
"content": f"""PR: {pr_data['title']}
Description: {pr_data['body']}
Files changed: {pr_data['files_changed']} (+{pr_data['additions']}/-{pr_data['deletions']})
DIFF:
{pr_data['diff']}"""
}]
)
return message.content[0].text
Post the review comment
async def review_pr(repo_name: str, pr_number: int):
"""Full pipeline: fetch PR โ analyze โ post review."""
pr_data = await get_pr_diff(repo_name, pr_number)
review_text = await run_review_agent(pr_data)
# Post as GitHub PR review
repo = gh.get_repo(repo_name)
pr = repo.get_pull(pr_number)
# Determine approval action from review text
if "APPROVE" in review_text:
event = "APPROVE"
elif "REQUEST_CHANGES" in review_text:
event = "REQUEST_CHANGES"
else:
event = "COMMENT"
pr.create_review(body=review_text, event=event)
print(f"โ Reviewed PR #{pr_number}: {event}")
Deploy and configure the webhook
# Start server
uvicorn main:app --host 0.0.0.0 --port 8000
# Or deploy to MoltBot (persistent, always-on):
moltbot deploy --file main.py --port 8000 --name pr-review-agent
In your GitHub repo: Settings โ Webhooks โ Add webhook. Set the URL to your server's address + /webhook/github, select application/json, set the secret, and choose "Pull requests" events.
โ Run this on MoltBot for free
Deploy this agent to a MoltBot instance for always-on, persistent webhook handling. No cold starts, no server management. Start your 14-day free trial and deploy in 5 minutes.
Ship your PR review agent today
MoltBot provides the persistent GPU infrastructure to run your agent 24/7. Deploy in 5 minutes.
Start Free Trial โ