Two directions, and people only think about one.
Everyone worries about what goes up to the model. The same rules run on the reply coming back down.
Rules plus a model you host.
Up: the model never sees the real value
user "Refund the card for [email protected], order 88213" | | redact before it leaves your environment v model "Refund the card for <REDACTED_EMAIL_ADDRESS_1/>, order 88213" | | model answers v gateway "I've refunded <REDACTED_EMAIL_ADDRESS_1/> for order 88213." | | restore on the way back v user "I've refunded [email protected] for order 88213."
Your own application never knows anything was swapped: no change to the agent, no change to the prompt, no change to how you read the answer.
Down: the user never sees what the tool returned
A model or a tool can return more than the person in front of it should read. A support assistant queries an order and gets back the full customer record. A sales assistant asks about an account and the tool returns the counterparty names on every other deal.
Nobody lists that second category as personal data and nobody scans for it, because the risk is a business secret leaking, not a card number. The same mechanism hides it on the way out, written as a rule on the response instead of the request.
What it recognizes
| Kind | How |
|---|---|
| Card numbers | pattern plus a Luhn check, so a random sixteen digits is not a card |
| Addresses, phones, emails | pattern plus a validator |
| Public IPs | routable addresses only — loopback, private, link-local and reserved ranges are not personal data |
| Secrets and keys | entropy, for a credential that matches no pattern |
| Names, places, identifiers, medical license numbers | a named-entity layer you switch on, running as its own container beside the engine, in English with a multilingual fallback |
And it knows what is not personal. noreply@, [email protected], [email protected], a placeholder local part — none of them are a person, and flagging them is how a redaction layer teaches your team to ignore it. That list exists because somebody ran this against real traffic and got tired of the noise.
In healthcare, the interesting failure is a combination
Fields that are harmless on their own identify a person together. Safe Harbor removes eighteen identifiers. A diagnosis is not among them, a birth year is allowed to remain, and a ZIP code keeps its first three digits. A prompt holding all three passes a check that reads one field at a time, and re-identifies the patient anyway. A rule can act on the combination rather than on the fields.
A de-identification miss is not a hypothetical, so the record shows what was redacted and what was not, decision by decision.
Streaming does not create a hole
A streaming model emits a few words at a time, too little to match against. The answer text is reassembled into whole sentences and checked before those sentences reach the reader, and it keeps streaming. Reasoning and tool-call arguments stream piece by piece unless your rule buffers the response. Where a rule does not buffer them, the record names what went unchecked instead of reporting the response clean.