Github hmac webhook verify
Skill kjuhwa/skills-hub/skills/cli/github-hmac-webhook-verify
Self-correcting knowledge corpus for Claude Code — 9 stable shape clusters, bias-correction pipeline baked into contribution flow. 47 papers, 45 techniques, 1.1k skills.
npx -y skills add kjuhwa/skills-hub --skill github-hmac-webhook-verifyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Verify a GitHub webhook's `X-Hub-Signature-256` using HMAC-SHA-256 over the raw body with `timingSafeEqual`, with a same-length guard, prefix-masked logs on mismatch, and a 200-then-async processing pattern.
SKILL.md
5.0 KB, ~1.1k tokens by cl100k_base, as published. Nobody here has run it
Verify GitHub Webhook HMAC-SHA-256 (with Timing-Safe Compare + Async Processing)
When to use
- Your service receives
POST /webhooks/githubevents (issue comments, PR events, etc.). - You have
GITHUB_WEBHOOK_SECRETconfigured in GitHub's repo settings and in your server env. - You want a compact, hardened verify helper that doesn't leak timing information and handles the common failure modes gracefully.
Steps
-
Read the raw body as text, before any JSON parse. HMAC is over the raw bytes; parsing and re-serializing will not reproduce the same digest. In Hono:
const payload = await c.req.text(); const signature = c.req.header('X-Hub-Signature-256') ?? ''; -
Compute
sha256=+ hex HMAC using your secret:import { createHmac, timingSafeEqual } from 'crypto'; const hmac = createHmac('sha256', webhookSecret); const digest = 'sha256=' + hmac.update(payload).digest('hex'); -
Guard against length mismatch before
timingSafeEqual.timingSafeEqualthrows on different lengths. A missing/malformed header shouldn't crash your handler — it should just fail verification.const digestBuffer = Buffer.from(digest); const signatureBuffer = Buffer.from(signature); if (digestBuffer.length !== signatureBuffer.length) { log.error({ receivedLength: signatureBuffer.length, computedLength: digestBuffer.length }, 'github.signature_length_mismatch'); return false; } const isValid = timingSafeEqual(digestBuffer, signatureBuffer); -
Log both prefixes (not full values) on mismatch. Signatures are not secrets, but "compressed" logging gives you a diagnostic hint without bloating the log:
if (!isValid) { log.error({ receivedPrefix: signature.substring(0, 15) + '...', computedPrefix: digest.substring(0, 15) + '...', }, 'github.signature_mismatch'); } -
Wrap the whole verify in a try/catch that returns
falseon any thrown error — a malformed signature header shouldn't crash the request handler. -
Return 200 immediately, process async. GitHub retries on non-2xx and has a 10-second timeout. Return 200 as soon as signature + authorization check pass, then do the heavy work (database writes, AI spawning, git operations) in a fire-and-forget path:
app.post('/webhooks/github', async c => { const payload = await c.req.text(); const signature = c.req.header('X-Hub-Signature-256') ?? ''; if (!adapter.verifySignature(payload, signature)) return c.text('', 401); // fire-and-forget — don't await adapter.handleWebhook(payload, signature).catch(err => log.error({ err }, 'github.handle_webhook_failed')); return c.text('', 200); }); -
Add an authorization layer after signature verification. The HMAC only proves "GitHub sent this with our secret." It doesn't prove the sender is authorized in your application. Archon keeps a whitelist of GitHub usernames and silently rejects unauthorized senders (log with masked username, return success) — no error response (prevents probing).
Counter / Caveats
sha1(X-Hub-Signature) is deprecated. UseX-Hub-Signature-256only.- The GitHub signature format is
sha256=<hex>. Don't forget thesha256=prefix when comparing. - Configure
c.req.text()before any body-consuming middleware — once Hono parses JSON, re-reading raw bytes is not safe. - If you need to re-deliver failed webhooks, persist the raw payload + signature for later reprocessing; do not reserialize from parsed JSON.
- Rotate the webhook secret periodically. Your verify code should accept a list of valid secrets during rotation (match on first success). Archon's current adapter takes a single secret; extend with a list if rotation matters.
Evidence
packages/adapters/src/forge/github/adapter.ts:260-296:verifySignaturewith length guard,timingSafeEqual, prefix-masked logs on mismatch, try/catch returningfalse.handleWebhookorchestrator atadapter.ts:691-725: verify → parse → authorize (whitelist check with masked username log) → close-event handling, etc.adapter.test.ts+context.test.tsexercise the signature path with real HMAC computation.- Root
CLAUDE.mdlines 800-810: "Return 200 immediately, process async. Verify webhook signatures (GitHub: X-Hub-Signature-256). Usec.req.text()for raw webhook body (signature verification)." - Commit SHA: d89bc767d291f52687beea91c9fcf155459be0d9.