Document pseudonymization for AI assistants: sending the spreadsheet without sending the customers A developer details a method for pseudonymizing documents before sending them to AI assistants, ensuring compliance with GDPR and trade-secret protection. The approach uses same-format surrogates via a local bijective mapping, with three detection layers: validated patterns, NER, and dictionary sweep. 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/ .