Use a powerful LLM without handing it your users' identifiers. Names, IDs, phones and addresses are replaced with random opaque tokens before the payload leaves your machine — and an egress gate refuses to send anything that still looks raw.
The honest answer to "but everything just goes to OpenAI, PII and all." The identifiers stay home. We also had this design adversarially audited — it failed, we fixed it, and the findings are published below rather than buried.
How it works
1
Detect & tokenize
Each identifier → a random 128-bit token, mapped in a scoped local vault that never leaves your tenant. Emails, SA IDs, phones, cards (deterministic patterns) plus a local-model pass for free-form names and addresses.
2
Send only tokens
The token is random, so there is no preimage to crack — and it doesn't encode the data type either. Within one conversation the same value maps to the same token, so the model can still reason about one person without knowing who.
3
The brain reasons blind
It returns its answer still in tokens. It never held a name, a number, or an address.
4
Re-hydrate locally — provenance-bound
Only tokens we actually issued into that request are swapped back. A model that echoes a token it was never given gets nothing — so a compromised or prompt-injected reply can't pump other people's data out of the vault.
⛔
The egress gate — fails closed
Every outbound payload is re-checked before the call. Raw identifier present → BLOCKED, and the request raises rather than proceeding. If the local recognizer is required but unreachable, that is also a refusal — a detector that is down must never look like a detector that found nothing.
What the model actually sees
A worked example with a fictional person. (Real values never appear — not in our logs, not on this page.)
Your textJane Example (ID 0000000000000, [email protected], +27 00 000 0000) asks if her R5 000 loan is approved.
What the brain sees⟦T-_SL7GQeQmW_0B7QmMhiZOg⟧ (ID ⟦T-AcaUK0seYRcTg3UdBaRfiw⟧, ⟦T-h4wn8Bg89SVcEZY25TJb6A⟧, +⟦T-5_Yws_YqiZcO2GNdlpfbDQ⟧) asks if her R5 000 loan is approved.
Brain repliesYes — ⟦T-_SL7GQeQmW_0B7QmMhiZOg⟧'s loan is approved.
You receiveYes — Jane Example's loan is approved.
↑ Every value here is invented (note the .invalid email, the all-zero number). Tokens are real output from the current build. This page contains no real personal data — the same rule the firewall enforces.
⚖️ Honest scope — what it does and doesn't cover
It tokenizes identifiers, not clinical content. Names, IDs, phones, addresses are
replaced. Free-text content — a diagnosis, a lab value, a medication — is
not detected and travels to the model as written. For health data this is the
limit that matters most, so we state it first.
Quasi-identifiers can still identify. An age, a clinic name and a date can single
out one person in a small population even when every explicit identifier is tokenized.
Tokenization does not solve this; scope and data-minimisation do.
Deterministic core, probabilistic assist. The pattern layer (email, SA ID with
checksum validation, phone, card) is deterministic and repeatable. The local-model pass that
catches free-form names and addresses is best-effort, runs entirely on your machine, and is
an addition to the deterministic layer — never the only control.
The gate is a second application, not a second opinion. It re-runs the same
detectors, so it catches a tokenizer that found a value and failed to substitute it — but it
cannot catch a class neither detector knows about. We would rather name that than imply a
catch-all.
The vault never leaves, is scoped per conversation, expires, and can be purged. It
never renders real values into logs or stack traces.
🔍 We attacked it ourselves
In July 2026 we ran a multi-agent adversarial audit against this design — six independent
threat lenses, every finding required to survive a refutation pass. It found four critical
issues. We publish them because a security claim you can't check is just marketing.
Found: tokens were a truncated unsalted hash of the value. An SA ID was
recovered from its token in 0.08 s with no access to the vault; names and emails fell
instantly to a wordlist. Fixed: tokens are now random — there is no preimage to attack.
Found: re-hydration blind-replaced every token it held, so a prompt-injected reply
could pump out other people's values. Fixed: substitution is bound to the tokens
actually issued in that exchange.
Found: the send path computed its leak check and then called the model anyway.
Fixed: it now refuses.
Found: one unbounded vault shared across every subject. Fixed: scoped, TTL'd,
purgeable.
Each fix ships with an attack regression — the audit's exact exploit,
re-run on every build, asserted to fail. 20/20 checks pass, deterministic and offline.
github.com/ajgreyling/pii-firewall