EzAI
Back to Blog
Tutorial Mar 4, 2026 8 min read

Build an AI GitHub PR Bot with Python and Claude

E

EzAI Team

Build an AI GitHub PR Bot with Python and Claude

Every team has that one pull request that sits open for three days because no one has time to review it. An AI GitHub PR bot fixes that. It reads the diff, flags bugs and security issues, and posts a review comment — all within seconds of the PR being opened. In this tutorial, you'll build one with Python, Flask, and the Claude API through EzAI.

The complete bot is under 80 lines of Python. It catches real issues — SQL injection risks, missing error handling, hardcoded secrets — not just linting nitpicks. And at roughly $0.009 per review through EzAI, it costs less than a cup of coffee per thousand PRs.

How the Bot Works

The architecture is straightforward: GitHub sends a webhook when a PR is opened or updated, your server fetches the diff, sends it to Claude for analysis, and posts the review back as a comment. The entire round-trip takes 2–5 seconds.

PR Bot Architecture — webhook to Claude to GitHub comment flow

PR Bot Architecture — GitHub webhook → your server → Claude API → GitHub comment

You'll need three things: a GitHub personal access token (for posting comments), an EzAI API key (for Claude access), and a server that can receive webhooks. Let's set it up.

Project Setup

Create a new directory and install the dependencies:

bash
mkdir pr-review-bot && cd pr-review-bot
pip install flask anthropic requests

Set your environment variables:

bash
export EZAI_API_KEY="sk-your-ezai-key"
export GITHUB_TOKEN="ghp_your-github-token"

The Review Bot — Core Code

Here's the complete bot. It listens for GitHub webhook events, extracts the PR diff, sends it to Claude Sonnet via EzAI, and posts the review as a comment:

python — bot.py
import os, requests
from flask import Flask, request, jsonify
import anthropic

app = Flask(__name__)

client = anthropic.Anthropic(
    api_key=os.environ["EZAI_API_KEY"],
    base_url="https://ezaiapi.com",
)
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
HEADERS = {"Authorization": f"token {GITHUB_TOKEN}",
           "Accept": "application/vnd.github.v3.diff"}

SYSTEM_PROMPT = """You are a senior code reviewer. Analyze this PR diff and respond with:
1. **Issues** — bugs, security risks, logic errors (if any)
2. **Suggestions** — improvements, cleaner patterns
3. **Verdict** — LGTM ✅ or NEEDS CHANGES ⚠️
Be concise. Skip praise. Only flag things that matter."""

def get_diff(pr_url):
    """Fetch the PR diff from GitHub."""
    resp = requests.get(pr_url, headers=HEADERS, timeout=10)
    resp.raise_for_status()
    # Truncate large diffs to avoid blowing token limits
    return resp.text[:12000]

def review_diff(diff, pr_title):
    """Send diff to Claude via EzAI for review."""
    msg = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=[{
            "role": "user",
            "content": f"PR: {pr_title}\n\nDiff:\n```\n{diff}\n```"
        }]
    )
    return msg.content[0].text

def post_comment(comments_url, body):
    """Post the review as a PR comment."""
    requests.post(
        comments_url,
        json={"body": f"🤖 **AI Review**\n\n{body}"},
        headers={"Authorization": f"token {GITHUB_TOKEN}"},
        timeout=10
    )

@app.route("/webhook", methods=["POST"])
def handle_webhook():
    event = request.headers.get("X-GitHub-Event")
    payload = request.json

    if event != "pull_request":
        return jsonify({"status": "ignored"}), 200
    if payload["action"] not in ("opened", "synchronize"):
        return jsonify({"status": "skipped"}), 200

    pr = payload["pull_request"]
    diff = get_diff(pr["url"])
    review = review_diff(diff, pr["title"])
    post_comment(pr["comments_url"], review)

    return jsonify({"status": "reviewed"}), 200

if __name__ == "__main__":
    app.run(port=5000)

That's the entire bot. The get_diff function fetches the raw diff from GitHub's API. The review_diff function sends it to Claude Sonnet 4.5 via EzAI, and post_comment pushes the review back. Notice how the base_url points to ezaiapi.com — that's the only difference from using Anthropic directly.

Configuring the GitHub Webhook

Go to your repo's Settings → Webhooks → Add webhook and configure:

  • Payload URL: https://your-server.com/webhook
  • Content type: application/json
  • Events: Select "Pull requests" only
  • Secret: Add one for production (we'll cover that below)

For local development, use ngrok to expose your Flask server:

bash
# Terminal 1: start your bot
python bot.py

# Terminal 2: expose it with ngrok
ngrok http 5000

Copy the ngrok URL (e.g., https://abc123.ngrok.io/webhook) into the GitHub webhook config. Open a test PR and watch the bot respond.

Securing the Webhook

In production, always verify the webhook signature to prevent spoofed requests. Add a secret to your webhook config and validate it:

python
import hmac, hashlib

WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]

def verify_signature(payload_body, signature):
    """Verify GitHub webhook HMAC-SHA256 signature."""
    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(), payload_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.before_request
def check_signature():
    sig = request.headers.get("X-Hub-Signature-256", "")
    if not verify_signature(request.data, sig):
        return jsonify({"error": "invalid signature"}), 403

Cost Breakdown

One of the best parts of using EzAI for this: the cost is negligible. A typical PR diff is 300–800 tokens, and Claude's response runs another 400–800. Here's how different models compare per review:

Cost per PR review across AI models

Cost per PR review — Claude Sonnet 4.5 offers the best accuracy-to-cost ratio via EzAI

For most teams, Sonnet 4.5 is the sweet spot. It catches subtle logic bugs that Haiku misses, while costing 80% less than Opus. At $0.009 per review, a team pushing 50 PRs per day spends about $13.50 per month. Compare that to the hours saved on manual reviews. Check EzAI pricing for the latest per-token rates.

Tips for Better Reviews

The system prompt makes or breaks your bot. Here are patterns that produce better reviews:

  • Be specific about what matters: Tell Claude to focus on security, performance, or correctness rather than style. Linting tools handle formatting better.
  • Include file context: If the diff is partial, fetch the full file and include it. Claude catches more bugs with surrounding context.
  • Set severity levels: Ask the model to tag issues as Critical / Warning / Nit so developers know what to fix first.
  • Limit scope: Truncate diffs over 10K tokens. Massive PRs should be reviewed in chunks or flagged for human review.

For more advanced patterns like multi-model fallback (use Opus for large PRs, Haiku for tiny ones), check our guide on model routing.

Deploying to Production

For production, swap Flask's dev server for Gunicorn and deploy behind a reverse proxy:

bash
pip install gunicorn
gunicorn bot:app --workers 2 --bind 0.0.0.0:5000 --timeout 30

If you're on a VPS, a simple systemd unit file keeps it running. On a platform like Railway or Fly.io, just push and it deploys.

What's Next

You've got a working PR review bot. Some ideas to extend it:

  • Inline comments: Use GitHub's Pull Request Reviews API to post comments on specific lines instead of a single summary
  • Auto-approve small PRs: If Claude says LGTM and the diff is under 50 lines, auto-merge with GitHub Actions
  • Slack notifications: Pipe the review to Slack when issues are found (see our Slack bot tutorial)
  • Model routing: Use Haiku for docs-only PRs, Sonnet for code, Opus for security-critical paths — read our model routing guide

The hardest part of code review isn't finding bugs — it's finding the time. An AI bot handles the first pass in seconds, so humans can focus on architecture and design decisions that actually need judgment. Get started with a free EzAI account and have your bot reviewing PRs in the next 10 minutes.


Related Posts