Hookshot
The self-hosted webhook relay. Receive Stripe, GitHub, and other webhooks on localhost—without paying for ngrok.
Stripe ──→ your-server.fly.io ──→ tunnel ──→ localhost:3000
Why Hookshot?
- Free forever — No $8/mo subscription, no signup, no limits
- Self-hosted — Your webhooks never touch third-party servers
- Replay included — Re-send any webhook for debugging (ngrok charges extra)
- Route by path — Send
/api to port 3000, /webhooks to port 4000
- Beautiful TUI — Live request stream with Catppuccin colors
Quick Start
1. Deploy the server (on any VPS, Fly.io, Railway, etc.)
hookshot server --port 8080 --public-url https://hooks.yoursite.com
2. Connect locally
hookshot client -s https://hooks.yoursite.com -t http://localhost:3000
✓ Connected!
Public URL: https://hooks.yoursite.com/t/a1b2c3d4-...
Forwarding: http://localhost:3000
────────────────────────────────────────────
[15:04:05] → POST /webhooks/stripe (d08ba939)
[15:04:05] ← 200 OK (12ms)
3. Point your webhook provider to your public URL. Done.
Installation
go install github.com/lance0/hookshot/cmd/hookshot@latest
Or grab a binary from Releases.
Interactive TUI
hookshot client -s https://hooks.yoursite.com --tui
┌────────────────────────────────────────────────────────────────────┐
│ hookshot tunnel: a1b2c3d4 ● connected │
│ https://hooks.yoursite.com/t/a1b2c3d4-... │
├────────────────────────────────────────────────────────────────────┤
│ REQUESTS [r]eplay [/]filter │
│ ──────────────────────────────────────────────────────────────── │
│ ▸ POST /webhooks/stripe 200 12ms just now d08ba939 │
│ POST /webhooks/github 200 45ms 2s ago f4a21c87 │
│ POST /webhooks/slack 500 8ms 5s ago b7e93d12 │
├────────────────────────────────────────────────────────────────────┤
│ REQUEST DETAIL │
│ ──────────────────────────────────────────────────────────────── │
│ POST /webhooks/stripe │
│ Content-Type: application/json │
│ {"type":"payment_intent.succeeded","amount":2000} │
└────────────────────────────────────────────────────────────────────┘
↑/↓ navigate r replay / filter q quit
Inspect headers, bodies, and response times. Replay any request with r.
Features
| Feature |
Description |
| Webhook Relay |
Forward external webhooks to localhost over WebSocket |
| Request Replay |
Re-send any webhook with one keypress |
| Path Routing |
Route different paths to different local ports |
| TUI Inspector |
Live request stream with search/filter |
| Auth Tokens |
Secure your relay with bearer tokens |
| TLS/HTTPS |
Native TLS support for production |
| Config Files |
YAML config for both server and client |
| Graceful Shutdown |
Clean disconnect on Ctrl+C |
| SQLite Storage |
Persistent request history across restarts |
Configuration
Create hookshot.yaml:
server:
port: 8080
public_url: https://hooks.yoursite.com
token: your-secret-token # Recommended: require auth
database: /var/lib/hookshot/requests.db # Optional: persist requests
# max_body_size: 10485760 # Optional: 10MB default
# max_message_size: 10485760 # Optional: 10MB default
# allowed_origins: [] # Optional: restrict WebSocket origins
# tls_cert: /path/to/cert.pem # Optional: enable HTTPS
# tls_key: /path/to/key.pem
client:
server: https://hooks.yoursite.com
token: your-secret-token
target: http://localhost:3000
# Or route by path:
# routes:
# - path: /api
# target: http://localhost:3000
# - path: /webhooks
# target: http://localhost:4000
CLI Reference
# Server
hookshot server [flags]
-p, --port int Port (default 8080)
--public-url Public URL for display
--token Auth token (recommended for production)
-d, --database SQLite database path (default: in-memory)
--max-body-size Max webhook body size (default 10MB)
--max-message-size Max WebSocket message size (default 10MB)
--allowed-origins Allowed WebSocket origins (default: all)
--tls-cert TLS certificate path
--tls-key TLS key path
# Client
hookshot client [flags]
-s, --server Server URL (required)
-t, --target Local target (default localhost:3000)
--token Auth token
--tui Interactive mode
-v, --verbose Show request/response bodies
# Utilities
hookshot requests --server URL --tunnel ID # List recent requests
hookshot replay --server URL --tunnel ID --request ID # Replay a request
API
| Endpoint |
Description |
POST /t/{tunnel}/* |
Receive webhooks |
GET /api/tunnels/{id}/requests |
List stored requests |
POST /api/tunnels/{id}/requests/{req}/replay |
Replay a request |
GET /health |
Health check |
Security
Hookshot is hardened for internet exposure:
- UUID tunnel IDs — 36-character IDs prevent guessing
- Request ownership — Replay API validates tunnel ownership
- Size limits — Configurable body/message limits (default 10MB)
- Origin validation — WebSocket origin checks
- No query tokens — Auth only via headers (no URL leakage)
For production, always set --token and use TLS.
Deploy
Fly.io (recommended)
One command to deploy your own relay:
# Clone and deploy
git clone https://github.com/lance0/hookshot.git
cd hookshot
fly launch --copy-config --name my-hookshot
# Create persistent volume for request history
fly volumes create hookshot_data --size 1
# Set your auth token
fly secrets set TOKEN=your-secret-token
# Deploy and get your URL
fly deploy
fly status
Your relay is now live at https://my-hookshot.fly.dev. Connect with:
hookshot client -s https://my-hookshot.fly.dev --token your-secret-token
Docker
# With persistent storage
docker run -p 8080:8080 -v hookshot-data:/data hookshot server --token=secret
# Or in-memory only
docker run -p 8080:8080 -e DATABASE_PATH= hookshot server --token=secret
Build from source
git clone https://github.com/lance0/hookshot.git
cd hookshot
go build -o hookshot ./cmd/hookshot
./hookshot server --port 8080
vs ngrok
|
ngrok |
Hookshot |
| Price |
$8-20/mo |
Free |
| Self-hosted |
No |
Yes |
| Replay |
Paid add-on |
Included |
| Path routing |
No |
Yes |
| Your data |
Their servers |
Your servers |
Hookshot is purpose-built for webhook development. For general HTTP tunneling or TCP proxying, ngrok may be a better fit.
License
MIT — use it however you want.
Built with Go. Themed with Catppuccin.