{"slug": "document-pseudonymization-for-ai-assistants-sending-the-spreadsheet-without-the", "title": "Document pseudonymization for AI assistants: sending the spreadsheet without sending the customers", "summary": "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.", "body_md": "*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.*\n\nThe pattern shows up in every company that adopts AI assistants, usually within weeks:\n\n\"Can you summarize this contract?\"\n\n\"Clean up this workbook and add a total per region.\"\n\n\"Draft a reply to this email thread.\"\n\nEvery 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.\n\nUnder 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.\n\nSo the real requirement, as stated by the people who have to sign off on this, is:\n\nRequirement 1 is what kills naive redaction. Replace every name with `███████`\n\nand the AI can no longer tell two parties of a contract apart; replace every amount with `[REDACTED]`\n\nand \"add a total per region\" is meaningless. The document has to stay *plausible*.\n\nThat is pseudonymization: each value is replaced by a **same-format surrogate** through a **bijective mapping kept locally**. `Jean Dupont`\n\nbecomes 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`\n\ndoesn't get half-restored by the `Jean Dupont`\n\nentry.\n\nDetection runs in three layers, because each one fails differently:\n\n**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`\n\n, `admin@host.internal`\n\n). 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`\n\nin a code snippet is not matched) but drops public-suffix validation.\n\n**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.\n\n**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`\n\nonce and `ACME INDUSTRIES`\n\nin a header is covered too, with a case-shaped surrogate, each variant restored exactly.\n\nNumbers get their own rule system, because \"hide the amounts\" means different things per column: a multiplicative `scale`\n\npreserves order and ratios (the AI can still find the biggest customer), `offset`\n\nshifts, `keep`\n\nleaves quantities and years alone. Formulas are re-evaluated from the transformed inputs — a `=SUM()`\n\nover 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.\n\nThe 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:\n\n| Format | Trap |\n|---|---|\nxlsx |\nRewriting 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. |\ndocx / pptx |\nVisible 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. |\nODF (odt/ods/odp) |\nThe 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. |\nPDF |\nInput-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. |\nRTF |\nSurrogates must be re-encoded through RTF escaping (non-ASCII as unicode escapes), and the metadata `\\info` group holds author names. |\nHTML / XML / EPUB |\nText nodes only; `script` and `style` are code, not prose, and are left alone. |\nemail (.eml/.msg) |\nAddressing headers and body must map consistently — the sender named in the signature is the same entity as the `From:` header. |\n\nThe 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).\n\nFail-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.\"\n\n`suggest`\n\nPatterns and NER don't know your company. `Project Nightingale`\n\n, 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.\n\nTwo configuration levels solve this:\n\n`[terms]`\n\ndictionary and `[numeric]`\n\nrules. 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`\n\n, `report-numeric.txt`\n\n) complement the pack.The cold-start problem — \"what should even be in the dictionary?\" — is handled by a `suggest`\n\ncommand: 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`\n\nline per numeric column, annotated with the header). A human prunes the false positives, flips `scale`\n\nto `keep`\n\nfor quantities and years, renames the file — done. Ten minutes of review instead of a blank page.\n\n**CLI**, for the one-shot case: `pseudonymize report.docx`\n\nwrites `report-pseudonymized.docx`\n\nplus a local mapping file; the AI's text answer pipes through `depseudonymize`\n\n, and a modified document restores the same way.\n\n**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.\n\n**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.\n\nPrecision matters here, because \"GDPR-compliant AI\" is a marketing phrase and this is an engineering article.\n\nWhat 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.\n\nIt 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.\n\n`.rels`\n\nparts, 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`\n\nbootstrap 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/).", "url": "https://wpnews.pro/news/document-pseudonymization-for-ai-assistants-sending-the-spreadsheet-without-the", "canonical_source": "https://dev.to/genevieve_breton_cb795f52/document-pseudonymization-for-ai-assistants-sending-the-spreadsheet-without-sending-the-customers-1lf9", "published_at": "2026-07-29 10:57:39+00:00", "updated_at": "2026-07-29 11:03:35.625138+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-safety", "ai-policy", "ai-tools", "developer-tools"], "entities": ["Presidio", "spaCy", "GDPR"], "alternates": {"html": "https://wpnews.pro/news/document-pseudonymization-for-ai-assistants-sending-the-spreadsheet-without-the", "markdown": "https://wpnews.pro/news/document-pseudonymization-for-ai-assistants-sending-the-spreadsheet-without-the.md", "text": "https://wpnews.pro/news/document-pseudonymization-for-ai-assistants-sending-the-spreadsheet-without-the.txt", "jsonld": "https://wpnews.pro/news/document-pseudonymization-for-ai-assistants-sending-the-spreadsheet-without-the.jsonld"}}