By Sagar Shankaran, Founder of CallSphere
Step-by-step guide to building an automated code review bot using the Claude API. Covers GitHub integration, diff analysis, security scanning, style enforcement, and delivering actionable feedback on pull requests.
Key takeaways
Manual code review is a bottleneck in every engineering team. Senior engineers spend 5-10 hours per week reviewing pull requests. Reviews are inconsistent -- what one reviewer catches, another misses. And review latency delays merges, slowing the entire development cycle.
An AI code review bot does not replace human reviewers. It augments them by catching the mechanical issues (bugs, security vulnerabilities, style violations, missing tests) so that human reviewers can focus on architecture, design, and business logic.
The system has four components:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent for IT support in your browser — 60 seconds, no signup.
flowchart LR
USER(["User message"])
LOOP{"messages.create<br/>agent loop"}
THINK["Extended thinking<br/>optional"]
TOOL{"stop_reason<br/>tool_use?"}
EXEC["Execute tool<br/>append tool_result"]
DONE(["stop_reason<br/>end_turn"])
USER --> LOOP --> THINK --> TOOL
TOOL -->|Yes| EXEC --> LOOP
TOOL -->|No| DONE
style LOOP fill:#4f46e5,stroke:#4338ca,color:#fff
style THINK fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style DONE fill:#059669,stroke:#047857,color:#fff
GitHub PR Event -> Webhook -> Diff Analyzer -> Claude API -> GitHub Comments
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import os
app = FastAPI()
GITHUB_WEBHOOK_SECRET = os.environ["GITHUB_WEBHOOK_SECRET"]
@app.post("/webhook/github")
async def handle_github_webhook(request: Request):
# Verify webhook signature
signature = request.headers.get("X-Hub-Signature-256", "")
body = await request.body()
expected = "sha256=" + hmac.new(
GITHUB_WEBHOOK_SECRET.encode(),
body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
raise HTTPException(status_code=403, detail="Invalid signature")
payload = await request.json()
event_type = request.headers.get("X-GitHub-Event")
if event_type == "pull_request" and payload["action"] in ("opened", "synchronize"):
await review_pull_request(
repo=payload["repository"]["full_name"],
pr_number=payload["pull_request"]["number"],
base_sha=payload["pull_request"]["base"]["sha"],
head_sha=payload["pull_request"]["head"]["sha"],
)
return {"status": "ok"}
import httpx
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
async def get_pr_diff(repo: str, pr_number: int) -> list[dict]:
"""Fetch the PR diff and parse it into structured file changes."""
async with httpx.AsyncClient() as client:
# Get list of changed files
response = await client.get(
f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files",
headers={
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json",
}
)
files = response.json()
changes = []
for file in files:
if file["status"] == "removed":
continue # Skip deleted files
changes.append({
"filename": file["filename"],
"status": file["status"], # added, modified, renamed
"additions": file["additions"],
"deletions": file["deletions"],
"patch": file.get("patch", ""), # The actual diff
"language": detect_language(file["filename"]),
})
return changes
def detect_language(filename: str) -> str:
ext_map = {
".py": "python", ".ts": "typescript", ".tsx": "typescript",
".js": "javascript", ".jsx": "javascript", ".go": "go",
".rs": "rust", ".java": "java", ".rb": "ruby",
}
for ext, lang in ext_map.items():
if filename.endswith(ext):
return lang
return "unknown"
This is the core of the system. We send each file's diff to Claude with specialized review instructions.
from anthropic import Anthropic
import json
client = Anthropic()
REVIEW_SYSTEM_PROMPT = """You are an expert code reviewer. For each code diff provided,
analyze the changes and identify:
1. **Bugs**: Logic errors, off-by-one errors, null pointer issues, race conditions
2. **Security**: SQL injection, XSS, auth bypasses, secrets exposure, input validation
3. **Performance**: N+1 queries, unnecessary allocations, missing indexes, O(n^2) algorithms
4. **Style**: Naming conventions, code organization, readability
5. **Missing tests**: New logic paths that lack test coverage
For each issue found, provide:
- severity: "critical", "warning", or "suggestion"
- line: the line number in the diff (from the + side)
- description: clear explanation of the issue
- suggestion: specific code fix when possible
Return your review as a JSON array of issues. If the code looks good, return an empty array.
Do NOT fabricate issues -- only report genuine problems."""
async def review_file(filename: str, patch: str, language: str) -> list[dict]:
"""Review a single file's changes."""
if not patch or len(patch) < 10:
return []
response = client.messages.create(
model="claude-sonnet-4-5-20250514",
max_tokens=4096,
system=REVIEW_SYSTEM_PROMPT,
messages=[{
"role": "user",
"content": f"""Review this {language} code change in {filename}:
```diff
{patch}
Return your findings as a JSON array.""" }] )
try:
# Extract JSON from the response
text = response.content[0].text
# Handle markdown code blocks in response
if "```json" in text:
text = text.split("```json")[1].split("```")[0]
elif "```" in text:
text = text.split("```")[1].split("```")[0]
return json.loads(text)
except (json.JSONDecodeError, IndexError):
return []
async def review_pull_request(repo: str, pr_number: int, base_sha: str, head_sha: str): """Review all files in a pull request.""" changes = await get_pr_diff(repo, pr_number)
all_issues = []
for file_change in changes:
issues = await review_file(
filename=file_change["filename"],
patch=file_change["patch"],
language=file_change["language"],
)
for issue in issues:
issue["filename"] = file_change["filename"]
all_issues.extend(issues)
# Post results to GitHub
await post_review_comments(repo, pr_number, head_sha, all_issues)
## Step 4: GitHub Comment Writer
```python
async def post_review_comments(
repo: str, pr_number: int, commit_sha: str, issues: list[dict]
):
"""Post review comments on the GitHub PR."""
if not issues:
# Post a summary comment
await post_pr_comment(
repo, pr_number,
"AI Review: No issues found. The changes look good."
)
return
# Group by severity
critical = [i for i in issues if i["severity"] == "critical"]
warnings = [i for i in issues if i["severity"] == "warning"]
suggestions = [i for i in issues if i["severity"] == "suggestion"]
# Create review with inline comments
comments = []
for issue in issues:
body = f"**{issue['severity'].upper()}**: {issue['description']}"
if issue.get("suggestion"):
body += f"\n\nSuggested fix:\n```\n{issue['suggestion']}\n```"
comments.append({
"path": issue["filename"],
"line": issue.get("line", 1),
"body": body,
})
# Determine review action
event = "REQUEST_CHANGES" if critical else "COMMENT"
summary = f"""## AI Code Review Summary
| Severity | Count |
|---|---|
| Critical | {len(critical)} |
| Warning | {len(warnings)} |
| Suggestion | {len(suggestions)} |
{"**Action required**: Critical issues found that should be addressed before merging." if critical else "No blocking issues found."}"""
async with httpx.AsyncClient() as http_client:
await http_client.post(
f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews",
headers={
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json",
},
json={
"commit_id": commit_sha,
"body": summary,
"event": event,
"comments": comments,
}
)
Large PRs can exceed Claude's context window. Split the review into manageable chunks:
Still reading? Stop comparing — try CallSphere live.
See the IT support AI agent handle a real call — complete, industry-specific, and live in your browser. No signup.
async def review_large_pr(changes: list[dict], max_tokens_per_call: int = 50_000):
"""Break large PRs into reviewable chunks."""
current_batch = []
current_tokens = 0
for change in changes:
patch_tokens = len(change["patch"]) // 4 # Rough estimate
if current_tokens + patch_tokens > max_tokens_per_call and current_batch:
# Review current batch
yield await review_batch(current_batch)
current_batch = []
current_tokens = 0
current_batch.append(change)
current_tokens += patch_tokens
if current_batch:
yield await review_batch(current_batch)
The biggest challenge with AI code review is false positives. Every false positive erodes developer trust in the tool. Strategies to minimize them:
.ai-review-config.yml that describes coding standards, acceptable patterns, and known exceptionsFor an average PR with 10 changed files and 500 lines of diff:
| Component | Tokens | Cost (Sonnet) |
|---|---|---|
| System prompt (cached) | 500 | $0.00015 |
| 10 file diffs | 5,000 | $0.015 |
| 10 review outputs | 3,000 | $0.045 |
| Total per PR | 8,500 | $0.06 |
At 50 PRs per day, the monthly cost is approximately $90 -- less than one hour of a senior engineer's time. The ROI is immediate and substantial.

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
See how Circini's automated incident management pipeline turns alert emails into triaged Jira tickets using Snowflake Cortex AI, GPT-4.1, Airflow & MS Teams.
A clean before/after of agent architecture in 2026. The control loop moved from your framework code into the model's reasoning chain. What that looks like.
Google's May 2026 MCP 1.0 + A2A developers guide is the cleanest protocol picker we have seen. The takeaways, in plain English, with a CallSphere lens.
Workspace Studio puts a Gemini-powered AI agent builder inside Google Workspace. A walkthrough of what it does, who it is for, and where it fits in 2026.
Gemini 3.1 Ultra ships with a 2-million token context window and full text, image, audio, and video multimodality. What changes and how to build for it.
A 'did the agent answer correctly?' pass/fail hides broken tool calls, wasted tokens, and silent retries. Here is how to evaluate intermediate steps.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco