Handbooks  /  PII in LLM Pipelines
Handbook~15 min readPrivacyworked math + runnable code
The PII in LLM Pipelines Handbook

Personal data
stays home.

The moment you send a prompt to a hosted model, whatever's in it — including a customer's email, a patient's record, a support ticket full of names — leaves your systems. For regulated data that can be a violation; for everything else it's exposure you didn't have to take on. The fix is a pattern with a satisfying invariant: replace the personal data with placeholders before the model ever sees it, let it reason about the shape of the request, then swap the real values back into the answer. The provider only ever sees [PII_0]. Here's how to make that airtight.

01

The boundary problem

Every hosted-model call crosses a trust boundary: your prompt travels to a third party's servers, where it may be logged, cached, or — depending on the terms — retained. Anything personal in that prompt goes with it. For data governed by regulation (health records under HIPAA, personal data under GDPR, and their kin) that transfer can be unlawful without the right agreements; even where it's permitted, it enlarges the set of places your users' data lives and the number of ways it can leak.

The principle is straightforward: personal data you don't send can't be exposed. So the goal is to let the model do its job without ever seeing the actual PII. You can't just strip the data and hope the request still makes sense — a support summarizer needs to know that there's a customer and an email, even if it doesn't need the real address. The trick is to preserve the structure while hiding the values, and that's exactly what redact-before-send does.

The one-sentence version

Replace every piece of PII with a stable placeholder before the prompt reaches the model, restore the real values in the response — so the provider only ever sees placeholders, never personal data.

02

Redact before send

The redact-before-send pattern is a round trip. On the way out, you detect PII in the prompt and mask each value — swap it for a placeholder token like [PII_0] — keeping a private mapping from placeholder back to the real value. You send the masked prompt; the provider sees only placeholders. On the way back, you restore: replace the placeholders in the model's response with the real values from your mapping, inside your own trust boundary.

The round trip — PII never crosses the boundary
DetectFind PII in the prompt (emails, phones, IDs, names…).
MaskReplace each value with a placeholder; save the value→placeholder map.
SendThe model sees only placeholders — zero real PII leaves your systems.
RestoreSwap placeholders back to real values in the response, inside your boundary.

The model reasons about a request that's structurally intact — "email [PII_0] asked about their order" — and produces an answer that references the placeholders, which you rehydrate into "email jane@acme.com asked about their order." The whole exchange with the provider is placeholder-only. The invariant you're enforcing is that the outbound text contains no original PII — that's the property to test, and the math makes it exact.

03

Consistent placeholders

A subtlety makes the pattern actually usable: the same value must map to the same placeholder. If a prompt mentions one email twice, both occurrences become [PII_0], so the model understands they're the same entity and its reasoning stays coherent — and you can restore them together. Map them inconsistently and the model would think one person is two, garbling the answer.

Equally, distinct values get distinct placeholders ([PII_0], [PII_1], …), preserving the relationships in the text — who emailed whom, which id belongs to which record. So the mapping is a bijection between the real values and the placeholders: consistent within a value, unique across values. That's what lets a masked prompt carry all the structure the model needs while carrying none of the content that's sensitive. Get the mapping right and the model can't tell it's working on redacted data — except that it never learns anything it shouldn't.

04

The invariant

Masking maps each token through a substitution: PII values become placeholders, everything else passes through. The mapping m is a bijection from the PII values to fresh placeholders. The property that matters is the leakage invariant: the masked, outbound text contains none of the original PII:

leaked(mask(text))  =  { t ∈ mask(text) : t ∈ PII }  = 

Every detected PII value is replaced, so nothing on the deny-list survives into the text you send. That empty set is the guarantee.

And because the mapping is a bijection, masking is reversible — restore composes with mask to recover the original exactly:

restore( mask(text) )  =  text   (round-trips via the inverse mapping m−1)

So you lose nothing: the provider sees placeholders, you keep the ability to reconstruct the real answer. The runnable version below masks PII, checks the leak invariant is empty, and round-trips the restore.

RUN IT YOURSELF

Mask, send, restore

Redact-before-send is a reversible substitution. Masking replaces each PII value with a stable placeholder and records the mapping, so the outbound text contains zero original PII — that's the leak invariant, an empty set. The same value always gets the same placeholder (so the model treats it as one entity), and distinct values get distinct placeholders (preserving relationships). Because the mapping is a bijection, restore round-trips the masked text back to the original exactly. Change the tokens or the PII set and confirm nothing leaks.

CPython · WebAssembly
05

Defense in depth

Redaction is powerful but not sufficient alone — because detection is imperfect. Combine it with other controls:

ControlWhat it adds
Redact before sendMask PII to placeholders so real values never reach the provider — the primary control.
Data minimizationSend only the fields the task needs; the best-protected data is the data you never transmit.
Contract (BAA / DPA)A business-associate or data-processing agreement, with training-on-your-data disabled.
Local / private modelFor the most sensitive data, a local model means nothing leaves at all.
Output guardrailsScan responses for leaked PII too (a guardrail) — model or tools can surface it.
Human reviewRoute high-risk flows past a person before anything sensitive is sent or shown.

Structured PII — emails, phone numbers, card numbers, IDs — is easy to detect with patterns and reliable to redact. The hard cases are names, addresses, and context-dependent identifiers, where even good detectors miss some. So layer the controls: redaction as the main line, minimization to shrink the surface, contracts and (for the most sensitive data) local models to bound the worst case, and output scanning to catch anything that slips through. This is the same defense-in-depth logic as everywhere in security — no single control is perfect, so you stack independent ones.

06

Pitfalls

The core limitation to internalize: a missed entity is a leak. Redaction is only as good as detection, and no detector catches every name or oddly-formatted identifier — so treat it as strong risk reduction, not an absolute guarantee, and back it with minimization and contracts. A related trap is redacting too aggressively and destroying the task: strip so much that the model can't understand the request, and you've traded a privacy win for a broken feature. Preserve structure with consistent placeholders rather than blanking everything.

Two more. Leaking through the response or logs: your own logs, traces, and prompt-caches can store the un-redacted prompt if you're not careful — apply the same discipline to observability data, and restore PII only at the last moment, inside the boundary. And PII in unexpected places: it hides in free-text fields, file uploads, image content, and tool results, not just the obvious form fields — scan everything that becomes part of a prompt. Done well, redact-before-send lets you use hosted models on real-world data without handing that data away — the personal parts simply never leave.

Worth knowing

Redaction is only as strong as detection — a missed name or odd identifier is a real leak, so redaction is risk reduction, not a guarantee. Layer it with data minimization, a BAA/DPA (training disabled), local models for the most sensitive data, and output scanning — and keep your own logs and caches redacted too.

Frequently asked

Quick answers

What's the risk of PII in prompts?

Sending a prompt to a hosted model transfers any personal data in it to a third party, which can be a compliance violation and always widens exposure.

What is redact-before-send?

Detect PII, replace it with placeholders before the model sees it, and restore the real values in the response — the provider sees only placeholders.

Why consistent placeholders?

Mapping the same value to the same placeholder lets the model treat it as one entity, keeping its reasoning coherent and the restore correct.

Is detection enough alone?

No — detectors miss some names and identifiers, so pair redaction with minimization, contracts, local models, and output scanning.

Finished this one? 0 / 99 Handbooks done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.