Skip to content

Verify redaction with a local echo upstream

Tokenhush is a local, loopback-only HTTP gateway that redacts secrets and PII in AI coding tool requests before they reach the model, and restores them in the response. This check proves the whole round trip without sending anything to a real provider: you run a fake vendor that echoes request bodies, point a throwaway gateway at it, send one request that carries a fake secret, and watch where the secret does and does not appear.

You need Go 1.25+, plus python3 and curl.

In a first terminal, start the echo upstream:

Terminal window
python3 - <<'PY'
import http.server, sys
class Echo(http.server.BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
sys.stderr.write("upstream received: " + body.decode() + "\n"); sys.stderr.flush()
self.send_response(200); self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body))); self.end_headers()
self.wfile.write(body)
def log_message(self, *args): pass
http.server.HTTPServer(("127.0.0.1", 9999), Echo).serve_forever()
PY

It prints every request body it receives to stderr. Leave it running.

In a second terminal, create a throwaway TOKENHUSH_HOME, write a minimal tokenhush.yaml that routes /v1/chat/completions to the echo upstream, and start the gateway:

Terminal window
export TOKENHUSH_HOME="$(mktemp -d)"
mkdir -p "$TOKENHUSH_HOME/config"
cat > "$TOKENHUSH_HOME/config/tokenhush.yaml" <<'YAML'
listen:
host: 127.0.0.1
port: 8787
upstreams:
- match: /v1/chat/completions
target: http://127.0.0.1:9999
YAML
tokenhush run

Leave it running too. It prints one masked stderr line per redaction.

In a third terminal, send a request whose body carries a fake secret:

Terminal window
curl -sS http://127.0.0.1:8787/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"echo","messages":[{"role":"user","content":"my email is [email protected]"}]}'

Before any request the gateway prints a startup banner: the loopback endpoint, the effective upstream routing (configured entries plus the built-in fallbacks) and the two base-URL forms to point a tool at. Nothing in it is secret. Then the three observations below together prove the round trip.

1. The upstream terminal printed the body with the secret replaced. The value left the gateway as a placeholder, so this is what the fake vendor received:

upstream received: {"model":"echo","messages":[{"role":"user","content":"my email is __PII_email_<digest>__"}]}

The <digest> is a per-session hash, so the exact value differs between runs. The point is that the upstream never saw [email protected].

2. The gateway terminal printed two metadata-only lines. The value it changed is reported by type, length, and a masked form, never in full; the return path reports only how many placeholders it restored:

tokenhush: redacted request email (len=14) ****@example.com
tokenhush: restored response placeholders=1

3. The curl output contains the original value again. The response path restored the session placeholder before the client saw the body:

{"model":"echo","messages":[{"role":"user","content":"my email is [email protected]"}]}

The upstream never saw the secret; the client never saw the placeholder.

With the gateway still running, query its metadata:

Terminal window
tokenhush status --json

The JSON document carries the ten frozen keys (state, addrs, port, uptime_ms, requests, redactions, content_policy_blocks, rule_blocks, walk_skips, pack_serial). After the single request above, redactions is 1:

"redactions": 1

The format is:

tokenhush: redacted request <type> (len=<N>) <masked>

It goes to stderr only and is never persisted. <masked> is **** for most types. For the opaque credential types (api_key, high_entropy) it is a bounded prefix and suffix instead, for example sk-p…j0. A private_key match reports its PEM header kind, for example RSA PRIVATE KEY, and an email match reports only its domain, for example ****@example.com. The masked form can never equal the secret: a literal **** renders as [redacted].

The log is on by default. Silence it with tokenhush run --log-redactions=false.

Every client-bound response that restores at least one session placeholder emits one count line:

tokenhush: restored response placeholders=<N>

A buffered response emits one line with the total; a streaming (SSE) response emits one line per restored placeholder. The line carries only a count, goes to stderr only, and is never persisted. tokenhush run --log-redactions=false silences it together with the redaction log.

The upstream is the stand-in for the model. It received only __PII_email_<digest>__, so the secret never left the machine in a provider-bound request. The client received [email protected] back, so the placeholder was restored on the return path. And the outbound direction never backfills: the gateway has the forward map (secret to placeholder) and no reverse map, so a prompt-injection trick that asks the model to echo a secret cannot make the gateway re-fill a placeholder on the way out. That is the round trip you just watched: secret out of the body, placeholder upstream, original back to the client.

Last updated: