[ Web Proxy ]
URL:
Viewing: https://developers.cloudflare.com/sandbox/tutorials/code-review-bot/ [Back]  [Original]

Build a code review bot Cloudflare Sandbox SDK docsSkip to content
SearchCtrlKLog in
  1. Home
  2. /Sandbox SDK
  3. /Tutorials
  4. /Build a code review bot

Build a code review bot

Last updated May 5, 2026Copy as MarkdownView as MarkdownAgent setup
OverviewPrerequisites1. Create your project2. Install dependencies3. Build the webhook handler4. Set up local environment variables5. Expose local server with Cloudflare Tunnel6. Configure GitHub webhook for local testing7. Test locally with a pull request8. Deploy to production9. Update webhook for productionWhat you builtNext steps

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

Prerequisites

  1. Sign up for a Cloudflare account .
  2. 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

1. Create your project

npmyarnpnpm
npm create cloudflare@latest -- code-review-bot --template=cloudflare/sandbox-sdk/examples/minimal
cd code-review-bot

2. Install dependencies

npmyarnpnpmbun
npm i @anthropic-ai/sdk @octokit/rest

3. Build the webhook handler

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,
		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();
	}
}

4. Set up local environment variables

Create a .dev.vars file in your project root for local development:

cat > .dev.vars << EOF
GITHUB_TOKEN=your_github_token_here
ANTHROPIC_API_KEY=your_anthropic_key_here
WEBHOOK_SECRET=your_webhook_secret_here
EOF

Replace the placeholder values with:

  • GITHUB_TOKEN: Your GitHub personal access token with repo permissions
  • ANTHROPIC_API_KEY: Your API key from the Anthropic Console
  • WEBHOOK_SECRET: A random string (for example: openssl rand -hex 32)

Note

The .dev.vars file is automatically gitignored and only used during local development with npm run dev.

5. Expose local server with Cloudflare Tunnel

To test with real GitHub webhooks locally, use Cloudflare Tunnel to expose your local development server.

Start the development server:

npm run dev

In a separate terminal, create a tunnel to your local server:

cloudflared tunnel --url http://localhost:8787

This will output a public URL (for example, https://example.trycloudflare.com). Copy this URL for the next step.

Note

If you do not have cloudflared installed, refer to Downloads.

6. Configure GitHub webhook for local testing

Important

Configure this webhook on a specific GitHub repository where you will create test pull requests. The bot will only review PRs in repositories where the webhook is configured.

  1. Navigate to your test repository on GitHub
  2. Go to Settings > Webhooks > Add webhook
  3. Set Payload URL: Your Cloudflare Tunnel URL from Step 5 with /webhook appended (for example, https://example.trycloudflare.com/webhook)
  4. Set Content type: application/json
  5. Set Secret: Same value you used for WEBHOOK_SECRET in your .dev.vars file
  6. Select Let me select individual events Check Pull requests
  7. Click Add webhook

7. Test locally with a pull request

Create a test PR:

git checkout -b test-review
echo "console.log('test');" > test.js
git add test.js
git commit -m "Add test file"
git push origin test-review

Open the PR on GitHub. The bot should post a review comment within a few seconds.

8. Deploy to production

Deploy your Worker:

npx wrangler deploy

Then set your production secrets:

# GitHub token (needs repo permissions)
npx wrangler secret put GITHUB_TOKEN

# Anthropic API key
npx wrangler secret put ANTHROPIC_API_KEY

# Webhook secret (use the same value from .dev.vars)
npx wrangler secret put WEBHOOK_SECRET

9. Update webhook for production

  1. Go to your repository Settings > Webhooks
  2. Click on your existing webhook
  3. Update Payload URL to your deployed Worker URL: https://code-review-bot.YOUR_SUBDOMAIN.workers.dev/webhook
  4. Click Update webhook

Your bot is now running in production and will review all new pull requests automatically.

What you built

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

Next steps

PreviousBuild an AI coding agent with OpenAI Agents SDKNextCode interpreter with Workers AI

Was this helpful?

YesNo
Edit pageReport issue
[]

Web Proxy Viewer  |  New URL  |  Original Page