hookwarden documentation

hookwarden finds the webhook handlers in your codebase and checks that each one verifies the provider's signature before it trusts the payload. It runs on your machine, makes no network calls, and gives every finding a file, a line, and a fix.

Install

Run it with no install at all, or add it natively through your OS package manager.

$ npx hookwarden scan . # zero install, works everywhere
$ npm install -g hookwarden
$ brew install Hookwarden/tap/hookwarden # macOS + Linux
$ pip install hookwarden # Python toolchains

Requires Node 22+ for the npm / npx path. JavaScript, TypeScript, Python 3.10+, PHP 8.0+, and Go 1.21+ are scanned out of the box. Windows: scoop install hookwarden.

Quick start

Point scan at any app. Each finding carries a severity glyph, the exact file:line, the rule id, the three-state verdict, and a fix drawn verbatim from the provider's docs.

app — hookwarden scan
➜appnpx hookwarden scan ./your-app
×criticalserver.js:10:1stripe/express-middleware-orderingnot-verified
express.json() runs before the webhook route — the raw bytes are gone before the HMAC is computed.
fix › mount express.raw({ type: 'application/json' }) on the webhook path.
Found 1 critical · 0 high · 0 manual-review — 1 webhook handler across 1 file
Scanned in 0.0 s · 100.0% coverage · engine v0.11.0 · rules v0.11.0

The exit code tells CI what happened. Set the threshold with --fail-on (default critical).

0No findings at or above the threshold.
1At least one finding at or above the threshold.
2The engine hit an error and the scan is incomplete.
3The command line or config file is invalid.

Three-state verdicts

Every handler gets one of three verdicts. Severity is separate: it ranks how bad a finding is, and it's what --fail-on compares against.

  1. verified const event = stripe.webhooks.constructEvent(rawBody, sig, secret);

    A correct signature check runs before the handler uses the payload. Nothing to do.

  2. not-verified const event = JSON.parse(req.body);

    The handler trusts the payload without a valid check: the signature is never verified, compared with ==, or computed over an already-parsed body. Fix it before you ship.

  3. manual-review app.post("/webhooks/stripe", verifyFrom(config), handle);

    The source alone can't prove it safe or unsafe, for example when verification happens in middleware hookwarden couldn't follow. It doesn't fail CI by default; someone should take a look.

Fixing findings

hookwarden fix applies the mechanical remediations with a real AST-rewrite engine — === → crypto.timingSafeEqual, == → hash_equals, and so on. It's dry-run by default; pass --write to apply.

$ npx hookwarden fix ./your-app # dry-run: print the diff, change nothing
$ npx hookwarden fix ./your-app --write # apply the safe fixes

Read path and write path are separate by design — scan --fix is rejected at parse time. Only fixes the three-state engine classified as safe are auto-applied.

Continuous integration

Gate every pull request. The official GitHub Action runs the scan, fails the build at your severity threshold, and uploads SARIF to Code Scanning so findings appear as PR annotations.

# .github/workflows/hookwarden.yml
name: hookwarden
on: [pull_request]
permissions:
  contents: read
  security-events: write
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Hookwarden/hookwarden-action@v1
        with: { fail-on: high }

Providers & frameworks

Each provider's rule pack encodes signature quirks a generic scanner can't know — Stripe's 5-minute timestamp tolerance, Slack's v0:${ts}:${body} scheme, Twilio's SHA1 outlier.

Providers

  • Anthropic Agent SDK
  • Auth0
  • Bitbucket
  • Calendly
  • Discord
  • DocuSign
  • GitHub
  • HubSpot
  • Intercom
  • Linear
  • Mailchimp
  • n8n
  • Notion
  • PagerDuty
  • Postmark
  • Sentry
  • Shopify
  • Slack
  • Square
  • Stripe
  • Twilio
  • Zendesk
  • Zoom
  • Standard Webhooks (Clerk, Resend, Svix and others)

Frameworks

JavaScript, TypeScriptExpress, Hono, Fastify, Next.js
PythonFlask, FastAPI, Django
PHPLaravel, Symfony, Slim, plain PHP
Gonet/http, chi, gin, echo

From CLI to platform

The CLI finds and fixes bugs on your machine. The hosted platform keeps watching after you ship: connect a repo and hookwarden scans every push, rotates webhook secrets without dropping a delivery, and keeps a tamper-evident record your auditor can read. Org-wide coverage runs through a read-only GitHub App.

The hosted platform is launching soon. Everything above this section works today with the free CLI.

Connect a repo

Install the hookwarden GitHub App and choose which repositories to watch. The App takes read-only access to source — it never needs write access to your code. From then on every push is scanned automatically; there's nothing to run.

Org-wide installs use a GitHub App (not an OAuth App) so coverage follows the org, not one person's token.

Continuous scanning

The same engine as the CLI, run on every push. The dashboard tracks your findings delta over time — new, persisting, and fixed — alongside a live inventory of every webhook handler across your connected repos, each labelled with its provider, framework, and verdict.

Automated rotation

Rotating a webhook secret is risky — swap it too early and you drop live deliveries. hookwarden opens a dual-secret window: both the old and new secret stay valid until a real webhook validates against the new one, then the old is retired. Every rotation requires your confirmation, and every step is signed into the audit log.

Fully automated rotation runs for Stripe and GitHub today. Every other provider is handled by a guided runbook — same dual-secret safety, same audit trail.

Old secret New secret new secret issued delivery verified, old secret retired
Both secrets stay valid until a real webhook validates against the new one. Delivery never drops.

Rotation runbooks

Some providers expose no rotation API, and some have no overlap window at all. For those, hookwarden ships a guided runbook per provider: an ordered, gated checklist that walks you through the exact dashboard steps, verifies a live delivery against the new secret before you finish, and signs each step to the audit log — so a manual rotation is as auditable as an automated one.

Each runbook is labelled dual-secret or single-swap so you know whether there's an overlap window before you start.

Audit log & SOC2 evidence

Every scan, finding, and rotation step is appended to a hash-chained, tamper-evident log — each entry links to the one before it and the chain is KMS-signed, so a missing or altered row is detectable. It's the chain-of-custody an auditor asks for, not a dashboard screenshot.

seq 1043 rotation.verified a3f9c1
seq 1044 secret.retired 9b0247 ⛓ prev a3f9c1
seq 1045 runbook.step.signed e1d7fa ⛓ prev 9b0247

Missing a provider or framework? Open an issue on GitHub.