# Document pseudonymization for AI assistants: sending the spreadsheet without sending the customers

> Source: <https://dev.to/genevieve_breton_cb795f52/document-pseudonymization-for-ai-assistants-sending-the-spreadsheet-without-sending-the-customers-1lf9>
> Published: 2026-07-29 10:57:39+00:00

*Your code is not the only thing leaking into AI prompts. The Excel file with your customer list, the Word contract, the HR email thread — those go too. Here's how to let the AI work on them without a single real name leaving the machine.*

The pattern shows up in every company that adopts AI assistants, usually within weeks:

"Can you summarize this contract?"

"Clean up this workbook and add a total per region."

"Draft a reply to this email thread."

Every one of those documents is dense with exactly the data a company must not hand to a third party: customer names, email addresses, phone numbers, IBANs, salaries, negotiated prices, internal product codenames. A single invoice workbook can identify dozens of natural persons and reveal your entire pricing structure at the same time.

Under the GDPR, sending that document to a remote AI API is **processing of personal data by a third party**, often with a transfer outside the EU. Under any sane reading of trade-secret protection, it is also handing your commercial terms to infrastructure you don't control. The compliance answer cannot be "don't use AI" — the productivity gap is too large, and people will paste the document into a chat window anyway. Shadow usage is worse than governed usage.

So the real requirement, as stated by the people who have to sign off on this, is:

Requirement 1 is what kills naive redaction. Replace every name with `███████`

and the AI can no longer tell two parties of a contract apart; replace every amount with `[REDACTED]`

and "add a total per region" is meaningless. The document has to stay *plausible*.

That is pseudonymization: each value is replaced by a **same-format surrogate** through a **bijective mapping kept locally**. `Jean Dupont`

becomes another plausible person name, an internal email becomes another valid email address, an IBAN becomes another correctly-structured IBAN, a date becomes a date shifted by a constant offset. Same original → same surrogate everywhere, so cross-references survive. The mapping file never leaves the machine, and restoration is exact reverse substitution — longest match first, so `Jean-Pierre Dupont`

doesn't get half-restored by the `Jean Dupont`

entry.

Detection runs in three layers, because each one fails differently:

**Validated patterns** catch the values with verifiable structure: email addresses, phone numbers, IBANs, card numbers, dates. One gotcha worth sharing: off-the-shelf recognizers often validate email domains against the public suffix list — which makes them *reject* addresses on internal domains (`user@company.local`

, `admin@host.internal`

). Those are precisely the addresses you most need to pseudonymize, because they reveal your infrastructure. The fix was a permissive recognizer that keeps the local-part/domain shape requirement (so a `@decorator`

in a code snippet is not matched) but drops public-suffix validation.

**NER** (named-entity recognition, via a local Presidio + spaCy sidecar — nothing goes to any cloud for detection either) catches the values with no structure at all: person names, organizations, locations. NER over spreadsheet fragments is noisy — a single-word column header like "Téléphone" gets tagged as an entity — so an exclusion list is a first-class feature, not an afterthought.

