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 fine-grained personal access token ↗ with the following permissions:
- Repository access: Select the specific repository you want to test with
 - Permissions > Repository permissions:
- Metadata: Read-only (required)
 - Contents: Read-only (required to clone the repository)
 - Pull requests: Read and write (required to post review comments)
 
 
 - An Anthropic API key ↗ for Claude
 - A GitHub repository for testing
 
npm create cloudflare@latest -- code-review-bot --template=cloudflare/sandbox-sdk/examples/minimalyarn create cloudflare code-review-bot --template=cloudflare/sandbox-sdk/examples/minimalpnpm create cloudflare@latest code-review-bot --template=cloudflare/sandbox-sdk/examples/minimalcd code-review-botnpm i @anthropic-ai/sdk @octokit/restyarn add @anthropic-ai/sdk @octokit/restpnpm add @anthropic-ai/sdk @octokit/restReplace 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,    ctx: ExecutionContext,  ): 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 contentType = request.headers.get("content-type") || "";      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");
      // Parse payload (GitHub can send as JSON or form-encoded)      let payload;      if (contentType.includes("application/json")) {        payload = JSON.parse(body);      } else {        // Handle form-encoded payload        const params = new URLSearchParams(body);        payload = JSON.parse(params.get("payload") || "{}");      }
      // Handle opened and reopened PRs      if (        event === "pull_request" &&        (payload.action === "opened" || payload.action === "reopened")      ) {        console.log(`Starting review for PR #${payload.pull_request.number}`);        // Use waitUntil to ensure the review completes even after response is sent        ctx.waitUntil(          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 });  const sandbox = getSandbox(env.Sandbox, `review-${pr.number}`);
  try {    // Post initial comment    console.log("Posting initial comment...");    await octokit.issues.createComment({      owner: repo.owner.login,      repo: repo.name,      issue_number: pr.number,      body: "Code review in progress...",    });    // Clone repository    console.log("Cloning 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    console.log("Fetching 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    console.log(`Analyzing ${files.length} files 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    console.log("Posting review...");    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*`,    });    console.log("Review complete!");  } catch (error: any) {    console.error("Review failed:", error);    await octokit.issues.createComment({      owner: repo.owner.login,      repo: repo.name,      issue_number: pr.number,      body: `Review failed: ${error.message}`,    });  } finally {    await sandbox.destroy();  }}Create a .dev.vars file in your project root for local development:
cat > .dev.vars << EOFGITHUB_TOKEN=your_github_token_hereANTHROPIC_API_KEY=your_anthropic_key_hereWEBHOOK_SECRET=your_webhook_secret_hereEOFReplace the placeholder values with:
GITHUB_TOKEN: Your GitHub personal access token with repo permissionsANTHROPIC_API_KEY: Your API key from the Anthropic Console ↗WEBHOOK_SECRET: A random string (for example:openssl rand -hex 32)
To test with real GitHub webhooks locally, use Cloudflare Tunnel to expose your local development server.
Start the development server:
npm run devIn a separate terminal, create a tunnel to your local server:
cloudflared tunnel --url http://localhost:8787This will output a public URL (for example, https://example.trycloudflare.com). Copy this URL for the next step.
- Navigate to your test repository on GitHub
 - Go to Settings > Webhooks > Add webhook
 - Set Payload URL: Your Cloudflare Tunnel URL from Step 5 with 
/webhookappended (for example,https://example.trycloudflare.com/webhook) - Set Content type: 
application/json - Set Secret: Same value you used for 
WEBHOOK_SECRETin your.dev.varsfile - 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-reviewOpen the PR on GitHub. The bot should post a review comment within a few seconds.
Deploy your Worker:
npx wrangler deployThen set your production secrets:
# GitHub token (needs repo permissions)npx wrangler secret put GITHUB_TOKEN
# Anthropic API keynpx wrangler secret put ANTHROPIC_API_KEY
# Webhook secret (use the same value from .dev.vars)npx wrangler secret put WEBHOOK_SECRET- Go to your repository Settings > Webhooks
 - Click on your existing webhook
 - Update Payload URL to your deployed Worker URL: 
https://code-review-bot.YOUR_SUBDOMAIN.workers.dev/webhook - Click Update webhook
 
Your bot is now running in production and will review all new pull requests automatically.
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