Build a code review bot
Build a GitHub bot that responds to pull requests, clones the repository in a sandbox, uses Claude to analyze code changes, and posts review comments.
Time to complete: 30 minutes
- Sign up for a Cloudflare account ↗.
- Install
Node.js
↗.
Node.js version manager
Use a Node version manager like Volta ↗ or nvm ↗ to avoid permission issues and change Node.js versions. Wrangler, discussed later in this guide, requires a Node version of 16.17.0
or later.
You'll also need:
- A GitHub account ↗ and personal access token with repo permissions
- An Anthropic API key ↗ for Claude
- A GitHub repository for testing
npm create cloudflare@latest -- code-review-bot --template=cloudflare/sandbox-sdk/examples/minimal
yarn create cloudflare code-review-bot --template=cloudflare/sandbox-sdk/examples/minimal
pnpm create cloudflare@latest code-review-bot --template=cloudflare/sandbox-sdk/examples/minimal
cd code-review-bot
npm i @anthropic-ai/sdk @octokit/rest
yarn add @anthropic-ai/sdk @octokit/rest
pnpm add @anthropic-ai/sdk @octokit/rest
Replace src/index.ts
:
import { getSandbox, proxyToSandbox, type Sandbox } from '@cloudflare/sandbox';import { Octokit } from '@octokit/rest';import Anthropic from '@anthropic-ai/sdk';
export { Sandbox } from '@cloudflare/sandbox';
interface Env { Sandbox: DurableObjectNamespace<Sandbox>; GITHUB_TOKEN: string; ANTHROPIC_API_KEY: string; WEBHOOK_SECRET: string;}
export default { async fetch(request: Request, env: Env): Promise<Response> { const proxyResponse = await proxyToSandbox(request, env); if (proxyResponse) return proxyResponse;
const url = new URL(request.url);
if (url.pathname === '/webhook' && request.method === 'POST') { const signature = request.headers.get('x-hub-signature-256'); const body = await request.text();
// Verify webhook signature if (!signature || !(await verifySignature(body, signature, env.WEBHOOK_SECRET))) { return Response.json({ error: 'Invalid signature' }, { status: 401 }); }
const event = request.headers.get('x-github-event'); const payload = JSON.parse(body);
// Only handle opened PRs if (event === 'pull_request' && payload.action === 'opened') { reviewPullRequest(payload, env).catch(console.error); return Response.json({ message: 'Review started' }); }
return Response.json({ message: 'Event ignored' }); }
return new Response('Code Review Bot\n\nConfigure GitHub webhook to POST /webhook'); },};
async function verifySignature(payload: string, signature: string, secret: string): Promise<boolean> { const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( 'raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] );
const signatureBytes = await crypto.subtle.sign('HMAC', key, encoder.encode(payload)); const expected = 'sha256=' + Array.from(new Uint8Array(signatureBytes)) .map(b => b.toString(16).padStart(2, '0')) .join('');
return signature === expected;}
async function reviewPullRequest(payload: any, env: Env): Promise<void> { const pr = payload.pull_request; const repo = payload.repository; const octokit = new Octokit({ auth: env.GITHUB_TOKEN });
// Post initial comment await octokit.issues.createComment({ owner: repo.owner.login, repo: repo.name, issue_number: pr.number, body: 'Code review in progress...' });
const sandbox = getSandbox(env.Sandbox, `review-${pr.number}`);
try { // Clone repository const cloneUrl = `https://${env.GITHUB_TOKEN}@github.com/${repo.owner.login}/${repo.name}.git`; await sandbox.exec(`git clone --depth=1 --branch=${pr.head.ref} ${cloneUrl} /workspace/repo`);
// Get changed files const comparison = await octokit.repos.compareCommits({ owner: repo.owner.login, repo: repo.name, base: pr.base.sha, head: pr.head.sha });
const files = []; for (const file of (comparison.data.files || []).slice(0, 5)) { if (file.status !== 'removed') { const content = await sandbox.readFile(`/workspace/repo/${file.filename}`); files.push({ path: file.filename, patch: file.patch || '', content: content.content }); } }
// Generate review with Claude const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY }); const response = await anthropic.messages.create({ model: 'claude-sonnet-4-5', max_tokens: 2048, messages: [{ role: 'user', content: `Review this PR:
Title: ${pr.title}
Changed files:${files.map(f => `File: ${f.path}\nDiff:\n${f.patch}\n\nContent:\n${f.content.substring(0, 1000)}`).join('\n\n')}
Provide a brief code review focusing on bugs, security, and best practices.` }] });
const review = response.content[0]?.type === 'text' ? response.content[0].text : 'No review generated';
// Post review comment await octokit.issues.createComment({ owner: repo.owner.login, repo: repo.name, issue_number: pr.number, body: `## Code Review\n\n${review}\n\n---\n*Generated by Claude*` });
} catch (error: any) { await octokit.issues.createComment({ owner: repo.owner.login, repo: repo.name, issue_number: pr.number, body: `Review failed: ${error.message}` }); } finally { await sandbox.destroy(); }}
# GitHub token (needs repo permissions)npx wrangler secret put GITHUB_TOKEN
# Anthropic API keynpx wrangler secret put ANTHROPIC_API_KEY
# Webhook secret (generate a random string)npx wrangler secret put WEBHOOK_SECRET
npx wrangler deploy
- Go to your repository Settings > Webhooks > Add webhook
- Set Payload URL:
https://code-review-bot.YOUR_SUBDOMAIN.workers.dev/webhook
- Set Content type:
application/json
- Set Secret: Same value you used for
WEBHOOK_SECRET
- Select Let me select individual events → Check Pull requests
- Click Add webhook
Create a test PR:
git checkout -b test-reviewecho "console.log('test');" > test.jsgit add test.jsgit commit -m "Add test file"git push origin test-review
Open the PR on GitHub and watch for the bot's review comment!
A GitHub code review bot that:
- Receives webhook events from GitHub
- Clones repositories in isolated sandboxes
- Uses Claude to analyze code changes
- Posts review comments automatically
- Git operations - Advanced repository handling
- Sessions API - Manage long-running sandbox operations
- GitHub Apps ↗ - Build a proper GitHub App
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark
-