Back to Blog
Abstract data patterns representing personal information being tokenized before an LLM call

LLM PII Redaction: Scrubbing Data Before It Reaches the Model

11 min read

PII redaction fails in two expensive ways. A weak filter lets names, account details, or health information reach the model. An aggressive filter strips order numbers, URLs, and domain terms the workflow needs, and the output becomes useless. One giant regular expression fixes neither. Put a typed privacy gateway before every model call, decide which fields the task needs, replace protected values with stable tokens, keep the restoration map out of the prompt, scan the output, and test both privacy and task quality. This guide gives backend engineers the sequence, decision rules, an example, and release checks, plus two findings most guides leave out: what redaction costs in accuracy, and why checksum-based detectors misfire on long IDs.

Why PII filters fail in real workflows

Personal data rarely stays in one field. A support request can hold a customer name in the sender record, an email address in the body, an account number in a screenshot transcript, and a delivery address pasted into an agent note, next to values that look sensitive and are harmless, such as an internal ticket ID.

Detection is a classification problem. A regular expression can spot a credit-card-like sequence but cannot decide whether AB-10492 is an account identifier, a product code, or a case number. Entity recognition adds context, along with scores, model behavior, and false positives. Presidio's Analyzer documentation exposes recognizers, context, and confidence scores because detection needs policy and tuning around it.

False positives are real. In Presidio issue 1498, users reported a URL recognizer firing on ordinary content inside code snippets. The lesson holds whatever happens to that one recognizer: a detector's label is evidence for a policy decision, not permission to delete text.

Misses are the other risk. Presidio's README warns that "there is no guarantee that Presidio will find all sensitive information" and that additional protections should be used. Treat detection as the last layer behind field minimization, never the only one.

Removing obvious names does not prove that a record is anonymous. NIST defines de-identification around whether data can still be linked to specific people. The ICO's anonymisation guidance also distinguishes anonymisation from pseudonymisation and treats identification risk as contextual. A token that can be reversed through a lookup table is a useful engineering control. It is a different claim from irreversible anonymisation.

Put one privacy gateway around model access

Route model-bound content through one service or library, not redaction calls scattered across prompts, controllers, and background jobs. The gateway accepts a typed request and returns a sanitized payload plus a reference to protected restoration state.

Pass these fields in each request:

  • the workflow name and version;
  • the data-purpose label, such as classification or response drafting;
  • the authenticated subject and tenant;
  • structured fields plus free text;
  • the permitted entity types for this task;
  • whether any tokens may be restored after inference.

It returns the sanitized content, detector findings, policy decisions, and a short-lived mapping identifier, and never the raw token map. That keeps a debug log, trace, or later prompt step from recording both the token and its original value.

This follows OWASP LLM02:2025 Sensitive Information Disclosure, which recommends sanitization before model processing and names tokenization and redaction as mitigations.

Classify fields by purpose before scanning text

Begin with the input schema. For each field, record four properties:

  1. whether the model needs the field to perform the task;
  2. which entity types the field may contain;
  3. whether replacement must preserve relationships between repeated values;
  4. whether the output is allowed to restore the original value.

A support-ticket classifier probably does not need the sender's email address. Drop that structured field before running any detector. A response-drafting workflow may need to distinguish the customer from another person mentioned in the ticket, but it does not need either name. Replace them with stable typed tokens such as [PERSON_1] and [PERSON_2]. A shipping-status workflow may need the final four characters of an order identifier for the user-facing response. Keep only that permitted representation.

Purpose-first classification avoids the common mistake of sending every field through a detector and trusting the masked output. Data with no task purpose never enters the model-bound payload, and detection runs only on fields that must continue.

Keep the policy in version-controlled configuration so a reviewer can see that support_triage.v3 drops customer_email, tokenizes person_name, permits product_name, and never restores health_identifier. A policy change goes through the same release process as a prompt or tool-schema change.

Use layered detection and typed tokens

Run deterministic rules first for values you control: exact customer IDs, internal account prefixes, known secret formats, and values already present in structured fields. Escape the values before matching and set minimum lengths so short common strings do not erase unrelated words.

