The pipeline looks finished the moment it runs. You wire up a PII detector — regex, or Microsoft's Presidio — in front of your model, it finds the names and card numbers and email addresses, swaps them for <PERSON> and <CREDIT_CARD>, and sends the clean text off to the API. The regulated data never leaves your walls. Ship it.
It isn't finished. That's the two-step version of a three-step problem, and the missing step is where agents quietly break.
Redaction destroys two things the model was using#
The first is coreference — the model's ability to track who is who. Presidio's default replace operator maps every entity of a type to the same token. Feed it "Alice and Bob and Alice" and you get "<PERSON> and <PERSON> and <PERSON>" — three mentions the model can no longer distinguish from three different people. "Alice paid Bob" and "Bob paid Alice" anonymize to the identical string. Any task that turns on which person did what — routing, dispute resolution, summarizing a thread — just lost its subject. You protected the data and deleted the meaning.
The second is reversibility. Your agent's job usually ends with the model producing text a human will read: "Dear <PERSON>, your refund of <AMOUNT> is approved." Now what? You have to put the real name back, and you can't — because replace, mask, redact, and hash are all one-way by design. Of Presidio's operators, exactly one round-trips: encrypt, paired with its matching decrypt. Redact with anything else and the model's output is stuck talking to <PERSON> forever.
A black bar over a name protects the name and throws away the one thing your pipeline needs next: the ability to put it back.
The shape that actually works#
The fix is not better redaction. It's a different operation — consistent, reversible pseudonymization, with the mapping held outside the model. Microsoft's own privacy-proxy pattern calls it the anonymize → LLM → de-anonymize sandwich:
- Anonymize to stable, distinct placeholders. John becomes
PERSON_1— the samePERSON_1every time he appears — and Jane becomesPERSON_2. Coreference survives: the model sees two people and keeps them straight, without ever seeing a real name. - Store the map (
PERSON_1 → John Okafor) in your infrastructure, keyed to the request. The model never receives it. - Run the model on the pseudonymized text.
- De-anonymize the output by substituting the real values back from the map.
The word doing the heavy lifting is consistent. Plain replace isn't; it collapses. Salted hash only preserves identity if you pin a fixed salt across the call — and even then you can distinguish people but can't rehydrate them. Instance-preserving replace, the thing most people assume is the default, is still an open feature request. If you want the sandwich, you build the stable mapping yourself or you use the reversible operator.
Two things that will still bite you#
Detection is not a guarantee. PII recognition is largely NER under the hood, and its recall is below 100%. An unusual surname, a passport format you didn't configure, a name that only context reveals — some fraction slips through every model of detector, and which detector you reach for (Presidio, GLiNER, or an LLM doing the scrubbing) is its own tradeoff. Redaction is risk reduction, and selling it internally as a guarantee is how a leak becomes a surprise. The only hard control is not sending the data at all: keep the sensitive field in a place the agent never reads, or route the sensitive step to a locally hosted model.
The prompt is not the only exit. You can scrub the request perfectly and still leak, because the request is one of several places the data travels. Your observability and eval stack logs the raw prompt and the raw completion by default — so a clean prompt with a raw trace has just written the PII to a second system, often a third-party SaaS one. Put the redaction at the proxy boundary, and make sure the anonymized text is what gets logged, traced, and stored for evals.
The mental model to keep is that redaction is lossy compression of identity, and most teams pick a setting that loses far more than privacy requires. A black bar throws away position, coreference, and reversibility to hide one name. Consistent pseudonymization with a key held outside the model throws away only the real values — and hands them back the moment the model is done. Same privacy boundary. All of the task, intact.