**A dictionary sweep** runs after detection: every mapped value is re-searched across *all* text fragments on word boundaries, catching occurrences the detector missed (the organization name inside the workbook's title property, the person mentioned mid-sentence in a cell comment). Accepted entities are expanded across case variants: detect `Acme Industries`

once and `ACME INDUSTRIES`

in a header is covered too, with a case-shaped surrogate, each variant restored exactly.

Numbers get their own rule system, because "hide the amounts" means different things per column: a multiplicative `scale`

preserves order and ratios (the AI can still find the biggest customer), `offset`

shifts, `keep`

leaves quantities and years alone. Formulas are re-evaluated from the transformed inputs — a `=SUM()`

over scaled cells shows the scaled total, so the workbook stays internally consistent and the AI never sees a sum that doesn't match its column.

The detection layer is maybe a third of the effort. The rest is format plumbing, and it is full of traps where the original value survives in a corner of the file you didn't rewrite. A few that cost real debugging time:

| Format | Trap |
|---|---|
xlsx |
Rewriting a cell leaves the original string orphaned in , readable by unzipping the file. Comment authors: the API call appends the new author and keeps the old one in the comments table. Hyperlink targets live in a separate `sharedStrings.xml` `.rels` part. |
docx / pptx |
Visible text is split across formatting runs — a bold surname interrupting a name breaks naive per-run matching. In pptx, a brand in a footer lives in the slide master, not the slide. |
ODF (odt/ods/odp) |
The zip must keep the `mimetype` entry first and uncompressed, or applications refuse the file. Easy to break on write, invisible until a user opens the output. |
PDF |
Input-only: text is extracted and pseudonymized as text (no layout rebuild). Scanned pages need OCR — and since OCR misreads, dictionary terms are fuzzy-matched (edit distance ≤ 2) on OCR'd content to catch a codename read with one wrong letter. |
RTF |
Surrogates must be re-encoded through RTF escaping (non-ASCII as unicode escapes), and the metadata `\info` group holds author names. |
HTML / XML / EPUB |
Text nodes only; `script` and `style` are code, not prose, and are left alone. |
email (.eml/.msg) |
Addressing headers and body must map consistently — the sender named in the signature is the same entity as the `From:` header. |

The lesson that generalizes: **coverage must be locked by a test, not by review**. The test plants a forced term in every rewritable channel of a workbook, pseudonymizes, then greps the raw output archive: the term must not survive anywhere. Anything that *cannot* be rewritten — defined names referenced by formulas, pivot caches, chart series, embedded images — is not silently passed through: it produces an explicit warning telling the user to check before submitting, and raster images are dropped by default (pixel-rendered text in a screenshot cannot be pseudonymized; an OCR-gated mode keeps only images whose text is clean and readable).

Fail-closed is the other non-negotiable: a document that cannot be pseudonymized is **withheld and replaced by an explicit placeholder**. The failure mode of a privacy tool must never be "sent in clear."

`suggest`

Patterns and NER don't know your company. `Project Nightingale`

, the internal product codename, is not in any model's training as a sensitive term. And you can't ask every project manager to maintain regexes.

Two configuration levels solve this:

`[terms]`

dictionary and `[numeric]`

rules. Transport is https-only, packs are cached (an unreachable server falls back to the cached copy; no cache and no server → the run fails rather than running without the company policy), and each pack is `report-terms.txt`

, `report-numeric.txt`

) complement the pack.The cold-start problem — "what should even be in the dictionary?" — is handled by a `suggest`

command: feed it a few representative documents of a category, and it emits a candidate terms file (NER results with occurrence counts, all-caps/acronym candidates in a commented "uncertain" section, where internal codes usually hide) and a candidate numeric-rules file (one `scale`

line per numeric column, annotated with the header). A human prunes the false positives, flips `scale`

to `keep`

for quantities and years, renames the file — done. Ten minutes of review instead of a blank page.

**CLI**, for the one-shot case: `pseudonymize report.docx`

writes `report-pseudonymized.docx`

plus a local mapping file; the AI's text answer pipes through `depseudonymize`

, and a modified document restores the same way.

**An HTTP proxy**, for the transparent case: the AI client's API traffic passes through a local proxy that pseudonymizes *in both directions*. Attachments are transformed before leaving; prompt text naming a value from an attached document reaches the AI under the same surrogate the document uses (otherwise your own question deanonymizes the attachment); tool reads of document files are swept; and the AI's streamed response is restored on the fly. The user types real names and reads real names — the provider only ever stores surrogates.

**An MCP server**, for clients whose network path can't be proxied (Claude Desktop): one tool reads a local document and returns its pseudonymized content into the conversation; a second restores an answer and writes it to a local file *without returning the restored text to the conversation* — because anything returned to the conversation is sent back to the provider on the next turn.

Precision matters here, because "GDPR-compliant AI" is a marketing phrase and this is an engineering article.

What this technique implements is **pseudonymization in the GDPR sense** (art. 4(5)): personal data that can no longer be attributed to a person *without additional information* — the mapping — which is kept separately, locally, under your control. That supports data-minimization arguments (art. 5(1)(c)), data-protection-by-design (art. 25) and security-of-processing measures (art. 32): what the provider receives identifies no one without a file that never left your machine.

It is **not anonymization**. Context can still re-identify: if a document says "the CEO of the company headquartered in [small town]," no surrogate on the name prevents inference. Detection is best-effort by nature — that's exactly why the residual channels produce warnings instead of silence, and why documents that fail are withheld instead of forwarded. Pseudonymized data remains personal data under the GDPR; the transformation reduces risk and supports compliance, it does not exempt you from it. Any tool claiming otherwise is overclaiming.

`.rels`

parts, RTF escapes — original values hide in places no visual review catches. Lock coverage with a plant-and-grep test over the raw archive, and warn loudly about every channel you can't rewrite.`suggest`

bootstrap turn "maintain a dictionary" from a chore nobody does into a ten-minute review.The document pseudonymization described here ships in PromptCape alongside its code-obfuscation pipeline — CLI, proxy and MCP server share one engine and one session mapping. Worked examples and sample documents are in the companion repo at [gitlab.com/gbreton7/promptcape-docs](https://gitlab.com/gbreton7/promptcape-docs), and the tool is free to try for 3 months at [promptcape.com](https://promptcape.com/).
