A fast, terminal-first, open-source webhook development tool built in Rust.
hookforge opens a public tunnel to your localhost and shows every incoming webhook in a rich terminal UI — no account, no signup, no browser. It can be self-hosted so your payloads never touch a server you don't own.
[ webhook provider ] → [ hookforge-relay ] →ws→ [ hookforge CLI ] → localhost:PORT
└→ TUI inspector
┌────────────────────────────────────────────────────────────────────────────────────────┐
│ hookforge → localhost:3000 https://abc123.hooks.example.com │
└────────────────────────────────────────────────────────────────────────────────────────┘
┌─ Requests (4) ────────────────────────┐┌─ Detail ──────────────────────────────────────┐
│▶ ✓ POST /webhooks/stripe 200 12ms││POST /webhooks/stripe │
│ POST /webhooks/stripe 200 9ms││→ 200 12ms 14:02:11 │
│ ↻ POST /webhooks/github 200 15ms││✓ Stripe signature valid │
│ ✗ POST /webhooks/shopify 401 4ms││event: payment_intent.succeeded │
│ ││ │
│ ││Request Headers │
│ ││ content-type: application/json │
│ ││ stripe-signature: t=1699..,v1=5c9.. │
│ ││Request Body │
│ ││ { │
│ ││ "type": "payment_intent.succeeded", │
│ ││ "data": { "amount": 2000 } │
│ ││ } │
│ ││Response Headers │
│ ││ (none) │
│ ││Response Body │
└───────────────────────────────────────┘└───────────────────────────────────────────────┘
[r]eplay [e]dit [s]ave [c]url [/]search [↑↓]nav [Tab]panel [q]uit
The left pane lists every inbound request (↻ = replayed, ✓/✗/• = signature verdict);
the right pane shows the selected request's headers, JSON-pretty body, and response.
Run the relay and client on the same machine. In three terminals:
# 1. Build
cargo build
# 2. Start the relay server
cargo run --bin hookforge-relay # listens on 127.0.0.1:8080
# 3. Start a local app to receive webhooks (any server on :3000)
python3 -m http.server 3000
# 4. Start hookforge, pointed at your local port
cargo run -- 3000 # prints the public URL, opens the TUIhookforge prints a public URL like http://127.0.0.1:8080/t/<id>. Send a webhook to it:
curl -X POST http://127.0.0.1:8080/t/<id>/webhook \
-H 'content-type: application/json' \
-d '{"hello":"world"}'The request appears in the TUI request list; select it to inspect headers and the (JSON-pretty-printed) body and response.
| Key | Action |
|---|---|
↑ / k |
Previous request (list) / scroll up (detail) |
↓ / j |
Next request (list) / scroll down (detail) |
PageUp / PageDown |
Scroll the detail pane |
Tab |
Switch panel |
r |
Replay the selected request to the local port |
e |
Edit the body in $EDITOR, then replay |
s |
Save the session to hookforge-session-<ts>.json |
c |
Export the selected request as a curl command (hookforge-curl-<ts>.sh) |
/ |
Filter/search the list (method/path/status/body); Enter applies, Esc clears |
q / Ctrl-C |
Quit (Esc clears an active filter first) |
Replayed requests appear in the list marked with ↻; verified requests show ✓/✗/•.
hookforge verifies provider webhook signatures client-side, so signing secrets stay on
your machine and never reach the relay. Add secrets to ~/.hookforge/config.toml:
[verify]
stripe_secret = "whsec_…" # Stripe-Signature (t=…,v1=… HMAC-SHA256 of "t.body")
github_secret = "…" # X-Hub-Signature-256 (sha256=… HMAC-SHA256 of body)
shopify_secret = "…" # X-Shopify-Hmac-Sha256 (base64 HMAC-SHA256 of body)The provider is auto-detected from the signature header. The TUI detail pane then shows
✓ Stripe signature valid / ✗ … invalid / • … no secret configured, plus a one-line event
summary (Stripe type, GitHub X-GitHub-Event, Shopify X-Shopify-Topic). The verdict is
saved with the session.
- Replay (
r) re-sends a captured request straight tolocalhost:PORT— useful for iterating on a handler without waiting for the provider to fire again. - Edit + replay (
e) opens the request body in your$EDITOR($VISUAL/$EDITOR, falls back tovi); save & quit the editor to replay the modified body. - Save (
s) writes the whole session (requests + responses) to a JSON file. - Load a saved session with
--load, and use--offlineto inspect/replay it with no relay connection:
hookforge 3000 --load hookforge-session-1234.json # preload into a live session
hookforge 3000 --offline --load hookforge-session-1234.json # replay offline, no relay- Fan-out — forward each webhook to several local ports at once:
hookforge 3000 --fanout 3001,3002. The first port's response is returned upstream; the others are best-effort. Each target produces its own inspector row. - Conditional routing — route by payload/header via
[[routing]]rules in the config:When a rule matches, the request goes to its port instead of the default(s).[[routing]] json_field = "type" # or: header = "x-event" equals = "refund" port = 3009
- Filter/search — press
/in the TUI to filter the list by method/path/status/body. - curl export — press
cto write the selected request as a runnablecurlscript. - CI mode —
hookforge 3000 --ciruns headless and prints each request as a JSON line to stdout (replay/TUI disabled), for piping into scripts and tests.
hookforge <PORT> # forward to localhost:PORT
hookforge --port 3000 # equivalent
hookforge 3000 --relay wss://hooks.example.com/tunnel [--auth-token <secret>]
hookforge 3000 --load session.json [--offline]
hookforge 3000 --fanout 3001,3002 # fan out to extra ports
hookforge 3000 --ci # headless JSON-to-stdout
hookforge 3000 --relay wss://… --insecure # accept a self-signed relay cert (testing)
hookforge-relay --bind 127.0.0.1:8080
hookforge-relay --bind 0.0.0.0:8080 --domain hooks.example.com --auth-token <secret>
hookforge-relay --tls-cert cert.pem --tls-key key.pem # built-in HTTPS/WSS
Run your own relay so payloads never touch a third party. See deploy/ for a
Docker + Caddy setup. Highlights:
- Wildcard-subdomain routing — with
--domain hooks.example.com, tunnels get clean URLs likehttps://abc123.hooks.example.comand your provider paths map 1:1 (no/t/<id>prefix). Without a domain, the relay falls back to path-based…/t/<id>/…for local dev. - Optional auth —
--auth-token <secret>gates tunnel creation (inbound webhooks are never gated, since providers don't know the token). - No-log — the relay writes nothing to disk; requests are forwarded, never stored.
Both binaries read this; CLI flags override it.
relay = "wss://hooks.example.com/tunnel"
port = 3000
auth_token = "your-shared-secret"
# relay-side:
domain = "hooks.example.com"
[verify] # used by provider signature verification
stripe_secret = "whsec_…"
github_secret = "…"
shopify_secret = "…"- No account, no signup, no telemetry. Requests are forwarded, never stored on disk.
- Ephemeral session tokens. Each tunnel gets a cryptographically-random token, valid only for the session — no long-lived credentials.
- Self-hostable with optional auth — payloads never reach a third party.
The relay can serve https:///wss:// two ways:
- Built-in TLS — single binary, you supply the cert. Best for internal networks, corp-PKI,
or local testing:
hookforge-relay --tls-cert cert.pem --tls-key key.pem
- Reverse proxy (Caddy) — best for public endpoints: Caddy auto-issues and auto-renews
Let's Encrypt certs (built-in TLS does not do ACME). See
deploy/.
Test HTTPS locally with a self-signed cert:
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365 -subj "/CN=localhost"
hookforge-relay --tls-cert cert.pem --tls-key key.pem # serves https/wss on :8080
hookforge 3000 --relay wss://127.0.0.1:8080/tunnel --insecure # --insecure: accept self-signed
curl -k https://127.0.0.1:8080/t/<id>/webhook -d '{"hi":1}' # -k: accept self-signed--insecure (client) and -k (curl) are needed only for self-signed certs — with a real cert
(Let's Encrypt) neither is required. Local dev without any --tls-* flags stays plain ws://.
cargo build # build both binaries
cargo test # unit + end-to-end integration testsProject layout:
src/
├── protocol.rs # wire protocol (JSON over WebSocket) + helpers
├── main.rs # `hookforge` binary: client + TUI
├── bin/
│ └── hookforge-relay.rs
├── relay/ # axum relay server + tunnel/request registry
├── tunnel/ # WebSocket client + local-port forwarder
├── replay/ # replay worker (re-send to local port)
├── session.rs # save/load sessions as JSON
├── config.rs # ~/.hookforge/config.toml loading
├── verify/ # provider signature verification (Stripe/GitHub/Shopify)
├── routing.rs # fan-out + conditional-routing targets
└── tui/ # ratatui inspector (app state, layout, input)
deploy/ # Dockerfile + docker-compose + Caddy for self-hosting
tests/
└── e2e.rs # round-trip + replay + subdomain/auth + signatures + fan-out + CI
Open source.