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.
1. Run a fake vendor on 127.0.0.1:9999
Section titled “1. Run a fake vendor on 127.0.0.1:9999”In a first terminal, start the echo upstream:
python3 - <<'PY'import http.server, sysclass 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): passhttp.server.HTTPServer(("127.0.0.1", 9999), Echo).serve_forever()PYIt prints every request body it receives to stderr. Leave it running.
2. Point a throwaway gateway at it
Section titled “2. Point a throwaway gateway at it”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:
export TOKENHUSH_HOME="$(mktemp -d)"mkdir -p "$TOKENHUSH_HOME/config"cat > "$TOKENHUSH_HOME/config/tokenhush.yaml" <<'YAML'listen: host: 127.0.0.1 port: 8787upstreams: - match: /v1/chat/completions target: http://127.0.0.1:9999YAMLtokenhush runLeave it running too. It prints one masked stderr line per redaction.
3. Send one request
Section titled “3. Send one request”In a third terminal, send a request whose body carries a fake secret:
curl -sS http://127.0.0.1:8787/v1/chat/completions \ -H 'Content-Type: application/json' \What you should see
Section titled “What you should see”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.comtokenhush: restored response placeholders=13. The curl output contains the original value again. The response path
restored the session placeholder before the client saw the body:
The upstream never saw the secret; the client never saw the placeholder.
Read the gateway like a script
Section titled “Read the gateway like a script”With the gateway still running, query its metadata:
tokenhush status --jsonThe 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": 1The redaction log
Section titled “The redaction log”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.
The restore log
Section titled “The restore log”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.
Why this proves it
Section titled “Why this proves it”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.
See also
Section titled “See also”- tool-setup.md for pointing a real tool at the gateway.
- security.md for the invariants this check exercises.
Last updated: