clsniff
v2.0.0
Published
Zero-configuration HTTP/HTTPS traffic sniffer for command-line tools like claude
Maintainers
Readme
clsniff
A zero-configuration HTTP/HTTPS traffic sniffer for command-line tools.
Wrap any command with clsniff and every HTTP/HTTPS request it makes will be captured and saved to a JSON file — including full request/response headers and bodies (SSE responses are captured as an array of parsed events).

Prerequisites
- Node.js 22.12 or later
- mitmproxy — the proxy engine, installed however you like (installation guide).
clsnifflooks for itsmitmdumpexecutable inPATHand in the usual installation directories of each platform; if yours lives elsewhere, point at it with--mitmdump <path>.
How it works
clsniff starts mitmdump(mitmproxy's headless mode) as a subprocess and launches your command as a child process with environment variables injected so it routes traffic through the proxy and trusts its CA certificate.
When a request passes through the proxy, clsniff:
- Captures the full request (method, URL, headers, body)
- Forwards it to the real server without modification
- Captures the full response (status, headers, body or SSE events)
- Forwards the response to the child process without modification
- Writes a JSON file with both sides of the exchange
The child process is completely unaware of the interception. Its stdin, stdout, and stderr are passed through directly.
Getting started
# Install globally
npm install -g clsniff
clsniff --viewer -- claude
# Or run without installing
npx -y clsniff@latest --viewer -- claudeIntercepted requests are saved to ~/.clsniff/ as JSON files, one per request/response pair.
Usage
clsniff [options] -- <command> [args...]The -- separator is required to separate clsniff options from the wrapped command. Everything after it belongs to the command, its own flags included:
clsniff --viewer -- claude --dangerously-skip-permissionsOptions
| Flag | Description | Default |
|---|---|---|
| --output-dir <path> | Directory where session log folders are created | ~/.clsniff |
| --name <name> | Name for the session folder instead of the auto-generated timestamp | (timestamp) |
| --port <number> | Port for the local proxy (0 = OS auto-assign) | 0 |
| --mask-headers <names> | Comma-separated header names to redact in JSON output. Can be repeated. | (none) |
| --exclude <hosts> | Comma-separated hosts to bypass interception entirely (NO_PROXY format). Bypassed hosts get a direct TCP tunnel — no MITM, no logging. Can be repeated. See Writing an --exclude entry. Example: example.com,.datadoghq.com,localhost:5000 | (none) |
| --exclude-url <patterns> | Comma-separated URL substrings whose matching requests are intercepted and forwarded as usual but excluded from the JSON log. Unlike --exclude, this filters by full URL (host + path + query), so you can drop specific endpoints of a host while still capturing the rest. Can be repeated. Example: /api/claude_code/,/api/oauth/validate | (none) |
| --configuration <path-or-url> | Path or http(s) URL of a JSON file providing defaults for outputDir, name, port, maskHeaders, exclude, excludeUrl and mitmdump. Explicit CLI options take precedence. See Configuration file. | (auto-discovered) |
| --no-configuration | Do not load any configuration file, not even the auto-discovered ones. See Automatic discovery. | (off) |
| --mitmdump <path> | Path to the mitmdump executable, or to the directory holding it. Only needed when it is not in PATH nor in a usual installation directory. | (auto-discovered) |
| --install-cert | Install mitmproxy's CA certificate in the system trust store | (off) |
| --viewer | Start the web-based log viewer | (off) |
| --no-open | Do not auto-open the browser when starting the viewer | (off) |
--viewerincludes a Claude tab with enhanced support for parsing and displaying Claude-specific request/response formats, according to https://platform.claude.com/docs/en/api/messages/create
Writing an --exclude entry
example.com— that host on any port. Subdomains are not included..example.com— the host and any subdomain, on any port.localhost:5000— that port only; the rest of the host stays captured.
Hosts match by name and by the address they resolve to, so --exclude 127.0.0.1:5000 also covers a request written as http://localhost:5000.
Configuration file
Instead of passing --output-dir, --name, --port, --mask-headers, --exclude, --exclude-url and --mitmdump on every invocation, you can store them in a JSON file and load it with --configuration <path-or-url>. This is especially handy for a long exclude/excludeUrl list.
{
"outputDir": "/home/me/captures",
"name": "my-session",
"port": 8080,
"maskHeaders": ["authorization", "x-api-key"],
"exclude": ["example.com", ".datadoghq.com", "localhost:5000"],
"excludeUrl": ["/api/claude_code/", "/api/oauth/validate"],
"mitmdump": "~/.local/bin/mitmdump"
}All keys are optional. Any explicit CLI option overrides the value from the file, which in turn overrides the built-in default. Unknown keys are ignored with a warning.
See clsniff.example.json for a ready-made example that keeps Claude Code's /v1/messages traffic while filtering out its telemetry and housekeeping endpoints.
Automatic discovery
When --configuration is omitted, clsniff loads both of these files, if present, and merges them key by key:
~/.clsniff/clsniff.json— for your personal defaults./clsniff.json(current directory) — for per-project settings, overriding your personal defaults
Each key of the per-project file replaces the same key of the personal one; keys it does not define are inherited. Arrays such as exclude are replaced as a whole, not concatenated. For example, with maskHeaders and mitmdump in ~/.clsniff/clsniff.json and only exclude in ./clsniff.json, all three apply.
The paths being used are printed to stderr on startup. If neither file exists, clsniff runs with its built-in defaults as before. An explicit --configuration replaces automatic discovery: only that file is loaded.
Drop your favourite settings in ~/.clsniff/clsniff.json once and you never have to pass them again.
To skip automatic discovery for a single run and use only the built-in defaults plus your CLI options, pass --no-configuration.
Remote configuration
--configuration also accepts an http(s) URL, which is downloaded on every run. Useful for sharing a configuration across a team without committing it to each repo:
clsniff --viewer --configuration https://raw.githubusercontent.com/panicoenlaxbox/clsniff/main/clsniff.example.json -- claudeExamples
Intercept all traffic (everything after -- is passed straight to the command):
clsniff -- claudeIntercept traffic, open the viewer and redact the API key:
clsniff --viewer --mask-headers "authorization" -- claudeLocal traffic is captured by default (e.g. an MCP server listening on http://localhost:5000). To skip all of it, or just one port:
clsniff --exclude localhost,127.0.0.1 -- claude
clsniff --exclude localhost:5000 -- claudeBrowse existing sessions (standalone viewer, no command needed):
clsniff --viewerUse the shared configuration straight from GitHub:
npx --yes clsniff@latest --viewer --configuration https://raw.githubusercontent.com/panicoenlaxbox/clsniff/main/clsniff.example.json -- claudeInstall that configuration as your personal default (PowerShell Core), so you never have to pass it again:
New-Item -ItemType Directory -Force ~/.clsniff | Out-Null
Invoke-RestMethod https://raw.githubusercontent.com/panicoenlaxbox/clsniff/main/clsniff.example.json -OutFile ~/.clsniff/clsniff.jsonFrom then on:
npx --yes clsniff@latest --viewer -- claudeOutput format
Each clsniff invocation creates a new timestamped folder under ~/.clsniff/ (one folder per run):
~/.clsniff/
2026-04-09T14-30-00-000Z/
1744200600123_1.json
1744200601456_2.json
clsniff.logclsniff.log contains timestamped internal messages (proxy startup, per-request entries, errors).
Use --name to replace the timestamp with a name of your own. Already captured sessions can
also be renamed from the viewer. The session being recorded is the only one that cannot be renamed.
Each JSON file contains one request/response pair:
{
"id": 20,
"timestamp": "2026-04-09T14:53:19.651Z",
"duration_ms": 1936,
"request": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages?beta=true",
"headers": {
"authorization": "Bearer sk-ant-...",
"content-type": "application/json",
"anthropic-version": "2023-06-01"
},
"body": {
"model": "claude-sonnet-4-6",
"stream": true,
"messages": [
{ "role": "user", "content": "Hello" }
]
}
},
"response": {
"status": 200,
"headers": {
"content-type": "text/event-stream; charset=utf-8",
"request-id": "req_011CZpbpzFt2wEZFQmZAyayD"
},
"body": [
{ "event": "message_start", "data": { "type": "message_start", ... } },
{ "event": "content_block_delta", "data": { "type": "content_block_delta", ... } },
{ "event": "message_stop", "data": { "type": "message_stop" } }
]
}
}Certificate management
On first run, mitmproxy generates a CA (Certificate Authority) key pair and stores it in ~/.mitmproxy/:
~/.mitmproxy/
mitmproxy-ca.pem (CA key + certificate)
mitmproxy-ca-cert.pem (CA certificate, PEM — used by NODE_EXTRA_CA_CERTS)
mitmproxy-ca-cert.cer (CA certificate, DER — used by certutil on Windows)
mitmproxy-ca-cert.p12 (CA certificate, PKCS12)
mitmproxy-dhparam.pem (DH parameters)These files are generated once and reused across all sessions and tools.
The CA belongs to mitmproxy, not to a particular installation of it, so reinstalling or switching mitmdump keeps both the CA and the trust you already granted it.
Trusting the CA certificate
On first run (when ~/.mitmproxy/mitmproxy-ca-cert.pem does not yet exist), clsniff automatically tries to install the CA certificate in your system trust store. You can also trigger this manually at any time with --install-cert.
| Platform | Action |
|---|---|
| Windows | Runs certutil -addstore -user Root mitmproxy-ca-cert.cer. A Windows security dialog will appear asking for confirmation. |
| macOS | Prints the sudo security add-trusted-cert ... command to run manually. |
| Linux | Prints instructions to add the cert to your distro's CA bundle. |
Regardless of system-level trust, NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, and REQUESTS_CA_BUNDLE are always set in the child process environment to ensure Node.js, Python, and OpenSSL-based tools trust the proxy CA automatically.
Note:
SSL_CERT_FILEreplaces the system CA bundle for the child process. If you use--excludeto bypass certain hosts, those connections reach the real server but will fail TLS verification because the real CAs are no longer trusted inSSL_CERT_FILE. In that case, install the cert system-wide (via--install-cert) so the system CA bundle is used instead.
Note:
mitmdumpruns with--ssl-insecure, so certificates presented by the real upstream servers are not validated. A sniffer is not a security boundary, and this keeps local HTTPS endpoints with self-signed certificates (a development MCP server onhttps://localhost, for instance) working out of the box.
Environment variables
clsniff injects the following variables into the child process before launching it.
Proxy routing:
HTTP_PROXY,HTTPS_PROXY,http_proxy,https_proxy— set tohttp://127.0.0.1:<port>NO_PROXY,no_proxy— derived from--exclude, and empty by default. Whatever your own environment sets is replaced, so nothing can silently disable interception.
CA trust:
NODE_EXTRA_CA_CERTS= ~/.mitmproxy/mitmproxy-ca-cert.pemSSL_CERT_FILE= ~/.mitmproxy/mitmproxy-ca-cert.pemREQUESTS_CA_BUNDLE= ~/.mitmproxy/mitmproxy-ca-cert.pem
Development
git clone https://github.com/panicoenlaxbox/clsniff.git
cd clsniff
npm install
# Viewer only
npm run dev:hot
# Viewer + sniff a command
npm run dev:hot -- claude
# Viewer + sniff a command, with clsniff options
npm run dev:hot -- --configuration clsniff.example.json -- claude
# CLI only, no viewer
npm run dev -- -- claudeThe dev:hot commands use Vite's hot module reload, so edits under viewer/src show up instantly. Changes to the backend (anything under src/, e.g. the API or the CLI) are not hot-reloaded — restart the command to pick them up.
