A zero-config HTTPS traffic inspector for the terminal.
Wrap any CLI command — curl, aws, python, node — and see every request and response in real time, no browser or GUI required.
httpmon curl https://api.github.com/users/octocat
=== REQUEST #1 ===
Time: 14:32:01
GET https://api.github.com/users/octocat HTTP/1.1
Host: api.github.com
Headers:
User-Agent: curl/8.4.0
Accept: */*
=== RESPONSE #1 ===
HTTP/1.1 200 OK (212ms)
Headers:
Content-Type: application/json; charset=utf-8
X-Ratelimit-Remaining: 59
--- RESPONSE #1 body ---
{"login":"octocat","id":583231,...}
---------------------------------------------------- #1 end
httpmon starts a local MITM proxy, generates a self-signed CA on the fly, and injects the proxy address and CA certificate into the wrapped command's environment.
No system configuration is changed, and the temporary CA bundle is removed when the command exits.
- HTTP — forwarded and logged transparently
- HTTPS — intercepted via TLS termination with a per-host certificate signed by the ephemeral CA
- HTTP/2 and gRPC — negotiated with the client via ALPN and with the upstream independently; trailers are carried through, and gRPC calls show their status
- WebSocket (
wss://andws://) — upgrade handshake is proxied and frames are spliced bidirectionally - Body display — compressed responses (gzip, deflate, brotli, zstd) are decompressed automatically; only the first 1 KB is shown by default, the full stream is forwarded unmodified
- HAR export — all captured traffic can be written to an HTTP Archive (
.har) file on exit
go install github.com/hxddh/https-traffic-inspector@latestDownload the latest binary for your platform from the Releases page:
| Platform | File |
|---|---|
| macOS Apple Silicon (M1/M2/M3) | httpmon-<version>-macos-arm64 |
| macOS Intel | httpmon-<version>-macos-amd64 |
| Linux x86-64 | httpmon-<version>-linux-amd64 |
| Linux arm64 (Graviton, Pi) | httpmon-<version>-linux-arm64 |
| Windows x86-64 | httpmon-<version>-windows-amd64.exe |
# macOS / Linux — replace <version> with the release tag, e.g. v1.0.1
chmod +x httpmon-v1.0.1-macos-arm64
sudo mv httpmon-v1.0.1-macos-arm64 /usr/local/bin/httpmongit clone https://github.com/hxddh/https-traffic-inspector
cd https-traffic-inspector
go build -o httpmon .
sudo mv httpmon /usr/local/bin/httpmon [options] <command> [args...]
httpmon --replay <file> [--replay-target <url>]
| Flag | Default | Description |
|---|---|---|
--port |
0 |
Proxy listen port. 0 picks a random free port. |
--listen |
127.0.0.1 |
Address the proxy listens on. Only loopback is safe: any other address lets anyone who can reach it use the proxy, including to reach services bound to this machine's loopback. |
--filter |
(none) | Case-insensitive substring; only matching requests are logged. Non-matching traffic is still proxied. |
--format |
text |
Output format: text or json (NDJSON). |
--cert-ttl |
1h |
How long per-host TLS certificates are cached. 0 disables caching. |
--ui |
false |
Launch the interactive terminal UI. |
--record |
(none) | Append recorded traffic to an NDJSON file. |
--har |
(none) | Write captured traffic as HAR 1.2 JSON on exit. |
--replay |
(none) | Replay a recorded file (no proxy or command needed). |
--replay-target |
(none) | Override the base URL when replaying (e.g. https://staging.example.com). |
--replay-delay |
0 |
Pause between replayed requests. |
--replay-fail-on-diff |
false |
Exit with code 2 if any replayed response differs from the recording. |
--insecure-upstream |
false |
Skip TLS certificate verification for upstream servers. |
--upstream-proxy |
(env) | Proxy for httpmon's own upstream requests. Defaults to HTTP_PROXY/HTTPS_PROXY from the environment. |
--max-body |
1000 |
Max bytes of each body shown in output. |
--max-capture |
1048576 |
Max bytes of each body kept for --record / --har and decompression. |
--version |
Print version and exit. |
| Code | Meaning |
|---|---|
| (subprocess code) | The wrapped command's own exit code is passed through. |
1 |
httpmon failed to start, or writing the HAR file failed. In replay mode: the recording could not be read or parsed. |
2 |
Replay mode only, with --replay-fail-on-diff: at least one response differed from the recording. |
httpmon handles proxy configuration automatically for common tools:
| Tool | What is injected |
|---|---|
| curl | -x http://127.0.0.1:<port> --cacert <ca> (plus CURL_CA_BUNDLE for curl run from scripts) |
| aws / boto3 | AWS_CA_BUNDLE |
| Python requests / pip | REQUESTS_CA_BUNDLE, PIP_CERT |
| Node.js | NODE_EXTRA_CA_CERTS, and NODE_USE_ENV_PROXY=1 so the built-in fetch uses the proxy |
| git | GIT_SSL_CAINFO |
| cargo / Deno | CARGO_HTTP_CAINFO / DENO_CERT |
gRPC C-core (Python grpcio, Ruby, PHP) |
GRPC_DEFAULT_SSL_ROOTS_FILE_PATH |
| Any HTTP-proxy-aware tool | HTTP_PROXY, HTTPS_PROXY, SSL_CERT_FILE |
These override any inherited value of the same name, which would otherwise
point the tool at a bundle that lacks httpmon's CA. JVM tools read a
truststore rather than a PEM file and are not configured automatically.
NODE_USE_ENV_PROXY needs Node 22.21 or 24; older Node versions ignore it,
and their built-in fetch bypasses httpmon.
httpmon also clears NO_PROXY for the wrapped command. A host named in an
inherited no-proxy list would otherwise skip httpmon and go uncaptured with no
warning. httpmon's own upstream requests still honour the real NO_PROXY, so
routing is unchanged — the traffic just passes through httpmon on the way.
The CA bundle written to disk contains both the proxy CA and the system's trusted CA store (when found), so direct TLS connections made by the subprocess also work correctly.
Focus on the traffic you care about. Non-matching requests are still proxied silently.
# Only log requests to /api paths
httpmon --filter /api curl https://example.com/api/v1/users
# Only log requests to a specific domain
httpmon --filter stripe.com python3 payment_service.py
# Watch S3 uploads only
httpmon --filter s3 aws s3 sync ./dist s3://my-bucket/Emits one NDJSON object per request and one per response, correlated by id / req_id.
httpmon --format json curl https://api.github.com | jq '.url'
httpmon --format json --filter /users python3 app.py | jq 'select(.status) | .status'Request fields: id, time, method, url, proto, host, headers, body
Response fields: req_id, status, proto, headers, body
A full-screen terminal UI powered by Bubble Tea.
httpmon --ui curl https://api.github.com
httpmon --ui aws s3 ls httpmon proxy :41873 3 requests
# Method Status URL Duration
1 GET 200 api.github.com/users/octocat 245ms
2 ▶ POST 201 api.github.com/repos 123ms
3 GET … api.github.com/rate_limit pending
──────────────────────────────────────────────────────────────
━━ REQUEST #2 ━━
POST https://api.github.com/repos
Host: api.github.com Time: 14:32:01
━━ RESPONSE: 201 Created (123ms) ━━
{"id":12345,"name":"my-repo",...}
──────────────────────────────────────────────────────────────
[↑↓/jk] navigate [enter] detail [g/G] top/bottom [q] quit
| Key | Action |
|---|---|
↑ / k |
Move up |
↓ / j |
Move down |
Enter / Space |
Toggle detail panel |
g / G |
Jump to first / last entry |
PgUp / PgDn, Ctrl-U / Ctrl-D |
Scroll the detail panel |
Home / End |
Jump to top / bottom of the detail panel |
Esc |
Close detail panel |
q |
Quit (also terminates the subprocess) |
A ▼ in the status bar means there is more content below in the detail panel. Selecting a different entry returns the panel to the top; a response arriving for the entry you are reading does not move it.
Pending entries (awaiting response) are highlighted and update in place when the response arrives.
Subprocess stdout/stderr is captured during the TUI session and printed to stderr after exit.
httpmon automatically decompresses gzip, deflate, brotli (br) and zstd encoded response bodies before display. Most HTTPS APIs compress their responses — GitHub and anything behind Cloudflare default to br — so you see the decoded JSON or HTML rather than a binary placeholder.
Chained encodings (Content-Encoding: gzip, br) are decoded layer by layer. An encoding httpmon does not recognise is shown as a [<encoding>, N+ bytes] placeholder rather than being printed raw.
=== RESPONSE #1 ===
HTTP/1.1 200 OK (212ms)
Headers:
Content-Encoding: gzip
Content-Type: application/json
--- RESPONSE #1 body ---
{"login":"octocat","id":583231,...} ← decoded automatically
The full compressed stream is still forwarded to the subprocess unmodified.
When only part of a large body was captured, the decoded prefix is shown followed by a … [truncated] marker, so a cut-off body is never mistaken for a complete one. Raise --max-capture to decode more.
Streaming responses and uploads are forwarded as they arrive rather than being buffered. Server-Sent Events, chunked uploads, long-polling and slow downloads all pass through with no added latency — the body is sampled for logging alongside delivery, not ahead of it.
httpmon curl -N https://api.example.com/events # SSE arrives event by eventThe body of a stream is logged once the stream ends; its headers appear immediately.
httpmon offers HTTP/2 to the wrapped command through ALPN and negotiates it
with the upstream separately, so each side gets the best protocol it
supports — an HTTP/2 client talking to an HTTP/1.1-only server works, and so
does the reverse. Response trailers are forwarded, and TE: trailers is kept,
which is what gRPC needs to work end to end.
Inside a CONNECT tunnel httpmon also accepts cleartext HTTP/2 with prior
knowledge. That is how an insecure gRPC client (grpc.insecure_channel,
grpc.WithTransportCredentials(insecure.NewCredentials())) talks through a
proxy, and the request goes upstream the same way.
gRPC messages are length-prefixed binary, so bodies are summarised rather than printed, and the call's status is decoded from the trailers:
=== RESPONSE #2 ===
HTTP/2.0 200 OK (1ms)
Headers:
Content-Type: application/grpc
--- RESPONSE #2 body ---
[gRPC: 1 message, 0 payload bytes]
Trailers:
Grpc-Status: 5
gRPC status: 5 NOT_FOUND
--format json reports trailers in a trailers field, and --record in
resp_trailers. gRPC C-core clients (Python grpcio, Ruby, PHP) get
GRPC_DEFAULT_SSL_ROOTS_FILE_PATH so they trust httpmon's CA.
wss:// connections established via CONNECT tunnels, and plaintext ws:// upgrades sent to the proxy, are handled transparently. The upgrade handshake is proxied and logged; after the 101 response, frames are spliced bidirectionally between client and upstream without buffering.
httpmon node ws-client.js # wss:// connections work automatically
httpmon --ui node ws-client.js # upgrade handshake visible in TUISave all intercepted traffic to an NDJSON file for later replay or analysis.
# Record a curl session
httpmon --record traffic.ndjson curl https://api.github.com/users/octocat
# Record while watching the TUI
httpmon --ui --record session.ndjson python3 app.py
# Record only filtered traffic
httpmon --filter /api --record api.ndjson curl https://example.com/api/v1Each line of the output file is a complete request/response pair:
{
"id": 1,
"time": "2026-05-27T14:32:01Z",
"method": "GET",
"url": "https://api.github.com/users/octocat",
"req_headers": {"Accept": "*/*", "User-Agent": "curl/8.4.0"},
"req_body": "",
"status": 200,
"status_text": "200 OK",
"resp_headers": {"Content-Type": "application/json"},
"resp_body": "{\"login\":\"octocat\",...}",
"duration_ms": 245
}When a stored body is only a prefix — it exceeded --max-capture, or the
stream was cut off before it ended — req_body_truncated or
resp_body_truncated is true, and replay compares only that prefix. HAR
entries carry the same information as a comment on the body.
Re-issue recorded requests and compare the responses. Useful for regression testing or comparing environments.
# Replay against the original service
httpmon --replay traffic.ndjson
# Replay against a different environment
httpmon --replay traffic.ndjson --replay-target https://staging.example.com
# Replay with realistic pacing
httpmon --replay traffic.ndjson --replay-delay 500ms
# Gate a CI job on the responses being unchanged (exits 2 on any difference)
httpmon --replay traffic.ndjson --replay-target https://staging.example.com --replay-fail-on-diff
# Machine-readable results, one JSON object per replayed request
httpmon --replay traffic.ndjson --format json | jq 'select(.status_match | not)'Output for each request:
── REPLAY #1 ──
Original: GET https://api.example.com/users → 200 OK
Replaying: GET https://staging.example.com/users
Status: ✓ recorded=200 actual=200 (43ms)
Body: ✓ unchanged
── REPLAY #2 ──
Original: POST https://api.example.com/repos → 201 Created
Replaying: POST https://staging.example.com/repos
Status: ✓ recorded=201 actual=201 (67ms)
Body: ≠ changed
now: {"id":99,"name":"my-repo","created_at":"2026-05-27"}
Write all captured traffic to an HTTP Archive file (HAR 1.2) on exit. HAR files can be imported into Chrome DevTools, Charles Proxy, Postman, and other analysis tools.
# Capture to HAR
httpmon --har session.har curl https://api.github.com
# Combine with filter and TUI
httpmon --ui --har api.har --filter /api python3 app.pyOpen session.har in Chrome DevTools → Network tab → Import, or drag it into the HAR Analyzer.
# Inspect any curl request
httpmon curl https://api.github.com/repos/golang/go
# Debug AWS calls with S3 bucket/key highlighted
httpmon aws s3 cp ./file.txt s3://my-bucket/uploads/
# Tail only API traffic from a Python service
httpmon --filter /api python3 server.py
# Stream traffic as JSON into a log file
httpmon --format json node app.js >> traffic.log
# Record prod traffic, compare against staging
httpmon --record prod.ndjson curl https://api.prod.example.com/health
httpmon --replay prod.ndjson --replay-target https://api.staging.example.com
# Save a HAR file for sharing or import into DevTools
httpmon --har trace.har curl https://api.github.com
# Full session: TUI + HAR + filter
httpmon --ui --har session.har --filter /api python3 app.py
# Accept connections from a container or VM (exposes the proxy to the network)
httpmon --listen 0.0.0.0 --port 8080 python3 app.py- Go programs skip the proxy for loopback targets. Go's
ProxyFromEnvironment, which grpc-go andnet/httpboth use, never proxieslocalhostor127.0.0.1, so a Go client calling a local server is not captured. Clients in other languages are unaffected. - gRPC bodies are summarised, not decoded. httpmon has no
.protoschemas, so it shows message counts and sizes. Recordings of gRPC calls store that summary, so--replayskips them. - Only HTTP is inspected. A
CONNECTtunnel carrying another protocol (SSH, SMTP, database wire protocols) is relayed untouched and noted as such. A server-first protocol waits up to one second before httpmon concludes the client is not going to speak. - WebSocket over HTTP/2 (RFC 8441) is not supported. Clients fall back to an HTTP/1.1 connection for the upgrade, which works.
- Cleartext HTTP/2 cannot be sent through
--upstream-proxy. An insecure gRPC call to an upstream reached only via another proxy fails. --hardoes not apply to--replay. Replay mode neither starts the proxy nor captures entries.- Bodies are logged when they finish, not when their headers arrive. Request and response headers print as soon as they are seen, so a long-lived stream is visible while it runs, but its body is only shown once the stream ends.
--format jsonand--recordemit one complete record per exchange, so for a stream that record appears at the end. - Bodies are capped. Output shows
--max-bodybytes;--record/--har/ decompression keep--max-capturebytes. Raising--max-captureincreases per-request memory use, since each captured body is buffered in full. - The HAR
bodySizefield reports the transferred size only when the upstream sent aContent-Length, and-1otherwise.timings.sendandtimings.receiveare always-1: httpmon measures the round trip as a whole.
httpmon is a local development and debugging tool. It performs TLS interception using an ephemeral self-signed CA that exists only for the duration of the process. The CA private key is never written to disk. Do not use it in production environments or to intercept traffic you do not own.
Upstream certificates are verified by default. Because httpmon terminates TLS, the wrapped subprocess only ever validates the certificate httpmon presents — it can no longer see the real one. httpmon therefore performs that verification on its behalf, and a failure surfaces as a 502 plus an explanatory line on stderr.
--insecure-upstream disables that check. It is needed for self-signed or internal-CA endpoints, but while it is set nothing in the chain validates the upstream certificate, so the connection is open to interception. Use it only against hosts you control.
MIT