Hookbase
LoginGet Started Free
Back to Blog
Tutorial

How to Forward GitHub Webhooks to Localhost

Three ways to receive GitHub webhooks on your local machine for development — including the one that does not require an ngrok account, a public URL, or punching holes in your firewall.

Hookbase Team
April 7, 2026
5 min read

Why This Is Annoying

You're building a GitHub App or a CI integration. You need to test how your code reacts to a real pull_request event with a real payload from a real repo. The problem: GitHub sends webhooks to public URLs, and your dev server is on localhost:3000.

The traditional answer is "set up ngrok," but that has tradeoffs:

  • Free ngrok URLs change every restart — you have to update your GitHub webhook config every session
  • Stable URLs need a paid plan
  • The tunnel goes through ngrok's network, with their bandwidth limits and inspection
  • Your local machine is exposed to anyone who guesses the URL

Here are three approaches, ranked by how much friction they introduce.

Option 1: Hookbase CLI (Recommended)

Install the Hookbase CLI and run a single command:

npm install -g @hookbase/cli
hookbase login
hookbase tunnel --port 3000

You get a stable URL like https://acme.hookbase.app/ingest/your-source that doesn't change between sessions. Point GitHub at it once and you're done — restart your dev server as many times as you want.

Hookbase verifies the GitHub HMAC signature for you, stores every event for replay, and shows the full payload and your handler's response in the dashboard. When you break your handler at 2am and want to replay the webhook that crashed it, the event is right there.

# Replay any past event against your local handler
hookbase replay evt_abc123 --port 3000

This is the no-friction option for ongoing development.

Option 2: smee.io

GitHub's official suggestion. Free, no account required, runs in your browser:

npm install -g smee-client
smee --url https://smee.io/your-channel --target http://localhost:3000/webhook

Pros: free forever, official GitHub recommendation.

Cons: no signature verification on Smee's side (you still verify in your handler), no event history beyond the browser tab, no replay, no payload inspection. Fine for one-off testing, painful for daily use.

Option 3: ngrok

Still works, still familiar:

ngrok http 3000

Pros: well-known, mature.

Cons: URL changes on every restart unless you pay, no GitHub-specific features, no event history.

Configuring the GitHub Webhook

Whichever tunnel you pick, the GitHub setup is the same:

  1. Go to your repo → Settings → Webhooks → Add webhook
  2. Payload URL: your tunnel URL (the Hookbase ingest URL, smee channel, or ngrok address)
  3. Content type: application/json
  4. Secret: a random string — generate with openssl rand -hex 32
  5. Events: pick the ones you care about (or "Send me everything" while developing)
  6. Save

Make a commit, open a PR, or trigger whatever event you configured. The webhook should arrive within a second or two.

Verifying the Signature in Your Handler

GitHub signs every payload with HMAC-SHA256 using your secret. Always verify it — even in development. Catching a signature bug locally is much cheaper than catching it in production.

import crypto from 'crypto';

function verifyGitHubSignature(payload: string, signature: string, secret: string) {
  const hmac = crypto.createHmac('sha256', secret);
  const digest = 'sha256=' + hmac.update(payload).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.header('X-Hub-Signature-256');
  if (!verifyGitHubSignature(req.body.toString(), signature, process.env.GITHUB_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body.toString());
  console.log('Event type:', req.header('X-GitHub-Event'));
  console.log('Action:', event.action);

  res.status(200).send('ok');
});

Note the use of timingSafeEqual — never use === for signature comparison. Timing attacks are real even on localhost.

Common Issues

"I get a 401 on every webhook." Your secret is wrong, or you're hashing the parsed JSON instead of the raw body. The signature is computed over the exact bytes GitHub sent — re-serializing the JSON changes whitespace and breaks the hash.

"It works once, then GitHub stops sending." Your handler returned a non-2xx, GitHub marked the delivery as failed, and disabled the webhook. Check Settings → Webhooks → Recent Deliveries to see the response and re-enable the hook.

"My tunnel URL changed and now nothing works." This is the daily friction with ngrok free and smee. The Hookbase tunnel URL is stable for the lifetime of the source — restart as many times as you want.

Beyond Local Development

The same Hookbase tunnel that forwards to your laptop today is the same infrastructure you'd use to receive GitHub webhooks in production. You'd just point it at your production handler instead of localhost. No code changes, no second integration to maintain.

Install the CLI or start a free account.

githubwebhookstutoriallocalhostdevelopmentcli

Related Articles

Tutorial

Shopify Webhook Signature Verification, Explained

Shopify HMAC verification trips up almost every first-time integrator. Here is exactly how the signature is computed, what goes wrong, and a working implementation in Node, Python, Go, and Ruby.

Reference

Webhook Retries: What Every Provider Does Differently

Stripe retries for 3 days. GitHub gives up after one failure. Shopify retries 19 times. Knowing the rules for each provider is the difference between losing events and not. A reference table plus what it means for your handler.

Best Practices

Idempotency Keys for Webhooks: A Practical Guide

Webhooks get retried. Without idempotency, that means duplicate orders, double charges, and angry customers. Here is how to design a deduplication strategy that actually works.

Ready to Try Hookbase?

Start receiving, transforming, and routing webhooks in minutes.

Get Started Free
Hookbase

Reliable webhook infrastructure for modern teams. Built on Cloudflare's global edge network.

Product

  • Features
  • Pricing
  • Use Cases
  • Integrations
  • ngrok Alternative

Resources

  • Documentation
  • API Reference
  • CLI Guide
  • Blog
  • FAQ

Free Tools

  • All Tools
  • Webhook Bin
  • HMAC Calculator
  • JSONata Playground
  • Cron Builder
  • Payload Formatter
  • Local Testing

Legal

  • Privacy Policy
  • Terms of Service
  • Contact
  • Status

© 2026 Hookbase. All rights reserved.