nullwire-mcp
Go में शून्य-निर्भरता, केवल-RAM में चलने वाला HTTP डायग्नोस्टिक प्रॉक्सी और MCP सर्वर। क्रेडेंशियल्स को सॉल्टेड SHA-256 सहसंबंध टैग से बदलता है ताकि AI एजेंट बिना डिस्क पर सीक्रेट्स लिखे या LLM कॉन्टेक्स्ट में डाले ऑथ फ़्लो डीबग कर सकें।
दस्तावेज़
⚡ NullWire-MCP
See the traffic. Never the secrets.
A zero-dependency, RAM-only network inspector that lets your AI assistant debug HTTP, HTTPS and WebSocket traffic, with every secret scrubbed before the model ever sees it.
Quick start · How it works · What gets redacted · MCP tools · Configuration · CLI reference · Security model · How it compares · Contributing
▶ Try the live redactor (the real engine, running in your browser; nothing is uploaded)
🧠 Why NullWire exists
Your AI assistant is great at debugging an API call, until the request it needs to read carries an Authorization header, a session cookie, a database URL or a customer's email. Pasting raw traffic into a chat means pasting your secrets with it.
NullWire sits between your app and the network. It records what passes through, scrubs every secret out of the recorded copy, and hands that clean copy to your assistant through the Model Context Protocol. Your app keeps working exactly as before, because the live traffic is never touched.
your app ──► NullWire (127.0.0.1) ──► the real server
│
├─ forwards your traffic as-is (bodies untouched)
└─ keeps a scrubbed copy in RAM ──► your AI assistant (MCP)
✨ At a glance
| 🪶 Zero dependencies | Go standard library only. One static binary, nothing to install. |
| 🧠 RAM only | The last 50 flows live in memory. The server never writes a file, and a stop signal clears the buffer. (install edits your MCP client configs, but only when you run it.) |
| 🔒 Loopback only | The proxy binds to 127.0.0.1. A DNS-rebinding guard answers only loopback Host headers. |
| 🧽 Layered redaction | Private keys, JSON/XML/query secrets, JWTs, 200+ vendor key formats, cloud credentials (AWS, Azure, Cloudflare) with or without a prefix, IPs, secrets hiding in base64, and a randomness detector for keys nobody has seen before. |
| 🛡️ Fail-closed shields | Bodies that can't be inspected are refused or flagged, never silently passed to the model. |
| 🔌 HTTP, HTTPS and WebSocket | Plain proxying, blind CONNECT tunnels, a reverse gateway for full HTTPS visibility, and WebSocket text capture in both directions. |
| 🔁 Safe replay | Export any captured request as a curl command with secrets swapped for environment variables. |
| 🔎 Secret scanner | scan and protect look for secrets in your MCP configs, .env, TOML, YAML and ZIP files before they leak. |
| 📱 Runs anywhere | Linux, macOS, Windows, Docker and Termux on Android. |
⚖️ How it compares
NullWire is deliberately narrow: it lets an AI assistant look at your network traffic while the secrets in it are replaced before the model sees them. Other tools do other jobs, and some of them do those jobs better. Here is the honest picture.
| NullWire-MCP | mitmproxy | Langfuse | Presidio | |
|---|---|---|---|---|
| Built for | Letting an AI assistant debug traffic with secrets scrubbed | Interactive proxy to inspect and modify HTTP(S) and WebSocket traffic | LLM tracing, evals and prompt management | Detecting and anonymizing personal data (PII) in text |
| Sees inside HTTPS | Only through the reverse gateway; the plain proxy tunnels HTTPS blind | Yes, once you install its CA certificate | Not a proxy; traces come from your app's SDK | Not a proxy; you give it text |
| Secret redaction | Built in, applied before anything reaches the model | No built-in redaction; scriptable with Python add-ons | You write your own masking function (SDK mask callback) | Built in (patterns and NER), aimed mainly at personal data |
| Where data lives | RAM only (last 50 flows), never written to disk | In memory; saved to a file only if you choose to | Stored in its database (Cloud or self-hosted) | Stays in your own process |
| Runtime | One static Go binary, no dependencies | Python-based tool | Cloud, or self-hosted (web, worker, Postgres, ClickHouse, Redis) | Python package, or Docker with a REST API |
| Best when | You want an AI assistant to debug traffic without leaking keys | You need full traffic inspection and modification | You monitor LLM apps in production | You clean PII out of text pipelines |
NullWire does not replace mitmproxy for full HTTPS interception, Langfuse for production tracing, or Presidio for PII pipelines. If one of those fits your job, use it. If you want your assistant to see traffic and never the secrets in it, that is what NullWire is for.
These comparisons come from each project's own documentation. If something is out of date, please open an issue.
🚀 Quick start
1. Install it
Pick whichever fits your machine. Every method checks the download against the release's SHA256SUMS.txt and installs nothing if it doesn't match. Release binaries also carry signed GitHub build provenance and keyless cosign signatures (*.sigstore.json), and each release includes a CycloneDX SBOM (nullwire-sbom.cdx.json).
With Node.js (no install step): npx
npx -y nullwire-mcp install --client all
Nothing to download by hand: the launcher fetches and verifies the binary on first run and keeps it in a cache folder. Your MCP clients are then set to run npx -y nullwire-mcp.
macOS, Linux and Termux: install script
curl -fsSL https://raw.githubusercontent.com/VoxNode-ux/nullwire-mcp/main/scripts/install.sh | sh -s -- --setup
Installs to ~/.local/bin (or $PREFIX/bin on Termux). --setup also registers NullWire with your MCP clients. Drop it to install only. --uninstall removes everything again.
Windows: install script
irm https://raw.githubusercontent.com/VoxNode-ux/nullwire-mcp/main/scripts/install.ps1 | iex
nullwire-mcp install --client all
Installs to %LOCALAPPDATA%\Programs\nullwire-mcp and adds it to your user PATH. To pass options, run it as & ([scriptblock]::Create((irm https://raw.githubusercontent.com/VoxNode-ux/nullwire-mcp/main/scripts/install.ps1))) -Setup.
Short install URL (GitHub Pages)
curl -fsSL https://voxnode-ux.github.io/nullwire-mcp/install.sh | sh -s -- --setup
irm https://voxnode-ux.github.io/nullwire-mcp/install.ps1 | iex
Same scripts as above, just a shorter address.
Linux packages and distributions
# Debian, Ubuntu: download the .deb from the release page, then
sudo apt install ./nullwire-mcp_1.2.0_amd64.deb
# Fedora, RHEL
sudo dnf install ./nullwire-mcp-1.2.0-1.x86_64.rpm
# Alpine (packages are not signed with an Alpine key)
sudo apk add --allow-untrusted ./nullwire-mcp_1.2.0_x86_64.apk
# Arch Linux (AUR)
yay -S nullwire-mcp-bin
# Nix
nix run github:VoxNode-ux/nullwire-mcp
Packages are built for amd64, arm64 and ARMv7.
Windows: winget and Chocolatey
winget install VoxNode.NullWire
choco install nullwire-mcp
Python, GitHub CLI, mise and asdf
pipx run nullwire-mcp install --client all # or: pip install nullwire-mcp
gh extension install VoxNode-ux/gh-nullwire && gh nullwire scan --discover
mise use -g github:VoxNode-ux/nullwire-mcp # mise, no plugin needed
mise plugin install nullwire-mcp https://github.com/VoxNode-ux/asdf-nullwire-mcp # or the asdf plugin
Homebrew and Scoop
brew install VoxNode-ux/nullwire/nullwire-mcp # macOS, Linux
scoop bucket add nullwire https://github.com/VoxNode-ux/scoop-nullwire # Windows
scoop install nullwire-mcp
All package files (Homebrew, Scoop, AUR, winget, Chocolatey) are generated from the templates in packaging/.
Download a release by hand
| Platform | File |
|---|---|
| Linux x86-64 / ARM64 (Raspberry Pi 4+, Termux) | nullwire-linux-amd64 / nullwire-linux-arm64 |
| Linux ARMv7 (older Raspberry Pi) / RISC-V 64 | nullwire-linux-armv7 / nullwire-linux-riscv64 |
| macOS Intel / Apple Silicon | nullwire-darwin-amd64 / nullwire-darwin-arm64 |
| Windows x86-64 / ARM64 | nullwire-windows-amd64.exe / nullwire-windows-arm64.exe |
| FreeBSD x86-64 | nullwire-freebsd-amd64 |
chmod +x nullwire-linux-amd64
sha256sum -c SHA256SUMS.txt --ignore-missing # verify the download (macOS: shasum -a 256 -c)
gh attestation verify nullwire-linux-amd64 --repo VoxNode-ux/nullwire-mcp # optional: verify signed build provenance
Build from source
go install github.com/VoxNode-ux/nullwire-mcp@latest
# or
git clone https://github.com/VoxNode-ux/nullwire-mcp && cd nullwire-mcp
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o nullwire-mcp .
Go 1.26 or newer. There is nothing to go get: the module has no dependencies.
Docker
docker run -i --rm ghcr.io/voxnode-ux/nullwire-mcp
The server speaks MCP over stdio, so run it with -i. The proxy listens on the container's own loopback by design, so a published port (-p) cannot reach it. On Linux, add --network host to use it from the host's 127.0.0.1.
2. Add it to your MCP clients
One command registers NullWire with every supported client it finds on your machine:
nullwire-mcp install --client all --dry-run # look first: nothing is written
nullwire-mcp install --client all # then do it, and restart the client
It covers Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, Cline, Roo Code, Gemini CLI and Codex CLI. See Connect your MCP clients for details, or add it by hand:
# Claude Code
claude mcp add nullwire -- /path/to/nullwire-mcp
// Claude Desktop, Cursor, Windsurf, Cline and other JSON-configured clients
{
"mcpServers": {
"nullwire": {
"command": "/path/to/nullwire-mcp",
"args": ["-port", "8080"]
}
}
}
The client starts NullWire, and NullWire starts the proxy in the same process. A live status bar on stderr shows what is happening:
⚡ [NullWire-MCP] Proxy 127.0.0.1:8080 | RAM Ring: 3/50 | Scrubbed: 7 Tokens, 2 Cookies, 1 IPs | Disk: 0B
3. Send some traffic through it
# Plain HTTP through the proxy
curl -x http://127.0.0.1:8080 http://httpbin.org/headers
# HTTPS through the proxy (a blind tunnel, see "How it works")
curl -x http://127.0.0.1:8080 https://example.com
# HTTPS with full request visibility: the reverse gateway
nullwire-mcp -target https://api.example.com
curl http://127.0.0.1:8080/v1/things -H "Authorization: Bearer sk-live-..."
4. Ask your assistant
"Use
inspect_trafficto show my last 5 requests and tell me why the third one returned 401."
🔭 How it works
flowchart LR
A[Your app or curl] -->|HTTP_PROXY / base URL| P[NullWire proxy<br/>127.0.0.1:8080]
P -->|bodies untouched| S[(Real server)]
S -->|bodies untouched| P
P --> C[Capture copy]
C --> R[Redaction layers]
R --> B[(RAM ring<br/>50 flows)]
B -->|MCP over stdio| M[AI assistant]
The key idea: the model reads a scrubbed copy, your app talks to the real server. Request and response bodies are never rewritten. Only a few negotiation headers are adjusted (see Safety shields), so debugging barely changes the behaviour you are debugging.
Four ways to capture traffic
| Mode | How you use it | What the assistant sees |
|---|---|---|
| Forward proxy | HTTP_PROXY=http://127.0.0.1:8080 and http:// URLs | Full request and response, scrubbed |
| HTTPS tunnel | HTTPS_PROXY=http://127.0.0.1:8080 (CONNECT) | Only the target host:port, timing and byte counts. The encrypted payload is never opened. |
| Reverse gateway | Point your app's base URL at http://127.0.0.1:8080 and start with -target https://… (or send an X-Target-URL header) | Full HTTPS request and response, scrubbed. NullWire makes the TLS connection to the real server. |
| WebSocket | Any Upgrade: websocket request through either proxy mode | Text messages in both directions, scrubbed, as one flow recorded when the connection closes. Frames are forwarded unmodified. |
Why no HTTPS interception? Reading HTTPS from a forward proxy would require a custom root certificate installed on your machine. That is key material and a trust-store change, the opposite of a zero-footprint tool. The reverse gateway gets you the same visibility with no certificate games.
Several APIs at once
Give each upstream a name. The first path segment picks the upstream and is removed before forwarding:
nullwire-mcp -target openai=https://api.openai.com,anthropic=https://api.anthropic.com
export OPENAI_BASE_URL=http://127.0.0.1:8080/openai/v1 # → https://api.openai.com/v1
export ANTHROPIC_BASE_URL=http://127.0.0.1:8080/anthropic # → https://api.anthropic.com
Names are letters, digits, ., _ and - (up to 32 characters, case-insensitive). One unnamed URL may be added as the default for paths that match no name. An X-Target-URL header overrides everything for a single request.
🧰 MCP tools
NullWire exposes three tools over stdio. It speaks MCP protocol versions 2026-07-28, 2025-11-25 and 2024-11-05, and accepts JSON-RPC batches.
| Tool | What it does | Arguments |
|---|---|---|
inspect_traffic | Returns recent scrubbed flows. | limit (1 to 50, default 10), method, url_filter |
export_sanitized_curl | Rebuilds a captured request as a curl command. Redacted values become environment variables you fill in yourself. | flow_id (required) |
clear_ram_buffer | Clears all 50 slots and resets the counters. | none |
A flow looks like this (illustrative):
{
"id": 7,
"timestamp": "2026-10-31T09:14:02Z",
"method": "POST",
"url": "https://api.example.com/v1/charge",
"request_headers": { "Authorization": "[REDACTED_TOKEN#a1b2c3]", "Content-Type": "application/json" },
"request_body": "{\"card_token\":\"[REDACTED_TOKEN#9f3e21]\",\"amount\":4200}",
"status_code": 401,
"response_body": "{\"error\":\"invalid_api_key\"}",
"latency_ms": 143,
"redaction_count": 2
}
Tags are stable and correlatable. [REDACTED_TOKEN#a1b2c3] is a short salted hash, so the same secret gets the same tag everywhere in one run. Your assistant can see that two requests used the same token without learning it. The salt is random per run, so tags can't be compared across restarts or reversed.
Tag kinds: TOKEN, COOKIE, IP and KEY (private keys).
export_sanitized_curl produces something like:
# Replay of flow 7
# Set environment variables first:
# export NULLWIRE_AUTH_TOKEN=... # was [REDACTED_TOKEN#a1b2c3]
curl -X POST 'https://api.example.com/v1/charge' \
-H "Authorization: $NULLWIRE_AUTH_TOKEN" \
-H 'Content-Type: application/json' \
--data-raw '…'
Placeholders are NULLWIRE_AUTH_TOKEN, NULLWIRE_COOKIE, NULLWIRE_IP and NULLWIRE_PRIVATE_KEY. CONNECT tunnels have no replayable request, and NullWire says so.
🧽 What gets redacted
Every URL, header, request body and response body passes through these layers, in this order:
| # | Layer | Catches | Example |
|---|---|---|---|
| 1 | Private keys | PEM blocks: RSA, EC, OpenSSH, encrypted, PGP; JSON-escaped ones; blocks cut off by a size limit | -----BEGIN PRIVATE KEY-----… |
| 2 | Secret fields | JSON string values and arrays, and query parameters, for names like password, api_key, access_token, secret, session_id | "password": ["a","b"], ?api_key=… |
| 3 | XML elements | Element text for secret-looking names, with namespaces and CDATA; the tags themselves stay intact | <wsse:Password>…</wsse:Password> |
| 4 | URL credentials | user:password@host in any URL or connection string | postgres://app:pw@db/prod |
| 5 | Auth schemes | Bearer, Basic and Token credentials | Authorization: Bearer … |
| 6 | JWTs | Three-part eyJ… tokens | |
| 7 | Vendor keys | 200+ prefixes in three tiers (below), plus webhook URLs and connection strings | AKIA…, ghp_…, sk-ant-…, cfut_…, SG.… |
| 8 | Secret-named fields | key: value or key=value pairs where the name says secret and the value is 12 to 256 characters | x-custom-auth: …, session=… |
| 9 | Your patterns | Regexes from the config file | |
| 10 | IP addresses | IPv4 (strictly validated) and IPv6, except loopback and unspecified | 203.0.113.9 |
| 11 | Base64 | Base64 or URL-safe base64 whose decoded text contains a secret. Images and other binary data are left alone. | eyJhcGlfa2V5… |
| 12 | Randomness detector | 16 to 128 character tokens that look machine-generated: mixed case or digits, high Shannon entropy, few vowels. UUIDs are exempt. | an unknown vendor's key |
Headers. Authorization, Cookie, Set-Cookie, X-Api-Key, Api-Key, X-Auth-Token and Proxy-Authorization are always redacted, plus common cloud and developer-tool headers (X-Auth-Key, Ocp-Apim-Subscription-Key, X-Amz-Security-Token, X-Goog-Api-Key, Private-Token, X-Vault-Token, Apikey, and others), plus any you add. Use ignore_headers to opt a header out.
Cloud credentials without a prefix. AWS secret access keys, Azure storage and Service Bus keys, Azure AD client secrets (…8Q~…) and SQL connection-string passwords have no vendor prefix. They are caught by the name they are stored under (SecretAccessKey, AccountKey, SharedAccessKey, client_secret, Password= in a connection string), by their shape (a standalone 40-character random base64 string, an Azure AD secret), or by the header they travel in. Cloudflare API tokens are caught by Authorization: Bearer, X-Auth-Key, the new cfut_/cfat_ prefixes, or the randomness detector.
Vendor tiers. Matching all prefixes the same way would redact ordinary words, so they are tiered:
| Tier | Needs after the prefix | Examples |
|---|---|---|
| Strong | 16+ token characters | sk-ant-, sk-proj-, AKIA, ASIA, ghp_, github_pat_, glpat-, xoxb-, sk_live_, sk_test_, rk_live_, npm_, SG., AIza, ya29., pplx- |
| Weak (also occur in normal words) | 20+ characters, plus a digit, a capital or 24+ characters | hf_, r8_, secret_, api_key_, access_token_, bearer , token |
| Connection strings | 8+ characters | postgres://, mysql://, mongodb+srv://, redis://, amqp:// |
Prefixes are matched case-sensitively, so asiapacific_region is left alone.
Bodies are decoded before scrubbing. gzip and deflate (up to 1 MiB expanded, a decompression-bomb guard). multipart/form-data forms are split into fields; file parts keep only the field name, file name and size.
🛡️ Safety shields
| Shield | Behaviour |
|---|---|
| Request shield | A request body with a Content-Encoding NullWire can't read (br, zstd, stacked encodings) is refused with 415, because a body that can't be scrubbed must not reach the model. -allow-uninspectable forwards such requests anyway. |
| Response shield | The client's Accept-Encoding is cut down to gzip and deflate before it reaches the server, so servers never answer in Brotli or Zstd. A client that offers nothing usable gets identity. |
| Unreadable bodies | Any other encoded body is stored as a short placeholder with a visible warning in the flow's note. |
| Protobuf and gRPC | Binary bodies are not stored. Only their size is recorded. |
| Size limits | See the table below. A secret that straddles a limit is still scrubbed first, thanks to an 8 KiB scrub window. |
| Loopback only | The proxy binds to 127.0.0.1. The reverse gateway answers only loopback Host headers (403 otherwise) and refuses to proxy to itself (508). |
| Tunnel limits | At most 256 concurrent tunnels and upgraded connections, each closed after 10 minutes idle. |
| Clear on exit | SIGINT and SIGTERM clear the ring buffer before the process exits. |
Limits
| What | Limit |
|---|---|
| Flows kept in RAM | 50 |
| Stored request body | 32 KiB |
| Stored response body | 256 KiB |
| In-flight capture buffer | 64 KiB request, 512 KiB response |
| Scrub window past the stored size | 8 KiB |
| Decoded gzip/deflate | 1 MiB |
| Randomness detector | 16 to 128 characters |
⚙️ Configuration
Pass -secrets-config path/to/file.json to tune redaction without recompiling. Every field is optional.
{
"sensitive_headers": ["X-Company-Auth", "X-Api-Signature"],
"ignore_url_substrings": ["/healthz", "/metrics"],
"ignore_headers": ["User-Agent"],
"custom_vendors": [
{ "name": "Internal DB", "prefix": "db_prod_", "pattern": "" },
{ "name": "Acme Auth", "prefix": "acme_", "pattern": "acme_[a-zA-Z0-9]{32}" }
],
"extra_regex_patterns": [
{ "name": "app_key", "pattern": "app_k_[a-zA-Z0-9]{24}" }
]
}
| Field | Effect |
|---|---|
sensitive_headers | Header values that are always redacted, like Authorization. -scrub-headers a,b adds more from the command line. |
ignore_url_substrings | Flows whose URL contains one of these are never recorded (health checks, metrics). |
ignore_headers | Header values that are left as they are. |
custom_vendors | prefix redacts anything with 8+ token characters after it. pattern is an optional full regex. |
extra_regex_patterns | Extra regexes; every match is redacted. |
Patterns use Go's RE2 syntax, which runs in linear time, so a bad pattern cannot freeze the proxy.
🔌 Connect your MCP clients: install
install writes NullWire into your MCP clients' config files, and removes it again, so you never have to hunt for the right file or JSON key.
nullwire-mcp install --list # supported clients and whether they are here
nullwire-mcp install --client all --dry-run # preview every change
nullwire-mcp install --client cursor,vscode # just these two
nullwire-mcp install --client cursor -- -port 8081 # save server flags with the entry (after --)
nullwire-mcp install --uninstall --client all # take it back out
nullwire-mcp install --print # a snippet to paste into clients that aren't supported
| Client | --client | What it edits |
|---|---|---|
| Claude Desktop | claude-desktop | claude_desktop_config.json in your user config folder |
| Claude Code | claude-code | runs claude mcp add --scope user (needs the claude command) |
| Cursor | cursor | ~/.cursor/mcp.json |
| Windsurf | windsurf | ~/.codeium/windsurf/mcp_config.json |
| VS Code (Copilot) | vscode | your user mcp.json |
| Cline | cline | cline_mcp_settings.json |
| Roo Code | roo | mcp_settings.json |
| Gemini CLI | gemini | ~/.gemini/settings.json |
| Codex CLI | codex | ~/.codex/config.toml ([mcp_servers.nullwire]) |
Other clients (Zed, Continue, JetBrains and more) are not edited. Use --print and paste the snippet where that client keeps its MCP servers.
How it stays safe
- With
--client allit only touches clients it finds on your machine. Name a client explicitly to create its config. - The first time it changes a file it keeps a copy next to it as
<file>.nullwire-bak. That copy is never overwritten, so it always holds the original. - Files are written through a temporary file and renamed, and symlinked dotfiles stay symlinks.
- Your other servers, settings, numbers and extra fields on the NullWire entry (such as
env) are kept. JSON keys may be re-ordered. - A config it can't parse as plain JSON (for example one with comments) is left alone, with a clear message.
- Running it twice changes nothing the second time.
| Option | Meaning |
|---|---|
--client | Comma-separated client ids, or all (default). |
--name | Name to register the server under (default nullwire). |
--binary PATH | Register this binary instead of the one that is running. |
--npx | Register npx -y nullwire-mcp (used automatically when you started it through npx). |
--dry-run, --uninstall, --list, --print | As described above. |
-- … | Everything after -- is saved as the server's own flags. |
🔎 Scan your machine: scan and protect
Secrets leak long before traffic does: a token in an MCP config, a password in .env. NullWire can find them. Scanning never writes to disk and never prints a secret: findings are shown already scrubbed, and shell history text is never shown at all.
nullwire-mcp scan --discover # known MCP client configs, ~/.env, ~/.codex/config.toml, …
nullwire-mcp scan .mcp.json .env # specific files
nullwire-mcp scan --deep ./my-project --toml --yaml --zip
nullwire-mcp scan --history # opt-in: count secrets in shell history
| Option | Meaning |
|---|---|
--discover | Check the usual config locations under your home and config directories. |
--deep DIR | Walk a directory for MCP config JSON and .env files. Skips node_modules, .git, dist, build, vendor and similar by exact folder name. |
--toml, --yaml, --zip | Also scan *.toml, *.yaml/*.yml and ZIP archives. Off by default so a deep scan doesn't open every CI file. |
--history | Count lines with secrets in bash, zsh, fish and PowerShell history. Text is never printed. |
--fail | Exit with code 3 when any secret is found (for git hooks and CI). Without it, findings never change the exit code. |
| exit codes | 0 done, 1 every file failed to scan, 2 bad usage, 3 secrets found (only with --fail) |
File types are recognised by name, so nullwire-mcp scan some.env config.toml settings.yaml bundle.zip routes each to the right scanner. Files over 4 MiB are skipped. ZIP archives are read in memory under a zip-bomb shield: entries over 1 MiB, archives over 20 MiB of content or 1000 entries are skipped and reported.
Git hook. Block commits that contain secrets with pre-commit:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/VoxNode-ux/nullwire-mcp
rev: v1.2.0
hooks:
- id: nullwire-scan # builds nullwire-mcp from source (needs Go)
# - id: nullwire-scan-system # or use the nullwire-mcp already on your PATH
The hook runs nullwire-mcp scan --fail --yaml on staged .env, MCP config, TOML and YAML files. The report shows scrubbed values only.
Shortcut. Add alias nw="nullwire-mcp" to your ~/.bashrc or ~/.zshrc (PowerShell: Set-Alias nw nullwire-mcp), then nw scan --discover.
protect runs a quick scan of the usual places, then starts the normal server:
nullwire-mcp protect --target https://api.example.com --deep .
It accepts the server options (--port, --target, --cacert, --dns, --secrets-config, --scrub-headers, --allow-uninspectable) plus --deep DIR, --history and --no-scan. The scan report goes to stderr, so the MCP channel on stdout stays clean, and stop signals are passed on to the server.
📟 CLI reference
nullwire-mcp [flags] run the MCP server and the proxy
nullwire-mcp scan [options] [PATH…] look for secrets in files
nullwire-mcp protect [options] scan, then run the server
nullwire-mcp install [options] register NullWire with your MCP clients
| Flag | Default | Meaning |
|---|---|---|
-port | 8080 | Proxy port. Always bound to 127.0.0.1. |
-target | none | Reverse-gateway upstream: a URL and/or name=URL entries, comma-separated. |
-cacert | none | Extra CA certificates (a PEM file or a directory) for upstream HTTPS. |
-dns | auto | Upstream DNS: auto, system, or a comma list of IPs such as 1.1.1.1,9.9.9.9:53. |
-secrets-config | none | Path to the JSON config. |
-scrub-headers | none | Extra header names to always redact. |
-allow-uninspectable | off | Forward request bodies that can't be inspected instead of refusing them with 415. |
📱 Platform notes
Termux and Android. Static Go binaries have no system DNS settings or CA certificates on Android. NullWire detects Termux and uses 1.1.1.1 and 9.9.9.9 for lookups, printing a notice (those resolvers will see the hostnames you contact). Choose your own with -dns <resolver>, or -dns system to turn this off. Upstream HTTPS uses the CA certificates found in Termux's standard locations; add more with -cacert.
chmod +x nullwire-linux-arm64
sleep infinity | ./nullwire-linux-arm64 # keep stdin open; use a second session for curl
Corporate networks. If your company re-signs HTTPS traffic, add its root with -cacert /path/to/company-root.pem.
Windows. Use the .exe and the same flags. scan --history also checks PowerShell's PSReadLine history.
🔐 Security model and honest limits
NullWire is a diagnostic tool. Be clear-eyed about what it does and does not promise.
What it guarantees
- Nothing is written to disk by the server, and a stop signal clears the buffer (references are dropped; memory is not zeroed). The
installcommand edits MCP client configs only when you run it, and backs each file up first. - The proxy is reachable only from your own machine.
- The model never sees the live traffic, only the scrubbed copy.
- Request and response bodies are never rewritten. Headers are adjusted only where inspection needs it:
Accept-Encodingis reduced togzip/deflate, and the WebSocketpermessage-deflateoffer is dropped.
What it does not do
- Release binaries carry signed build provenance, not OS-vendor code-signing certificates. Verify authenticity with
SHA256SUMS.txtandgh attestation verify. Because they are not signed with an Apple Developer ID or Microsoft Authenticode certificate, on macOS you may needxattr -d com.apple.quarantine ./nullwire-darwin-arm64. - It is not a TLS interceptor.
CONNECTtunnels show only the destination and byte counts. Use the reverse gateway when you need HTTPS content. - Redaction is pattern and heuristic based. A secret in a shape nobody anticipated, and low in randomness, can slip through. The randomness detector can also redact a harmless long identifier. Add your own patterns in the config for secrets you know about.
- Brotli and Zstd bodies are never decoded (the standard library can't, and pulling in a package would break the zero-dependency rule). They are refused (requests) or flagged (responses).
- WebSocket flows appear when the connection closes. Messages compressed with
permessage-deflatecan't be read, so NullWire drops the client's extension offer to keep frames readable. - An XML secret element that contains nested markup is not covered by the XML layer.
- Upstream connections use HTTP/1.1. HTTP/2 and gRPC bodies are not captured.
If you find a secret NullWire missed, please open an issue with a fake sample that has the same shape. Never post a real one.
✅ Tested on
| Platform | Status | What was run |
|---|---|---|
| Linux x86-64 (Ubuntu 24.04) | ✅ verified | go vet, unit tests with -race, smoke test, end-to-end redaction |
| macOS 15 (Apple Silicon) | ✅ verified | smoke test, silent notifications, end-to-end redaction |
| macOS 15 (Intel) | ✅ verified | smoke test, silent notifications, end-to-end redaction |
| Windows x86-64 | ✅ verified | smoke test, silent notifications, end-to-end redaction |
| Windows ARM64 | ✅ verified | smoke test, silent notifications, end-to-end redaction |
| FreeBSD amd64 (VM) | ✅ verified | smoke test, silent notifications, end-to-end redaction |
| Linux ARMv7 (QEMU) | 🟡 emulated | smoke test, silent notifications, end-to-end redaction |
| Linux RISC-V 64 (QEMU) | 🟡 emulated | smoke test, silent notifications, end-to-end redaction |
| Linux ARM64 (Android / Termux) | ✅ verified on device (v1.2.0 CA-bundle change awaiting a device re-check) | live upstream proxy, redaction tags, export_sanitized_curl, reverse gateway, guards (needs termux-chroot) |
🟡 emulated means run under QEMU, not on real hardware. Details are in docs/test-results.md.
🧪 Development
go vet ./...
go test -race ./...
- Zero dependencies:
go.modhas norequirelines, and CI keeps it that way. - Release transparency: every release is built with Go 1.26 and includes
SHA256SUMS.txt, a CycloneDX SBOM (nullwire-sbom.cdx.json) and a signed build provenance attestation, which you can verify withgh attestation verify. The same proof is attached to each release asnullwire-provenance.intoto.jsonl. Thego vetandgo test -racelogs of every CI run are saved as atest-logsartifact for 30 days. Pushes tomainupdate a rollinglatestpre-release; numbered tags produce stable releases. Pull requests never publish anything. - Quality checks:
quality.ymlruns golangci-lint (errcheck, staticcheck, revive, ineffassign), reports test coverage (Codecov) and compares benchmarks against the base branch withbenchstat. Run the benchmarks yourself withgo test -run='^$' -bench=. -benchmem ./....chaos_test.goinjects network faults (slowloris, dropped connections, malformed and truncated streams) and checks that nothing crashes or leaks. - Supply chain: releases are signed with keyless cosign, the Docker image is signed and scanned with Trivy every week,
reproducible.ymlrebuilds the binaries on four different runners and requires identical checksums, andsecurity.yml(weekly) runsgovulncheck,gosecand the fuzz targets. Verify a download withcosign verify-blob --bundle nullwire-linux-amd64.sigstore.json --certificate-identity-regexp 'https://github.com/VoxNode-ux/nullwire-mcp/\.github/workflows/release\.yml@.*' --certificate-oidc-issuer https://token.actions.githubusercontent.com nullwire-linux-amd64. - Releases: a stable tag publishes the release, the
.deb/.rpm/.apkpackages, the npm package (with provenance), the Homebrew and Scoop taps, the AUR, winget, Chocolatey, PyPI and the official MCP Registry. Release notes are grouped by pull request label (.github/release.yml). - Fuzzing: the scrubber, the IPv6 and vendor-key matchers and the WebSocket frame reader have fuzz targets. They check that no secret survives scrubbing, that results don't depend on how input is chunked, and that nothing panics.
- CI: checks the shell and Node installers for syntax errors, runs
go vetand the race detector, cross-compiles static binaries for all 9 platforms (Linux x86-64, ARM64, ARMv7 and RISC-V 64; macOS Intel and Apple Silicon; Windows x86-64 and ARM64; FreeBSD x86-64), and publishes a multi-arch Docker image (amd64 and arm64).smoke.ymlandsmoke-extra.ymlrun on demand (Actions → Run workflow): end-to-end redaction tests on Ubuntu, macOS (Apple Silicon and Intel), Windows (x86-64 and ARM64), a FreeBSD VM, and QEMU-emulated Linux ARMv7 and RISC-V 64. Security checks rungovulncheckand the fuzz targets. GitHub Actions are pinned to full commit hashes.
Project layout
| File | Purpose |
|---|---|
main.go | Tiny entry point (keeps go install github.com/VoxNode-ux/nullwire-mcp@latest working) |
internal/app/app.go | Proxy, scrubber, ring buffer, MCP server and CLI |
internal/app/ws.go | WebSocket frame reader and capture (read-only taps) |
internal/app/inspect.go | Request shield, multipart scrubbing, Accept-Encoding and protobuf rules |
internal/app/scrub_extra.go | JSON array, XML and base64 redaction |
internal/vendors/ | Vendor prefix tiers |
internal/app/cloud_secrets.go | Prefix-less cloud credentials (AWS, Azure, Cloudflare) and their headers |
internal/scan/ | The scan command (scan.go, env_scan.go, toml_scan.go, yaml_scan.go, zip_scan.go, history_scan.go, plus the small Scrubber interface in scrubber.go) |
internal/protect/ | The protect command |
internal/install/ | The install command (client config editing) |
internal/wasm/ | Compiles the scrubber to WebAssembly for the live demo (scripts/build-wasm.sh) |
scripts/ | Installers for macOS, Linux, Termux and Windows (install.sh, install.ps1, termux-install.sh) |
npm/ | The npx launcher (no dependencies, no install script) |
packaging/ | Homebrew, Scoop, AUR, winget and Chocolatey templates, render.sh, Nix package, gh extension and asdf/mise plugin sources |
flake.nix, nfpm.yaml | Nix flake; .deb/.rpm/.apk package definition |
python/ | The pip / pipx launcher |
docs/ | GitHub Pages site (live redactor demo) and the short install URL |
internal/app/bench_test.go, internal/app/chaos_test.go | Benchmarks and network fault-injection tests |
internal/app/feature_test.go | Tests for the features above |
❓ Troubleshooting
| Symptom | Cause and fix |
|---|---|
HTTPS shows only a CONNECT entry | That is the blind tunnel. Use the reverse gateway (-target) to see HTTPS requests. |
415 Unsupported Media Type from NullWire | Your client sent a request body compressed with br or zstd. Send it uncompressed or gzipped, or start with -allow-uninspectable. |
403 reverse gateway only answers loopback Host headers | Reach the gateway as 127.0.0.1, localhost or [::1] on the proxy port. |
certificate signed by unknown authority (Termux or a corporate network) | Add the CA with -cacert. |
A harmless value shows as [REDACTED_TOKEN#…] | The randomness detector matched a long random-looking identifier. Add its URL to ignore_url_substrings or tell us the shape. |
install says a config is "not plain JSON" | The file has comments or a syntax error, so it was left untouched. Fix it, or use install --print and paste the entry yourself. |
address already in use | Another NullWire (or program) uses the port. Pick another with -port. |
| The assistant sees no flows | Check the status bar's RAM Ring: n/50. If it stays at 0, your traffic isn't going through the proxy. |
Built by VoxNode-ux · Version 1.2.0 · License: see the repository's LICENSE file
If NullWire saved you from pasting a secret into a chat, a ⭐ helps others find it.