NullWire-MCP
RAM-only HTTP debugging proxy and MCP server in Go. AI agents see salted tags, never secrets.
Install
The installation steps are in the documentation below. Documentation
Documentation
From the project's README on GitHub, refreshed weekly. Links open on GitHub.
Contents
- ⚡ NullWire-MCP
- 👀 See the difference
- ⚡ Quick start in 60 seconds
- 🔥 Why this exists
- 🧭 How it works
- 📊 NullWire-MCP vs. traditional MITM MCP wrappers
- 🚀 30-second install
- 🔀 Two capture modes
- 🧰 MCP tool reference
- 🛡️ Security & protocol engineering deep-dive
- ⚖️ Honest scope & known limitations
- 🧪 Tested on
- ✅ Verification & testing
- ❓ FAQ
- 🤝 Contributing
- 📄 License
⚡ NullWire-MCP
Your AI agent can debug live HTTP traffic.
It never sees a single secret.
Every token, cookie and IP is swapped for a stable, salted tag, so the agent can still tell
"same token reused" from "token rotated": the question behind most auth bugs.
-brightgreen) -brightgreen)
**Quick start** · **How it works** · **Install** · **Tools** · **Security** · [Limitations]#-honest-scope-&-known-limitations) · **FAQ**
👀 See the difference
You send Authorization: Bearer sk-live-51Hk3...
Your AI sees Authorization: Bearer [REDACTED_TOKEN#a1b2c3]
Request #1 Bearer [REDACTED_TOKEN#a1b2c3]
Request #2 Bearer [REDACTED_TOKEN#a1b2c3] ◄── same token, reused ✔
Request #3 Bearer [REDACTED_TOKEN#7f09de] ◄── different token, it rotated ✔
Blind [REDACTED] protects you but makes the AI useless for auth bugs. NullWire keeps the identity of each secret and throws away the value.
| 📦 0 dependencies | 💾 0 bytes written to disk | 🔍 1 source file to audit | 🖥️ 9 release targets |
|---|---|---|---|
| pure Go stdlib | RAM-only 50-flow ring | main.go | Linux · macOS · Windows · FreeBSD |
⚡ Quick start in 60 seconds
# 1. install (Go 1.23+), or grab a prebuilt binary from Releases
go install github.com/VoxNode-ux/nullwire-mcp@latest
# 2. register it with your AI client (Claude Code shown; Cursor/Windsurf/Desktop configs below)
claude mcp add nullwire -- nullwire-mcp -port 8080 -target https://api.example.com
# 3. point your app at the proxy instead of the real API
curl http://127.0.0.1:8080/v1/orders -H "Authorization: Bearer $MY_TOKEN"
Then just ask your agent:
"Why is
/v1/ordersreturning 401? Compare the auth headers of the last 5 requests." "Did request 3 and 4 use the same token, or did it rotate?" "Give me a curl to replay the failing call."
Your app always gets the real, untouched response. Only the agent's view is scrubbed.
🔥 Why this exists
Point an AI agent at your network traffic and a typical capture proxy hands it everything: live bearer tokens, session cookies, internal IPs. All of it lands in a cloud LLM context window, and often in a capture file left on disk.
Blind [REDACTED] stops the leak but kills the debugging. NullWire-MCP redacts every secret in RAM and swaps it for a deterministic, per-process salted tag: the agent keeps the identity of each secret and never sees the value.
The tag is
SHA-256(nonce ‖ secret)[:6]. Thenonceis 32 random bytes drawn fromcrypto/randat startup, kept only in RAM, and gone when the process exits. See the security deep-dive for why that matters.
🧭 How it works
flowchart LR
APP["Your app / curl / SDK"] -->|"plain HTTP to 127.0.0.1"| PROXY["NullWire proxy<br/>loopback only"]
PROXY -->|"original request, untouched"| UP["Upstream API<br/>http or https"]
UP -->|"original response"| PROXY
PROXY -->|"original response, untouched"| APP
PROXY -.->|"async copy"| DLP["DLP scrubber<br/>regex, then SHA-256 tag"]
DLP --> RING[("RAM ring<br/>50 flows")]
RING -->|"MCP over stdio<br/>scrubbed data only"| AGENT["AI agent<br/>Claude · Cursor · Windsurf"]
Two paths, one rule: the real bytes go to your app, only a scrubbed copy goes to the AI. Nothing touches the disk.
Diagram not rendering (mobile, Mermaid disabled)? Open the ASCII version
╔══ [1] LOCAL APP / CLI ══════════════════════════════════════╗
║ curl · SDK · browser · your service ║▒
║ speaks plain HTTP to 127.0.0.1:8080 ║▒
╚═════════════════════════════════════════════════════════════╝▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
│
▼ plain HTTP, loopback only
╔══ [2] NULLWIRE PROXY 127.0.0.1:8080 ═══════════════════════╗
║ bound with net.Listen(tcp4) + IsLoopback() check ║▒
║ forward mode : absolute URL (curl -x) ║▒
║ reverse mode : -target flag / X-Target-URL header ║▒
║ guards : DNS-rebind 403 · self-loop 508 · CONNECT 501 ║▒
╚═════════════════════════════════════════════════════════════╝▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
│
▼ io.TeeReader split
┌───────────────┴────────────────┐
▼ ▼
╔══ [3a] ROUNDTRIP ══════════╗ ╔══ [3b] DLP SCRUBBER ═══════╗
║ http.Transport (isolated) ║▒ ║ async copy, capped at 64KB ║▒
║ Accept-Encoding stripped ║▒ ║ regex scrub, THEN truncate ║▒
║ Host = upstream host ║▒ ║ SHA-256(nonce|secret)[:6] ║▒
║ bytes forwarded untouched ║▒ ║ 32-byte crypto/rand nonce ║▒
╚════════════════════════════╝▒ ╚════════════════════════════╝▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
│ │
▼ ▼
┌── UPSTREAM API ────────────┐ ╔══ [4] RAM RING ════════════╗
│ http:// or https:// │▒ ║ 50 slots · sync.RWMutex ║▒
│ TLS handled by Go, no MITM │▒ ║ req 8KB / resp 16KB max ║▒
└────────────────────────────┘▒ ║ newest-first reads ║▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ║ disk writes: none ║▒
│ ╚════════════════════════════╝▒
◄──┘ original response ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
returns to the app │
▼
╔══ [5] JSON-RPC 2.0 MCP (2026-07-28 + legacy) ═══════════════╗
║ initialize · discover · ping · tools/list · call ║▒
║ inspect_traffic · export_sanitized_curl · clear_ram_buffer ║▒
║ notifications → zero bytes written ║▒
╚═════════════════════════════════════════════════════════════╝▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
│ │
▼ ▼
╔══ STDOUT ▓▓▓ ══════════════╗ ┌── STDERR ░░░ ──────────────┐
║ one compact JSON line ║▒ │ ANSI status bar, │▒
║ per frame, ends in \n ║▒ │ banners, errors │▒
║ NOTHING else, ever ║▒ │ (human telemetry) │▒
╚════════════════════════════╝▒ └────────────────────────────┘▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
│ │
▼ ▼
╔══ AI AGENT (MCP) ══════════╗ ┌── HUMAN ───────────────────┐
║ Claude Code · Desktop ║▒ │ you, in a terminal │▒
║ Cursor · Windsurf ║▒ └────────────────────────────┘▒
╚════════════════════════════╝▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
Your app always gets the real, untouched response. Only the AI agent's view is scrubbed.
The live telemetry bar on stderr (redrawn in place with \r\033[K):
⚡ [NullWire-MCP] Proxy 127.0.0.1:8080 | RAM Ring: 14/50 | Scrubbed: 9 Tokens, 4 Cookies, 6 IPs | Disk: 0B
📊 NullWire-MCP vs. traditional MITM MCP wrappers
| ⚡ NullWire-MCP | Typical MITM-based MCP wrapper | |
|---|---|---|
| Third-party dependencies | 0 (Go standard library only; go.mod has no require) | Dozens of transitive Python / Node / JVM packages |
| Disk footprint | 0 B, RAM-only 50-slot ring; the code contains no file I/O | Flow files / SQLite databases on disk |
| Secret redaction | Salted SHA-256 correlation tags, so reuse is provable and the secret is unseen | None, or blind [REDACTED] that destroys correlation |
| Startup | Native static binary: no interpreter, no VM, no package resolution | Interpreter / VM cold start |
| MCP protocol | 2026-07-28 stateless + 2025-11-25 / 2024-11-05 legacy | Usually legacy only |
| HTTPS upstreams | Reverse gateway: no root CA, no keychain changes | Install a root CA and intercept TLS |
| Supply chain to audit | One main.go | The whole dependency tree |
The right-hand column describes the general class of tools that capture traffic with a MITM proxy and hand it to an LLM. It is not a claim about any specific product. We have not published startup benchmarks; the startup row is architectural.
🚀 30-second install
Option A: go install (Go 1.23+)
go install github.com/VoxNode-ux/nullwire-mcp@latest
Option B: prebuilt static binary, verified against SHA256SUMS.txt from GitHub Releases:
| Platform | File |
|---|---|
| Linux x86-64 / ARM64 | nullwire-linux-amd64 / nullwire-linux-arm64 |
| Linux ARMv7 (Raspberry Pi etc.) / 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 |
# Linux example (swap the file name for your platform)
BASE=https://github.com/VoxNode-ux/nullwire-mcp/releases/latest/download
curl -LO "$BASE/nullwire-linux-amd64"
curl -LO "$BASE/SHA256SUMS.txt"
grep ' nullwire-linux-amd64$' SHA256SUMS.txt | sha256sum -c - # macOS: shasum -a 256 -c -
chmod +x nullwire-linux-amd64
sudo mv nullwire-linux-amd64 /usr/local/bin/nullwire-mcp
# Windows (PowerShell)
Get-FileHash .\nullwire-windows-amd64.exe -Algorithm SHA256 # compare with SHA256SUMS.txt
Option C: build from source
git clone https://github.com/VoxNode-ux/nullwire-mcp && cd nullwire-mcp
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o nullwire-mcp .
Flags
| Flag | Default | Meaning |
|---|---|---|
-port | 8080 | Loopback port. The host is hard-coded to 127.0.0.1; there is no way to bind elsewhere. 0 picks a free port. |
-target | (none) | Default upstream base URL for reverse-gateway mode, e.g. https://api.example.com |
Try it without an AI client
The MCP side lives as long as stdin stays open, so for a quick demo feed it a request on a timer:
( sleep 2
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"inspect_traffic","arguments":{"limit":5}}}'
sleep 1 ) | nullwire-mcp -port 8080 &
sleep 1
curl -s -x http://127.0.0.1:8080 http://example.com/ \
-H 'Authorization: Bearer my_secret_token_123' \
-H 'Cookie: sid=my_secret_cookie' \
-H 'X-Forwarded-For: 198.51.100.1' > /dev/null
wait
You'll see the live status bar on stderr and, on stdout, a flow whose headers read Bearer [REDACTED_TOKEN#…], [REDACTED_COOKIE#…] and [REDACTED_IP#…].
Plug it into your AI client
The client launches nullwire-mcp as a subprocess and talks JSON-RPC over stdio. Your app then sends its traffic to 127.0.0.1:8080. (Config file locations change between client releases, so check your client's docs if a path below is out of date.)
Claude Code CLI
claude mcp add nullwire -- nullwire-mcp -port 8080 -target https://api.example.com
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"nullwire": {
"command": "nullwire-mcp",
"args": ["-port", "8080", "-target", "https://api.example.com"]
}
}
}
Cursor (~/.cursor/mcp.json, or .cursor/mcp.json in a project)
{
"mcpServers": {
"nullwire": {
"command": "nullwire-mcp",
"args": ["-port", "8080", "-target", "https://api.example.com"]
}
}
}
Windsurf (~/.codeium/windsurf/mcp_config.json)
{
"mcpServers": {
"nullwire": {
"command": "nullwire-mcp",
"args": ["-port", "8080", "-target", "https://api.example.com"]
}
}
}
Omit -target if you only use forward-proxy mode or the per-request X-Target-URL header.
🔀 Two capture modes
Mode 1: Forward proxy (plain HTTP)
Send a full URL through the proxy. The request is forwarded as-is and a scrubbed copy lands in the ring.
curl -x http://127.0.0.1:8080 http://httpbin.org/headers
# or for any app that honours the standard variable
HTTP_PROXY=http://127.0.0.1:8080 ./my-service
HTTPS_PROXYmakes clients sendCONNECT, which is answered501by design. See Honest scope. Use Mode 2 for HTTPS.
Mode 2: Reverse gateway (HTTPS upstreams, no MITM certificates)
Talk plain HTTP to NullWire; NullWire speaks TLS to the real server using Go's normal certificate verification. Nothing to install in your trust store.
# Fixed upstream for the whole session
nullwire-mcp -target https://api.github.com
curl http://127.0.0.1:8080/users/octocat
# Or choose the upstream per request (the header is removed before forwarding)
curl -H 'X-Target-URL: https://httpbin.org' http://127.0.0.1:8080/anything?api_key=hunter2hunter2
Point an SDK at it by changing only its base URL:
export OPENAI_BASE_URL=http://127.0.0.1:8080/v1 # with: nullwire-mcp -target https://api.openai.com
The upstream Host header is set to the target's host, so virtual hosting and TLS SNI behave normally.
🧰 MCP tool reference
All three tools return {"resultType":"complete","content":[{"type":"text","text":"…"}],"isError":false}. Tool-level problems (for example an unknown flow id) set isError to true instead of failing the RPC.
inspect_traffic
Recent scrubbed flows, newest first.
{
"name": "inspect_traffic",
"inputSchema": {
"type": "object",
"properties": {
"limit": { "type": "integer", "default": 10, "minimum": 1, "maximum": 50 },
"method": { "type": "string", "description": "HTTP method filter, case-insensitive" },
"url_filter": { "type": "string", "description": "Substring of the scrubbed URL, case-insensitive" }
},
"additionalProperties": false
}
}
Example result text:
{
"ring_size": 3,
"ring_capacity": 50,
"returned": 1,
"scrubbed_totals": { "tokens": 5, "cookies": 1, "ips": 1 },
"flows": [
{
"id": 3,
"timestamp": "2026-10-25T14:03:11Z",
"method": "POST",
"url": "http://[REDACTED_IP#98f1a2]:9000/v1/orders?api_key=[REDACTED_TOKEN#3c9a1e]",
"request_headers": {
"Authorization": "Bearer [REDACTED_TOKEN#a1b2c3]",
"Content-Type": "application/json",
"Cookie": "[REDACTED_COOKIE#d4e5f6]"
},
"request_body": "{\"item\":\"widget\",\"client_secret\":\"[REDACTED_TOKEN#5b7e20]\"}",
"status_code": 401,
"response_headers": { "Content-Type": "application/json" },
"response_body": "{\"error\":\"token expired\"}",
"latency_ms": 42,
"redaction_count": 5
}
]
}
export_sanitized_curl
Rebuilds a runnable POSIX curl command for one flow. Every redaction tag becomes an environment-variable placeholder, and a comment legend maps each placeholder back to the tag it replaced, so you can tell which real value belongs where.
{
"name": "export_sanitized_curl",
"inputSchema": {
"type": "object",
"properties": {
"flow_id": { "type": "integer", "minimum": 1, "description": "An id returned by inspect_traffic" }
},
"required": ["flow_id"],
"additionalProperties": false
}
}
Example result text for flow 3 above:
# Replay of flow 3 (2026-10-25T14:03:11Z)
# Set these environment variables first (distinct tags share one placeholder):
# export NULLWIRE_IP=... # was [REDACTED_IP#98f1a2]
# export NULLWIRE_AUTH_TOKEN=... # was [REDACTED_TOKEN#3c9a1e]
# export NULLWIRE_AUTH_TOKEN=... # was [REDACTED_TOKEN#5b7e20]
# export NULLWIRE_AUTH_TOKEN=... # was [REDACTED_TOKEN#a1b2c3]
# export NULLWIRE_COOKIE=... # was [REDACTED_COOKIE#d4e5f6]
curl -X 'POST' 'http://'"$NULLWIRE_IP"':9000/v1/orders?api_key='"$NULLWIRE_AUTH_TOKEN" \
-H 'Authorization: Bearer '"$NULLWIRE_AUTH_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Cookie: '"$NULLWIRE_COOKIE" \
--data-raw '{"item":"widget","client_secret":"'"$NULLWIRE_AUTH_TOKEN"'"}'
Hop-by-hop headers, Host, Content-Length and Accept-Encoding are left out so curl can set them itself. Truncated or encoded request bodies get a # WARNING: line instead of silently replaying something incomplete.
clear_ram_buffer
Zeroes all 50 slots and resets the scrub counters. Flow ids keep counting upward, so an old id can never point at a new flow. Takes no arguments ({"type":"object","properties":{},"additionalProperties":false}).
🛡️ Security & protocol engineering deep-dive
Every item below is implemented in main.go and exercised by main_test.go.
| Measure | What it does | Why |
|---|---|---|
| Strict loopback bind | The listener address is hard-coded 127.0.0.1, opened with net.Listen("tcp4", …), then re-checked with TCPAddr.IP.IsLoopback(). The process exits if the check fails. There is no -host flag. | Nothing on your LAN can reach a proxy that sees your tokens. |
| DNS-rebinding guard | In reverse mode, any request whose Host header is not 127.0.0.1, localhost or ::1 gets 403 Forbidden. | A malicious web page that rebinds its domain to 127.0.0.1 can't use your gateway as an authenticated relay. |
| Self-loop protection | A target that resolves to the proxy's own host:port gets 508 Loop Detected. | Prevents a request storm from a misconfigured -target. |
| Scrub-before-truncate | Up to 64 KB of each body is captured, fully regex-scrubbed, and only then cut to 8 KB (request) / 16 KB (response). | A secret straddling the 8 KB boundary is already a tag by the time we cut. Truncating first would keep half a token. |
| Per-process ephemeral nonce | 32 bytes from crypto/rand, held only in RAM. Tag = first 6 hex chars of SHA-256(nonce ‖ secret). | Without a salt, a hash of a low-entropy secret (a short PIN, a guessable password) can be brute-forced or rainbow-tabled from a leaked transcript. With a secret nonce nobody can precompute anything, and tags can't be linked across restarts. |
Strict stdout discipline | stdout carries only single-line JSON-RPC frames ending in \n, written under a mutex. Banners, errors and the status bar go to stderr. | One stray byte on stdout corrupts the MCP stream. |
| Silent notifications | A message with no id (or id: null with a notifications/* method), such as notifications/initialized, writes 0 bytes. | JSON-RPC forbids replying to notifications, and strict clients disconnect if you do. |
| Original traffic untouched | The app receives the real response through a streaming, flushing copy. Scrubbing happens on a separate copy in a background goroutine. | The proxy must never break the thing you're debugging. |
| Isolated transport | A dedicated http.Transport with no proxy, no compression and no redirect-following. | No accidental proxy chains; redirects reach your app unchanged. |
| Hop-by-hop stripping | Connection, Proxy-*, TE, Upgrade, … are removed in both directions. | Correct proxy behaviour per RFC 9110. |
| Panic containment | Capture and RPC dispatch recover from panics instead of killing the process. | A weird payload can't take your session down. |
What gets scrubbed
| Pattern | Tag |
|---|---|
Authorization, Proxy-Authorization, X-API-Key, API-Key, X-Auth-Token header values; Bearer / Basic / Token credentials anywhere | TOKEN |
Cookie, Set-Cookie header values (whole header hashed as one unit) | COOKIE |
JWTs (eyJ….….…) | TOKEN |
API keys: sk-…, ghp_…, github_pat_…, AIza…, xox[baprs]-…, AKIA… | TOKEN |
JSON fields: access_token, refresh_token, id_token, api_key, apikey, secret, client_secret, password, passwd, session_id, auth_token | TOKEN |
Query / form params: api_key, apikey, token, access_token, refresh_token, id_token, secret, client_secret, password, passwd, key | TOKEN |
user:password@host in URLs | TOKEN |
IPv4 addresses (127.0.0.1 and 0.0.0.0 are left alone) | IP |
⚖️ Honest scope & known limitations
We would rather you hear these from us than discover them in production.
HTTP CONNECTreturns501 Not Implemented, by design. Inspecting HTTPS throughCONNECTmeans man-in-the-middling TLS, which means generating a root CA and getting it into your OS or browser trust store. That is key material and keychain pollution, the opposite of a zero-footprint tool. The reverse gateway gets the same visibility without any of that: point your app athttp://127.0.0.1:8080and let NullWire do the TLS to the upstream.- No WebSocket /
Upgradepass-through.UpgradeandConnectionare treated as hop-by-hop headers and stripped, so upgraded protocols won't work through the proxy. Plain request/response and streaming responses (e.g. SSE) are supported. - Android / Termux: static Go binaries have no DNS resolver config on Android, so upstream hostnames fail with
502 … lookup … connection refused. Run insidetermux-chroot(pkg install proot), which provides a resolver. Linux desktops, macOS, Windows and FreeBSD are not expected to be affected. - Redaction is pattern-based, not magic. It catches the shapes in the table above. A secret in a format we don't recognise, in a field we don't know, or split across a body boundary can pass through. Treat the scrubber as a strong seatbelt, not a guarantee, and test it with your own traffic. IPv6 addresses are not redacted yet. IPv4-shaped strings that aren't addresses (some version numbers) can be redacted as false positives.
- Tags are 24 bits. Six hex characters are plenty to tell apart the handful of secrets in a 50-slot buffer, but collisions are possible in principle. They are for correlation, not identification.
- Bodies are bounded. Only the first 64 KB of each body is captured, and stored bodies are cut to 8 KB / 16 KB. Responses that arrive with a
Content-Encoding(e.g. gzip) are stored as a short placeholder rather than decoded. NullWire stripsAccept-Encodingfrom forwarded requests so most servers reply uncompressed, but it can't force them to. - "RAM-only" means the program never writes files.
Disk: 0Bin the status bar is a design invariant (the source has no file I/O), not a measured counter. The OS can still swap memory pages or write crash dumps; NullWire doesn't control that. - The ring is ephemeral. The 50 most recent flows vanish when the process exits. That is the point.
- Your app sees raw traffic. NullWire protects what the AI agent sees, not what your local app sees. It isn't a security boundary for the app itself.
- Protocol coverage is what's documented. Initialization, discovery, ping,
tools/listandtools/callfor the 2026-07-28 / 2025-11-25 / 2024-11-05 specs. Resources, prompts and sampling aren't implemented and return-32601. - Release binaries are unsigned. Verify them with
SHA256SUMS.txt. On macOS you may needxattr -d com.apple.quarantine ./nullwire-darwin-arm64.
🧪 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 |
| 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 | live upstream proxy, redaction tags, export_sanitized_curl, reverse gateway, guards (needs termux-chroot) |
| Windows ARM64 | ✅ verified | smoke test, silent notifications, end-to-end redaction |
🟡 emulated means run under QEMU, not on real hardware. Full logs for each run are linked from docs/test-results.md and the Actions tab.
✅ Verification & testing
go vet ./...
go test -v ./...
go test -v -race ./... # the race detector needs cgo (gcc/clang) on most platforms
The suite uses only the standard library (net/http/httptest) and covers:
- Deterministic tagging: equal secrets share a tag, different secrets don't, nothing leaks,
127.0.0.1survives. - Ring-buffer wrap-around eviction and
Clear()with monotonic ids. - A full reverse-gateway round trip:
Accept-Encodingstripped, tokens scrubbed in the ring, a valid sanitizedcurlexported. - MCP dispatch:
server/discover,initialize,ping,tools/list,tools/call, error codes, and zero stdout bytes for notifications.
CI (.github/workflows/release.yml) runs go vet and go test -race, cross-compiles static CGO_ENABLED=0 binaries for nine targets (Linux amd64/arm64/armv7/riscv64, macOS amd64/arm64, Windows amd64/arm64, FreeBSD amd64), and publishes them with SHA256SUMS.txt on v* tags.
❓ FAQ
Does the AI ever see my real token?
Not through NullWire's tools. The agent receives only tags like [REDACTED_TOKEN#a1b2c3]. Redaction is pattern-based, so a secret in a shape the scrubber does not recognise could pass through. See Honest scope.
Does it change what my app receives?
Your app gets the real response through a streaming copy, and scrubbing runs on a separate copy in a background goroutine. One small detail: NullWire strips Accept-Encoding on the upstream request, so responses normally arrive uncompressed. See Honest scope.
How do I debug HTTPS APIs?
Use Mode 2, the reverse gateway: your app talks plain HTTP to 127.0.0.1, NullWire talks verified TLS to the real server. No root CA, nothing in your trust store. HTTPS_PROXY (CONNECT) is answered 501 by design.
Can the agent replay a request without knowing the secrets?
Yes. export_sanitized_curl rebuilds a runnable curl where every tag becomes an environment variable such as $NULLWIRE_AUTH_TOKEN. You set the real values; the agent never has them.
Where is my traffic stored?
In a 50-slot RAM ring. It disappears when the process exits, and clear_ram_buffer wipes it on demand. The program contains no file I/O.
Can I expose it on my network?
No. The listener is hard-coded to 127.0.0.1 and checked with IsLoopback(); there is no flag to bind elsewhere.
🤝 Contributing
Issues and PRs are welcome. Read CONTRIBUTING.md for the ground rules, the test commands and the PR checklist. Security reports go through SECURITY.md, not public issues.
📄 License
MIT © 2026 VoxNode-ux
If NullWire saved you from pasting a live token into a chat window, a ⭐ helps other engineers find it.
Frequently asked questions
What is NullWire-MCP?
RAM-only HTTP debugging proxy and MCP server in Go. AI agents see salted tags, never secrets.
How do I install NullWire-MCP?
Follow the installation steps in its documentation, shown on this page and in its repository.
Is NullWire-MCP open source?
Yes. Its code is on GitHub under the MIT license.
Who maintains NullWire-MCP?
It is published by VoxNode-ux. AI.tn lists it but does not maintain or endorse it.
From the official MCP Registry. Listing is not an endorsement: review a server's code and permissions before giving it access to your data.