Run entity recognizers next for names, addresses, phone numbers, government identifiers, and other contextual data, configured per language and workflow. Do not enable every entity type globally: a recognizer useful for legal intake can be destructive in source-code support.

Convert accepted findings into typed tokens, not a generic [REDACTED] marker. If the same email appears three times, every occurrence becomes [EMAIL_1], and different people get different person tokens, so the model can still summarize who said what. Keep tokens stable within one run and fresh across runs: a token that follows the same person across sessions lets anyone holding the logs link those sessions.

Store the mapping in an encrypted, tenant-scoped record with a short expiry, bound to the workflow run and purpose. The model never requests restoration. Application code restores an allowed token into an allowed output field after the response passes validation.

def prepare_model_input(request, policy):
    payload = drop_unneeded_fields(request.payload, policy)
    findings = deterministic_scan(payload, policy.exact_rules)
    findings += entity_scan(payload, policy.enabled_entities)

    accepted = apply_thresholds_and_exceptions(findings, policy)
    sanitized, token_map = replace_with_typed_tokens(payload, accepted)

    mapping_id = token_store.put(
        tenant=request.tenant,
        workflow=request.workflow,
        purpose=request.purpose,
        values=token_map,
        expires_in=policy.mapping_ttl,
    )

    return sanitized, mapping_id, audit_summary(accepted)

Detection hides behind entity_scan so you can test the policy and token lifecycle and swap libraries later.

What redaction costs in accuracy

Redaction is not free for the task. A study of five language models on eleven benchmarks, On the Impact of Anonymization on the Performance of Large Language Models, found that anonymization generally lowers performance, with a spread that depends on the task and the model. Reversible methods that keep each entity distinct significantly outperformed irreversible redaction, and tasks centered on retrieving or matching entities fell sharply. Telling the model its input had been anonymized gave no clear benefit.

Three consequences for your gateway. Typed, distinct tokens such as [PERSON_1] and [PERSON_2] are worth their complexity compared with a single [REDACTED]. Workflows that look up or match entities (finding the right customer, joining two records) are the ones that will degrade, so measure them first and do the matching in application code before the model call. And no benchmark number replaces a before-and-after run on your own tasks.

Apply explicit decision rules

A detector score alone should not decide what leaves your system. Combine entity type, source field, purpose, and confidence.

Always block known credentials, payment data, health identifiers, and private keys when the workflow has no explicit approved need for them. Do this even when the detector score is low if a deterministic format or source-field label confirms the type.

Tokenize names, email addresses, phone numbers, and postal addresses when the model needs conversational structure but not identity. Preserve one token per distinct value so references remain coherent.

Allow operational identifiers only when the model needs them and the identifier cannot grant access by itself. Prefer partial or synthetic representations. If an order number is required for a drafted reply, application code can insert it after inference rather than exposing it to the model.

Send uncertain findings to a safe path. A synchronous workflow can remove the span and ask a person to review the output. A batch process quarantines the item with the detector type, score, and surrounding text. Never lower thresholds quietly until the queue empties.

Keep exceptions narrow and testable. An allow rule for example.com in a URL field beats disabling URL detection in every free-text field, and the report in Presidio issue 1498 makes a good regression case for code-heavy input.

Checksum detectors misfire on long IDs

Card-number detection usually combines a digit pattern with the Luhn check. The Luhn algorithm validates with a single check digit, so roughly one random digit string in ten passes, and many platform IDs are 13 to 19 digits long: a social media post ID, a payment-processor reference, a long database key. About one in ten of those IDs will pass the check, and any that also fit a card-length pattern get masked as card numbers.

Redaction also has to be applied at every destination, with a mode that fits it. In 2019 Meta disclosed, in Keeping passwords secure, that a January security review found some user passwords "stored in a readable format within our internal data storage systems", affecting hundreds of millions of Facebook Lite users, tens of millions of other Facebook users and, by a later update, millions of Instagram users. The login path was not the problem. A copy had gone to a different store with no protection. The same happens when a prompt is masked before the model call but the raw text still reaches traces, retries, queues and error reports. Choose the mode per destination. Logs, audit rows and issue reports want value scanning for card-like strings and sensitive keys. An approval screen that shows a person which record will change needs record IDs left intact, so key-based masking is enough there, and a value scan that masks an ID breaks the screen.

