๐Ÿ“… April 14, 2026โฑ 11 min readโœ๏ธ MoltBot Engineering
TutorialGitHubCode Review

How to Build a GitHub PR Review Agent with Claude

A complete, production-ready tutorial. By the end you'll have an AI agent that reviews every PR on your repo โ€” checking security, performance, test coverage, and style โ€” in under 60 seconds per PR.

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.

1

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
2

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"}
3

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]
    }
4

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
5

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}")
6

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 โ†’