Burp Suite, but for CLI / TUI applications.
β οΈ This entire app was vibed out in an afternoon, bugs exist, issues help me fix them :)
A split-pane terminal app that captures, inspects, and tampers with the network traffic of a command-line program. The left pane is a real terminal running your target CLI; the right pane is a live view of the HTTP(S), HTTP/2, gRPC, and WebSocket traffic it generates β which you can pause, edit, drop, replay, and export.
ββ terminal (target) ββββββββββββ Traffic (54) βββββββββββββββ
β $ gh pr list ββ done GET /user β
β ... ββ β done POST /graphql β
β ββ PAUSED POST /v1/messages β
β ββ active GET /events/streamβ
β ββ β
β ββ βΆ POST /v1/messages β
β ββ [e]dit [f]orward [d]rop β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
? for help Β· Ctrl+A w switch pane Β· Ctrl+A i arm intercept Β· Ctrl+A q quit
πΈ See the visual tour for a screenshot of every feature, or the docs for the guides: getting started Β· keybindings Β· scope Β· intercepting Β· repeater Β· exporting.
Browsers make traffic interception easy β you point them at a proxy and install a cert. Command-line tools don't cooperate that nicely. cli-capture solves that: it launches your target with its network environment rewritten so its traffic flows through a local man-in-the-middle proxy, decrypts TLS, parses the common protocols, and gives you a TUI to watch and modify it all in flight.
Use it to:
- see exactly what a CLI sends to its backend (headers, bodies, gRPC messages),
- intercept and modify requests or responses on the fly,
- replay a request, export it as curl / HAR / plain text,
- flag interesting requests during a run and export your shortlist.
- Capture two ways β proxy-env injection (no privileges) or transparent
redirect (Linux, for apps that ignore
HTTP_PROXY). - TLS MITM β generates a local CA and mints leaf certs on the fly; the target trusts it automatically because the CA is injected per-launch.
- Protocols β HTTP/1.1, HTTP/2, gRPC, WebSocket, with a raw-TCP passthrough fallback so nothing is ever dropped.
- Intercept & tamper β pause matching flows and edit / forward / drop. Whole-body for HTTP/1 and unary HTTP/2, per-message for streaming gRPC, per-frame for WebSocket, plus live WebSocket frame injection.
- Scope engine β decide what to intercept and what to MITM with composable include/exclude rules across host, path, method, protocol, body.
- Readable bodies β gzip / deflate / br / zstd are decompressed, JSON is pretty-printed and syntax-highlighted, binary falls back to a hex dump.
- Flag, filter, search β mark interesting flows, filter to just those, or substring-search the list.
- Replay & export β resend a flow to its origin; export as curl, HAR 1.2, or plain text (single flow or all flagged).
- Sessions β save/load the whole capture to JSON.
- A real terminal β the left pane is a full VT100/xterm emulator, so full-screen TUI targets render correctly.
brew install citizen-123/tap/cli-captureOr tap first, then install:
brew tap citizen-123/tap
brew install cli-captureUpgrade later with brew upgrade cli-capture.
Requires Go 1.26+. Installs the cli-capture binary into $(go env GOBIN)
(or $(go env GOPATH)/bin):
go install github.com/citizen-123/cli-capture/cmd/cli-capture@latestMake sure that directory is on your PATH, then run cli-capture -version to
confirm.
Grab a tarball/zip for your OS and architecture from the
Releases page (linux,
macOS, and Windows on amd64/arm64), verify it against checksums.txt, and drop
the binary on your PATH:
# example: Linux amd64
tar -xzf cli-capture_*_linux_amd64.tar.gz
install -m755 cli-capture ~/.local/bin/
cli-capture -versiongit clone https://github.com/citizen-123/cli-capture
cd cli-capture
go build -o cli-capture ./cmd/cli-captureThat produces a single static-ish binary ./cli-capture. Move it onto your
PATH if you like (install -m755 cli-capture ~/.local/bin/).
Run the tests:
go test ./... # or: go test -race ./..../cli-capture -- curl https://example.comEverything after -- is the target command and its arguments. On first run it
creates ~/.cli-capture/ (CA + config); subsequent runs reuse it.
Press ? at any time for the full keyboard reference.
The target is launched with these environment variables injected, so well-behaved clients route through the proxy and trust the MITM CA β for that child process only, nothing system-wide:
| Variable(s) | Purpose |
|---|---|
HTTP_PROXY HTTPS_PROXY ALL_PROXY |
route traffic through the proxy |
SSL_CERT_FILE CURL_CA_BUNDLE |
trust the CA (OpenSSL, curl) |
REQUESTS_CA_BUNDLE |
Python requests |
NODE_EXTRA_CA_CERTS |
Node.js |
GIT_SSL_CAINFO |
git |
Tools that ignore proxy env (statically-linked binaries, raw sockets) won't be caught this way β use transparent mode for those.
cli-capture [flags] -- <command> [args...]
Everything after -- is the target program to launch and monitor.
| Flag | Default | Description |
|---|---|---|
-listen <addr> |
127.0.0.1:0 |
Proxy listen address. :0 picks a free port. |
-dir <path> |
~/.cli-capture |
Data directory: CA, sessions, exports, log. |
-config <files> |
~/.config/cli-capture/config.json |
Config file(s) or preset name(s); repeatable and comma-separated, merged in order. See docs/configuration.md. |
-theme <name> |
dark |
dark, light, high-contrast, or none. Overrides the config file. |
-scope <specs> |
(all) | Comma-separated specs for which flows to intercept. Default intercepts everything (once armed). |
-exclude <specs> |
β | Comma-separated specs to never intercept; wins over -scope. |
-last-match |
off | Evaluate all scope rules and take the last match instead of the first. |
-mitm <specs> |
(all) | MITM (decrypt) only these TLS hosts. |
-no-mitm <specs> |
β | Pass these TLS hosts through without decrypting (blind tunnel, still logged). |
-load <file> |
β | Preload a saved capture session (JSON) into the flow list. |
-leader <key> |
ctrl+a |
Prefix key for cli-capture's own commands. ctrl+a β¦ ctrl+z, or ctrl+space. Move it if your target app needs the default. |
-transparent <addr> |
off | Also run a transparent-redirect listener at this address (Linux, needs root + nftables/iptables). |
-transparent-uid <uid> |
-1 |
The uid whose traffic the redirect rule targets. |
-transparent-apply |
off | Actually install/remove the redirect rules (needs root); otherwise they're just logged for you to run. |
-scope, -exclude, -mitm, and -no-mitm all take comma-separated specs
of the form [!][field:]pattern:
| Part | Values |
|---|---|
! |
negate the match |
field: |
host (default) Β· path Β· method Β· proto Β· body Β· any |
pattern |
~regex Β· =exact Β· contains */? β glob Β· otherwise substring |
Examples:
# intercept only GitHub's API, minus a noisy endpoint
./cli-capture -scope '*.github.com' -exclude 'path:/telemetry' -- gh pr list
# pause every POST, and anything carrying a token in its body
./cli-capture -scope 'method:=POST,body:token' -- some-cli
# monitor everything, but never decrypt the bank
./cli-capture -no-mitm '*.bank.example' -- some-cliPostures fall out of the defaults: give -scope and you get an allowlist
(intercept only those); give only -exclude and you get a denylist
(intercept everything except those). Specs within one flag are OR'd, and a flag
given twice keeps only the last value β see docs/scope.md.
Keys use a tmux-style leader (Ctrl+A) so the terminal pane keeps all of its
own keys β only the leader is intercepted (press it twice to send a literal
Ctrl+A to the target). Press ? for the in-app help overlay. The leader
and every binding below are configurable β see
docs/configuration.md.
If your target app wants Ctrl+A β readline's beginning-of-line, or tmux itself
β move the leader instead of fighting it. Set keys.leader in your config to any
ctrl+a β¦ ctrl+z; the help overlay follows whatever you pick, and the rebound
key is released back to the target.
Global β leader then:
| Key | Action |
|---|---|
w |
switch pane (terminal β traffic) |
< / > |
shrink / grow the terminal pane |
i / r |
toggle intercept: requests / responses |
s |
save session to JSON |
h |
export session as HAR |
f |
export flagged flows β flagged.txt |
m |
toggle mouse capture (off restores native text selection) |
? |
toggle help |
q |
quit |
With mouse capture on (the default), clicks move pane focus and select flows β
clicking the already-selected flow opens it β and the wheel scrolls the traffic
list or pages the hosted app. Toggle it off (leader m) to use your terminal's
own text selection and copy-paste.
Traffic pane (plain keys β it never forwards to the target):
| Key | Action |
|---|---|
j / k |
move selection (list scrolls to follow) |
space |
flag / unflag the selected flow |
F |
show flagged only |
o |
cycle sort: none / status / size (surfaces attack outliers) |
/ |
filter by host / method / path / status |
enter |
open the detail view |
e / f / d |
on a PAUSED flow: edit / forward / drop |
x |
resend the selected flow to its origin |
c |
export the selected flow as a curl command |
n / N |
inject a WebSocket frame (clientβserver / serverβclient) |
? |
toggle help |
Each row shows the HTTP status code (colored by class) and response size,
so you can triage at a glance; attack results also show the payload that produced
them, and o sorts by status or size to surface the outlier.
Repeater (R): the response is shown inline β Tab cycles request β
payloads β response, Ctrl+O cycles attack mode, Ctrl+S sends.
Detail view (enter): j/k scroll Β· s save this flow to .txt Β·
esc/q back.
Editor (while intercepting): Ctrl+S forward/send the edited bytes Β·
Ctrl+L fix Content-Length Β· Esc cancel.
Interception is off by default β cli-capture only records until you arm it:
Ctrl+A wto focus the traffic pane,Ctrl+A ito arm request interception (Ctrl+A rfor responses). The status bar showsreq:on.- The next in-scope flow shows
PAUSED. eopens the editor β change bytes βCtrl+Sforwards the edited version (ddrops it,fforwards unchanged).
Per protocol: HTTP/1 edits the full request/response; HTTP/2 edits the body
(unary buffered, gRPC per-message in flight so streams don't stall); WebSocket
edits per frame and supports live injection (n/N).
For targets that ignore HTTP_PROXY, redirect their traffic at the kernel level.
Run the target under a dedicated uid so only its traffic is redirected (and
so the proxy's own upstream connections aren't looped back):
# let cli-capture install & clean up the rules itself (opt-in, root only)
sudo cli-capture -transparent 127.0.0.1:8081 -transparent-uid 1500 -transparent-apply -- ./targetIt auto-detects nftables (preferred) or iptables, flushes stale rules
first, rolls back a partial apply, and removes the rules on exit (including
SIGINT/SIGTERM). Without -transparent-apply it just logs the exact commands for
you to run. The pre-redirect destination is recovered via SO_ORIGINAL_DST
(IPv4 and IPv6), then runs through the same MITM/scope/intercept pipeline.
- Session β
Ctrl+A swrites all flows to~/.cli-capture/session.json;-load <file>reloads one (flags survive the round-trip). - curl β
cwrites the selected flow's request as a curl command. - HAR 1.2 β
Ctrl+A hwrites the whole session ascapture.har. - Text β
sin the detail view writes the viewed flow toflow-<id>.txt;Ctrl+A fwrites all flagged flows toflagged.txt.
Note: exports are verbatim β they include auth headers, cookies, and full request bodies (e.g. an app's system prompt). Treat the files as sensitive.
The load-bearing idea: keep transport capture (getting the bytes) separate
from protocol parsing (understanding them), so adding a protocol is one file
implementing protocol.Protocol plus one Register call.
cmd/cli-capture entrypoint & wiring
internal/runner PTY-hosted target + proxy/CA env injection
internal/terminal full VT100/xterm emulator (vt10x) for the left pane
internal/proxy HTTP CONNECT proxy + TLS MITM + HTTP/2 bridge
internal/proxy/ca CA generation + on-the-fly leaf signing
internal/protocol parsers: http/1.1, http/2, gRPC, websocket, raw tcp
internal/capture Flow/Message model, thread-safe Store, session save/load,
Content-Encoding decompression
internal/intercept pause/edit queue + live-session registry (scope via internal/scope)
internal/scope composable match engine (field Γ strategy Γ include/exclude)
internal/replay resend a captured HTTP/gRPC flow to its origin
internal/export curl / HAR / plain-text rendering
internal/transparent Linux SO_ORIGINAL_DST + nftables/iptables redirect
internal/tui bubbletea split-pane UI: editor, detail view, filter, help
After TLS termination the proxy branches by ALPN: h2 goes through Go's
http2.Server β ReverseProxy β http2.Transport (streaming-safe); everything
else takes the byte-level Protocol.Handle path.
Built with bubbletea / lipgloss, creack/pty, hinshun/vt10x, and golang.org/x/net/http2.
- HTTP/2 header editing isn't wired β h2 modification is body-only (HTTP/1 edits the full request/response).
- The editor is a text field, so editing binary/protobuf bodies is clumsy (JSON/text is fine).
- HTTP/2 responses that are only observed (not intercepted) are streamed without buffering, so their bodies aren't captured for the detail view.
- Transparent mode needs
CAP_NET_ADMIN; its privileged redirect path can't be exercised without root, so it's less battle-tested than the proxy-env path. - Some native binaries pin certs or read only the system trust store and won't honor the injected CA β transparent mode helps, cert pinning doesn't.
- clipboard integration for curl export
- flow-list column sorting
- HTTP/2 header editing
- runtime-loadable protocol plugins
Do whatever you want with it. It was vibed out in an afternoon.
