A programmable HTTP/S traffic inspector & replay tool — a self-hosted Charles Proxy / Postman hybrid with a scriptable pipeline and a terminal UI.
Hermes sits as a local MITM proxy, intercepts HTTP and HTTPS traffic, lets you inspect it live, persist sessions, write transform scripts, and replay requests — all without leaving the terminal.
- MITM Proxy — Intercepts HTTP and HTTPS via dynamic certificate injection
- Live TUI — Real-time traffic viewer (Ratatui): scrollable list + full detail panel
- Session Storage — Persists captures to an embedded
sleddatabase (no external deps) - Request Replay — Re-send any captured request; diff original vs replayed response
- Rhai Scripting — Transform requests per-URL: strip headers, inject values, mock responses, or drop requests
- Binary Safety — Binary bodies (images, gzip, PDFs…) are detected and never rendered as text
Cargo workspace of five crates:
hermes/
├── cli/ # Entry point — clap CLI, wires all crates together
├── proxy/ # Core MITM proxy (raw TCP + rustls, no hyper)
├── tui/ # Ratatui interface (two-panel layout, async event loop)
├── scripts/ # Rhai scripting engine
└── store/ # sled-backed session persistence
Data flow:
Browser / curl
│ (proxy at 127.0.0.1:8080)
▼
proxy ← decrypts TLS, runs Rhai scripts
│ mpsc channel
▼
relay task
├──→ tui (Ratatui, live view)
└──→ store (sled, persisted)
Stack: tokio · rustls + rcgen · ratatui + crossterm · rhai · sled + bincode · clap
# Build
cargo build --release
# Run (listens on 127.0.0.1:8080 by default)
cargo run --release -- runOn first run a root CA is generated (hermes-ca.crt). Install it in your trust store to intercept HTTPS:
# Linux (NSS)
certutil -d sql:$HOME/.pki/nssdb -A -t "CT,C,C" -n "Hermes Proxy CA" -i hermes-ca.crt
# macOS
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain hermes-ca.crtThen send traffic through the proxy:
curl -x http://localhost:8080 --cacert hermes-ca.crt https://httpbin.org/gethermes run [--bind 127.0.0.1:8080] [--db .hermes-sessions] [--scripts .hermes-scripts]
hermes list [--db .hermes-sessions]
hermes replay <UUID> [--db .hermes-sessions] [--diff]
replay re-sends the original request and prints the new response. With --diff (default) it shows a line-by-line body diff.
| Key | Action |
|---|---|
↑ / k · ↓ / j |
Navigate list / scroll detail |
PgUp / PgDn |
Jump 10 rows |
Tab |
Toggle focus between panels |
q |
Quit |
Method colors: GET green · POST blue · PUT/PATCH yellow · DELETE red.
Status colors: 2xx green · 3xx blue · 4xx yellow · 5xx red.
Place .rhai files in .hermes-scripts/. The filename stem is used as a URL glob pattern:
| Filename | Matches |
|---|---|
*domain*.rhai |
Any URL containing domain |
*.rhai |
Every request |
Each script exports fn handle(req) receiving req.method, req.url, req.headers.
Return values:
| Value | Effect |
|---|---|
"passthrough" |
Forward unchanged |
"drop" |
Drop; proxy returns 502 |
#{ modify_headers: [[name, val], …] } |
Replace request headers |
#{ mock: true, status: N, body: "…", headers: […] } |
Return synthetic response |
Example — strip Authorization from your API Requests:
// *api.domain*.rhai
fn handle(req) {
let new_headers = [];
for pair in req.headers {
if pair[0].to_lower() != "authorization" { new_headers.push(pair); }
}
return #{ modify_headers: new_headers };
}
Example — mock httpbin status endpoints with 418:
// *httpbin.org/status/*.rhai
fn handle(req) {
if req.method != "GET" { return "passthrough"; }
return #{ mock: true, status: 418, body: "I'm a teapot (mocked by Hermes)" };
}
Scripts run via spawn_blocking so they never block the async runtime. The engine enforces a 100k-operation limit.
Transactions are stored in sled under .hermes-sessions/, serialized with bincode. Request and response bodies are captured up to 16 KiB; larger payloads are truncated at the proxy.
Transaction { id, timestamp, request { method, url, headers, body }, response?, duration_ms? }
- CA bootstrap — Generates
hermes-ca.crt/hermes-ca.keyon first run (reused on subsequent runs). - CONNECT — On
CONNECT host:443, Hermes replies200 Connection Establishedand intercepts the stream. - Server-side TLS — Dynamically issues a leaf cert for the target hostname, signed by the Hermes CA, and does a TLS handshake with the client. Certs are cached in memory per hostname.
- Client-side TLS — Opens a real TLS connection to the upstream using Mozilla's root store (
webpki-roots). - Inspection — Reads decrypted HTTP, runs the script pipeline, captures request/response (capped at 16 KiB), emits the
Transaction, and returns the response.
Stderr is redirected to <db>.log (e.g. .hermes-sessions.log) before the TUI starts, preventing proxy errors from corrupting the display:
tail -f .hermes-sessions.log