◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 44 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 44 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/API & SDK
⬡ API & SDK/2026-04-17Advanced

Building a GitHub PR Review Bot with Claude API — From Webhook Signature Verification to Structured Reviews with Tool Use

Build a production PR review bot with Claude API and GitHub Webhooks. Enforce structured scoring and security scanning via Tool Use, then handle the parts that bite in production — signature checks that pass only locally, 202 responses that hide failures, debouncing, and cost tracking.

Claude API128GitHub7PR ReviewTool Use9Webhook3TypeScript24Automation45Code Review

✦ Premium Article

Once your team grows past five engineers, code review consistency becomes a real problem. Some reviewers catch every instance of a particular bug pattern; others let the same thing slide. Review quality drops when people are tired. And more fundamentally, nobody has agreed on what "good" actually looks like.

Claude API can help. But asking it to "review this code" in the simplest possible way won't give you something you can rely on — the output varies too much. What makes this work at production scale is two things: using Tool Use to enforce structured output, and building a clean pipeline from GitHub Webhook all the way to posted review comments.

This article builds a bot that automatically posts review comments when a PR is opened or updated. We'll go from architecture design to deployment, explaining the why behind each implementation choice along the way.

System Architecture Overview

The flow is straightforward:

GitHub PR opened/updated
    ↓
Webhook event (POST /webhook)
    ↓
Express server (signature verification)
    ↓
Fetch PR diff via GitHub API
    ↓
Claude API (Tool Use) generates structured review
    ↓
Post review comment via GitHub API

The tech stack:

  • Runtime: Node.js 22 + TypeScript
  • Web framework: Express.js 4.x
  • Claude API: @anthropic-ai/sdk v1.x
  • GitHub API: @octokit/rest v21
  • Deployment: Railway / Render (always-on server required)

Why You Need an Always-On Server

GitHub Webhooks deliver POST requests in real time. While Cloudflare Workers or Vercel Serverless Functions technically work, architectures with cold starts are risky here. GitHub sets a 10-second timeout on webhook delivery, and a cold start that delays your response can trigger a retry loop — meaning the same PR gets reviewed twice or more.

Railway starts at around $5/month for an always-on server. That's more than sufficient for most teams.

Start by installing dependencies:

npm init -y
npm install express @anthropic-ai/sdk @octokit/rest
npm install -D typescript @types/express @types/node ts-node

Step 1: The Webhook Server

Receiving webhooks is where most implementations first go wrong: signature verification. GitHub attaches an X-Hub-Signature-256 header to every webhook request. Without verifying it, anyone can send forged requests to your endpoint.

// src/webhook.ts
import express from 'express';
import crypto from 'crypto';
import { handlePullRequestEvent } from './handlers/pullRequest';
 
const app = express();
 
// ⚠️ Critical: raw body required — do NOT use express.json()
app.use(
  express.raw({ type: 'application/json' })
);
 
function verifyWebhookSignature(
  rawBody: Buffer,
  signature: string,
  secret: string
): boolean {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(rawBody);
  const expectedSignature = `sha256=${hmac.digest('hex')}`;
 
  // Use timingSafeEqual to prevent timing attacks
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  } catch {
    // timingSafeEqual requires equal-length buffers
    return false;
  }
}
 
app.post('/webhook', async (req, res) => {
  const signature = req.headers['x-hub-signature-256'] as string;
  const eventType = req.headers['x-github-event'] as string;
 
  if (!signature) {
    return res.status(401).json({ error: 'Missing signature' });
  }
 
  const isValid = verifyWebhookSignature(
    req.body as Buffer,
    signature,
    process.env.GITHUB_WEBHOOK_SECRET!
  );
 
  if (!isValid) {
    console.error('Invalid webhook signature — possible spoofed request');
    return res.status(401).json({ error: 'Invalid signature' });
  }
 
  // Respond immediately — GitHub's timeout is 10 seconds
  res.status(202).json({ accepted: true });
 
  const payload = JSON.parse((req.body as Buffer).toString());
 
  if (
    eventType === 'pull_request' &&
    ['opened', 'synchronize'].includes(payload.action)
  ) {
    // Process asynchronously after responding
    handlePullRequestEvent(payload).catch(err => {
      console.error('[PR handler] Unhandled error:', err);
    });
  }
});
 
app.get('/health', (_req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
 
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Webhook server listening on port ${PORT}`);
});

Two things worth explaining here.

Why express.raw() instead of express.json(): The HMAC signature is computed against the raw bytes of the request body. Once Express parses the JSON, those bytes are gone. Re-serializing with JSON.stringify() gets you close enough to fool a local test and not close enough to survive production — I measure exactly how close further down, in Mistake 1. For now, keep the raw buffer and parse it yourself.

Why respond before processing: GitHub's webhook timeout is 10 seconds. Claude API calls can take several seconds. If you wait for the review to complete before responding, you'll hit the timeout, GitHub will retry, and the same PR gets reviewed multiple times. Send 202 immediately, then process asynchronously.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Measured on Node 22: express.json() signature verification passes for only 2 of 8 realistic bodies — why it succeeds locally and returns 401 only in production, and how express.raw() removes the problem
✦Force structured overall_score / security_issues through Tool Use (tool_choice: any) with exponential backoff for production stability
✦Measured: answering 202 first hides every downstream failure from GitHub's delivery log — a missing .catch() takes the whole process down, and a separate accepted/completed/failed ledger is what surfaces it
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ API & SDK2026-08-01
Swapping Tools Mid-Conversation Without Losing Your Prompt Cache
Tools can now be added and removed between turns, but the tool block sits at the very front of the cache prefix. I measured 12 mutation patterns with fingerprints and built a stable-core plus volatile-tail registry with a guard that refuses unsafe changes.
⬡ API & SDK2026-06-13
Claude Vision API in Production — Implementation Patterns for Image Analysis, PDF Processing, and OCR
Implementation patterns for taking Claude's vision capabilities to production: choosing between Base64, URL, and the Files API, native PDF processing, schema-enforced extraction with Tool Use, batch cost reduction, and error recovery — all with working code.
⬡ API & SDK2026-05-04
Claude API on Bun in Production: Migration Decisions and Implementation Patterns That Actually Survive Real Traffic
A practical guide to running Claude API services on Bun in production. Covers migration triggers from Node.js, built-in SQLite/WebSocket usage, streaming optimization, and the pitfalls that only surface after deployment — with working code and measured numbers.