Skip to main content
Glama

⚡ 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.

CI Release Go Dependencies: 0 Disk writes: 0 bytes MCP License: MIT

Quick start · How it works · Install · Tools · Security · 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


Related MCP server: mcp-guardian

⚡ 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/orders returning 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]. The nonce is 32 random bytes drawn from crypto/rand at 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.

╔══ [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_PROXY makes clients send CONNECT, which is answered 501 by 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 CONNECT returns 501 Not Implemented, by design. Inspecting HTTPS through CONNECT means 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 at http://127.0.0.1:8080 and let NullWire do the TLS to the upstream.

  • No WebSocket / Upgrade pass-through. Upgrade and Connection are 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 inside termux-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 strips Accept-Encoding from forwarded requests so most servers reply uncompressed, but it can't force them to.

  • "RAM-only" means the program never writes files. Disk: 0B in 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/list and tools/call for 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 need xattr -d com.apple.quarantine ./nullwire-darwin-arm64.


🧪 Tested on

Platform

What has been verified

Linux x86-64

CI: go vet, unit tests with -race, MCP smoke test, end-to-end redaction

Windows x86-64

CI: MCP smoke test, end-to-end redaction

macOS (Apple Silicon, Intel)

CI: MCP smoke test, silent notifications, end-to-end redaction

FreeBSD x86-64

CI (FreeBSD VM): MCP smoke test, silent notifications, end-to-end redaction

Linux ARMv7 & RISC-V 64

CI (QEMU user-mode): MCP smoke test, silent notifications, end-to-end redaction

Linux ARM64 (Android / Termux)

Manual on-device: live upstream proxy, redaction tags, export_sanitized_curl, reverse gateway and guards

Windows ARM64

Cross-compiled in CI only; not run. Reports welcome

CI results (including raw smoke-test output) are on 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.1 survives.

  • Ring-buffer wrap-around eviction and Clear() with monotonic ids.

  • A full reverse-gateway round trip: Accept-Encoding stripped, tokens scrubbed in the ring, a valid sanitized curl exported.

  • 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

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.

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.

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.

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.

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.

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. The whole project is one main.go plus tests, so it is easy to read end to end.

go vet ./... && go test -v ./...

Good places to help:

  • Found a secret format that slips through? Open an issue with a fake example of its shape (never a real secret).

  • Scrubber coverage: more key formats, IPv6 addresses (currently IPv4 only).

  • Platform reports: Windows ARM64 is cross-compiled in CI but not run. Reports are very welcome.

  • Client recipes: config snippets for other MCP clients.


📄 License

MIT © 2026 VoxNode-ux

If NullWire saved you from pasting a live token into a chat window, a ⭐ helps other engineers find it.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Guardrail sidecar proxy between AI agents and their MCP/REST/CLI tools. Policy engine, human approval gates, time-limited grants, rate limiting, and OTEL tracing. One Go binary, one YAML config, fail-closed by default.
    1
    Apache 2.0
  • A
    license
    A
    quality
    B
    maintenance
    Security, cost, and health governance proxy for MCP infrastructure. Enforces YAML-configurable security policies (blocklists, rate limits, token budgets), tracks real token costs via tiktoken, monitors server health with live JSON-RPC probes. Features OAuth 2.1/OIDC with RBAC, web dashboard, payload normalization, semantic shell AST analysis, mTLS, and a formal STRIDE threat model.
    4
    35 npm
    4
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Transparent Go proxy that intercepts, signs, rate-limits, redacts, and audits all MCP JSON-RPC tool calls without modifying client or server. Stores to JSONL or SQLite with HMAC-SHA256 signatures.
    14
    8
    Apache 2.0