Example: sanitize a support ticket

Assume a ticket contains this text:

Maria Lopez cannot access order AB-10492. Reply to maria@example.com. Her note says the delivery address is 18 King Street.

The triage model needs the issue type and the fact that an order exists. It does not need the name, email, or street address. The privacy gateway could produce:

[PERSON_1] cannot access order [ORDER_1]. Reply to [EMAIL_1]. The note says the delivery address is [ADDRESS_1].

The classifier returns account_access with a routing label. No restoration is needed. A later response-drafting step can use the same sanitized text. After the draft passes output checks, application code inserts the customer name into a fixed greeting and the order number into a fixed template field. The model never receives the original values.

If the detector labels AB-10492 a government identifier, policy overrides it because the source schema marks it an internal order ID. If the detector misses the email, a deterministic rule still catches it. The layers cover different failures.

Scan outputs and control restoration

Scan the output too. The model can repeat a value that escaped detection, emit one from retrieved context, or reproduce a token where restoration is not allowed. Run the same entity scan on the response, plus exact matching against values in the protected token map.

Apply a policy per destination. A private operator screen may show a restored customer name, while a Slack channel, analytics event, or vendor ticket gets a stricter policy.

Restore from parsed output, not by string replacement over prose. Require a structured response with fields such as category, summary, and draft_reply, and allow [ORDER_1] to be restored only in draft_reply. Reject unknown tokens, tokens from another mapping ID, and expired ones.

Log counts and entity types, never raw findings or token maps. A privacy control that copies every sensitive span into observability storage opens another exposure path.

Test leakage and utility together

A release test set needs labeled sensitive spans and expected task outcomes. Privacy-only tests reward a detector that deletes the document, and task-only tests reward a workflow that sends everything. Measure both.

For leakage tests, include obvious formats, names that are also common words, multilingual addresses, identifiers with punctuation changes, copied email threads, OCR errors, and secrets in code. Assert that forbidden raw values appear nowhere in the sanitized payload, request logs, output, or traces.

For utility tests, assert that the ticket still routes correctly, relationships between tokenized people survive, permitted product names remain, and required fields come back. Track false positives by entity type and source field. A rising rate after a recognizer update blocks release even when leakage tests pass.

Add isolation tests for the mapping store: tenant A cannot restore tenant B's token, an expired mapping stays unusable, and a workflow allowed to restore a customer name is still denied a health identifier. These are deterministic authorization tests.

Before rollout, run the gateway in report-only mode on a bounded sample, compare findings with human labels, add narrow exceptions, and freeze the policy version. Then enforce on one workflow and watch rejections, quarantined items, false positives, and downstream task failures.

Review failures that should block release

Do not call reversible tokenization anonymisation. A retained mapping changes the privacy claim, so use the terms in the ICO anonymisation guidance.

Do not keep raw and sanitized payloads side by side in logs. Log the policy version, finding types, counts, decisions, and mapping identifier instead.

Do not use one global confidence threshold. A low-confidence private-key match may deserve blocking, while a medium-confidence person name in a product catalog may need an exception.

Do not restore tokens inside the prompt. Restoration belongs in application code after output validation, with field-level authorization.

Do not judge the gateway by recall alone. A filter that breaks the business task gets bypassed under pressure, so leakage and utility are both release gates.

Next action

Pick one model-backed workflow and inventory every field it sends, marking each drop, allow, tokenize, or block. Build ten labeled examples from real input shapes, at least three of them known false positives. Run the gateway in report-only mode against those labels, and enforce only after the workflow passes both the no-leakage assertions and its task-quality checks.

References


About Fire In Belly: Independent senior engineering from Tallinn, Estonia. We design and build AI workflow automation with the privacy gateways, typed tokenization, and leakage-and-utility testing described above, at published fixed prices. Schedule a call to discuss your next project.