Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hookforge

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

What it looks like

┌────────────────────────────────────────────────────────────────────────────────────────┐
│ 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.

Quick start (local dev)

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 TUI

hookforge 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.

TUI keys

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 //.

Signature verification

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 & history

  • Replay (r) re-sends a captured request straight to localhost: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 to vi); 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 --offline to 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

Power features

  • 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:
    [[routing]]
    json_field = "type"     # or: header = "x-event"
    equals = "refund"
    port = 3009
    When a rule matches, the request goes to its port instead of the default(s).
  • Filter/search — press / in the TUI to filter the list by method/path/status/body.
  • curl export — press c to write the selected request as a runnable curl script.
  • CI modehookforge 3000 --ci runs headless and prints each request as a JSON line to stdout (replay/TUI disabled), for piping into scripts and tests.

CLI

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

Self-hosting

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 like https://abc123.hooks.example.com and 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.

Config file (~/.hookforge/config.toml)

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 = ""

Security & privacy

  • 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.

TLS / HTTPS

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://.

Development

cargo build      # build both binaries
cargo test       # unit + end-to-end integration tests

Project 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

License

Open source.

About

A fast, self-hostable webhook tunnel with a terminal inspector — built in Rust.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages