How it works
How Tokenhush works
Tokenhush is a local, loopback-only HTTP gateway that replaces secrets and PII in an AI coding tool's request body with reversible session placeholders before they reach the model, and restores them in the response. Point a tool at the loopback gateway instead of the vendor endpoint: outbound, the whole JSON body is walked and every match becomes a session placeholder before the request is forwarded; inbound, the entire response is buffered and decoded first, and the placeholders come back as the originals.
Loopback only, no MITM, no root certificate; the secret-to-placeholder mapping lives in memory for the session.
The two directions of one request
The gateway sits between your tool and the model provider. Request bytes travel out through one gate and response bytes travel back through it, but the two directions are not symmetrical: substitution runs on the request path, and the originals are restored on the response path.
Outbound request
Your tool sends the request
The tool points at the loopback gateway (127.0.0.1:8787), so the request body reaches the gateway before any vendor endpoint sees it.
The gateway walks and rewrites it
The whole declared-JSON body is read and the enabled detectors run; each match becomes a session placeholder such as
__PII_api_key_ae9c0b46a8f3__.The provider receives the rewritten body
The upstream sees the placeholders, not the values they stand for. A value no detector flagged still travels as your tool wrote it, which is why detector quality is the honest boundary of the tool.
Inbound response
The response is buffered whole
Status and headers are recorded and the body —
text/event-streamincluded — is buffered and decoded first. Nothing streams token-by-token, and nothing is committed yet.It is evaluated, then back-filled
Placeholders this session minted are restored to the originals; a foreign placeholder — one this session did not mint — is returned unchanged.
Your tool receives the response
Backfill runs last and the response is committed once, so there is no point where the client holds a partial response the gateway could not take back.
The outbound-never-backfill invariant
Placeholders are never filled back in on the way out. Only your client gets the originals.
Back-fill runs in exactly one direction: on the response path to your client. The public core pins that with a named test, and the outbound writer holds only the forward map — it has no reverse map and no restore method.
The invariant closes a specific attack path. Prompt injection can try to make a coding tool echo a stored secret into a new outbound request; if the gateway refilled placeholders on the way out, that request would carry the original value to the model. Because restore is client-bound only, a placeholder that appears in an outbound body stays a placeholder.
- A design invariant, not a protection guarantee. Detection is deterministic and precision-first: false positives degrade model output, false negatives let content through. Read this page as high-confidence interception, not as a promise that nothing sensitive can leave.
- The mapping is in-memory and session-scoped. No request or response body, detected secret, or placeholder-to-secret mapping is written to disk. Restarting the gateway drops the mapping, so a placeholder can reach your tool unchanged — visible degradation rather than a silent failure.
- A foreign placeholder is returned unchanged. A placeholder this session did not mint is returned byte-identical rather than guessed at, so nothing is fabricated into a secret.
Six detector categories, five on by default
Deterministic rules run on the outbound request body; each match becomes a session placeholder before the request is forwarded.
| Detector | What it catches | Trade-off |
|---|---|---|
| prefix | Vendor key shapes — sk-, AKIA, ghp_, glpat-, xox*, AIza, npm_ | A finite list: a credential that does not carry a known prefix is left to another detector. |
| jwt | JSON Web Tokens, recognized by their three-part structure | A shape rule rather than a validator: it matches the token form and makes no signature check. |
| pem | PEM private-key headers (-----BEGIN … PRIVATE KEY-----) | A private key that arrives without a PEM header is outside this rule. |
| luhn | Card numbers that pass the Luhn check | Checksum-first: a candidate digit run that fails the Luhn check is not replaced. |
| Addresses ending at a known public suffix (.com, .co.uk, …) | Precision by suffix: the domain must end at a label boundary with a known public suffix to match. | |
| entropy | High-entropy strings, off by default and opt-in per workload | Its false positives on real agent traffic — long tool names and session ids — broke function calling; a legitimate base64 payload looks the same as a secret by construction. |
Beyond the built-in rules, two extension paths exist: an allowlist for literals that must not be redacted, and compile-time content plugins. A signed rule pack may add detections, and the non-weakening floor refuses a pack that would weaken a built-in.
Buffered whole, committed once
Every upstream response — text/event-stream included — is buffered and decoded before any byte is committed. There is no token-level streaming, and no point where the client holds a partial response the gateway could not take back.
- Buffer capA buffered response over response_buffer_bytes (default 32 MiB) is a 502, decided before any byte is committed.
- DeadlineA response past response_timeout (default 5m) is a 504, decided before commit. The buffer cap wins if both trip.
- Backfill runs lastRestore is the final step on the client-bound path, after evaluation; a foreign placeholder is returned unchanged.
502 response over response_buffer_bytes
504 response over response_timeoutBoundaries that travel with the design
The two paths above come with limits that are written down rather than discovered later.
Encoded secrets are not caught
A secret that is base64-, hex-, or URL-encoded before it leaves is not detected: the rewrite has no normalization pass. JSON string escaping is handled in both directions, and a non-identity request Content-Encoding is refused with 415 rather than decoded for detection.
The response path does not redact
Response-scoped rules may allow, warn, or block only; substitution is request-path only, and a rule that tries to redact a response is rejected when it is compiled.
A secret in a JSON object key is not caught
Only values are walked, so a secret used as an object key is forwarded unchanged.
The full boundary list and the residual risks that come with it are recorded in the security model.
Where this is written down
Both paths are specified in the documentation, alongside the invariants and the risks that remain.
- Concepts
Architecture: layers and rule abstraction
The data path at a glance, the frozen byte surfaces, and why the CLI holds exactly seven commands.
Read the architecture doc - Security
Security model and invariants
The eight invariants the core keeps, the nine residual risks it accepts, and the test that pins each one.
Read the security model
Open source
Watch one request pass through
Start the gateway with tokenhush run and follow its stderr: one masked line per redaction, one count per restore, one reason per local refusal. The verification guide walks through the whole round trip.
- Apache-2.0
- No root certificate