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.

One request, two lanes: the outbound lane forwards a body in which every match is a placeholder, and the inbound lane restores the originals only on the way back to your tool.

Outbound request

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

  2. 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__.

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

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

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

  3. 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 set, defaults, and trade-offs from the current public core.
DetectorWhat it catchesTrade-off
prefixVendor 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.
jwtJSON Web Tokens, recognized by their three-part structureA shape rule rather than a validator: it matches the token form and makes no signature check.
pemPEM private-key headers (-----BEGIN … PRIVATE KEY-----)A private key that arrives without a PEM header is outside this rule.
luhnCard numbers that pass the Luhn checkChecksum-first: a candidate digit run that fails the Luhn check is not replaced.
emailAddresses 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.
entropyHigh-entropy strings, off by default and opt-in per workloadIts 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.

Response path

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.
Refusals, before commit
502  response over response_buffer_bytes
504  response over response_timeout

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

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