Docs
One detection core, two integrations, three ways to verify it.
What is detected
| Type | How it is found | Validation |
|---|---|---|
| Email address | pattern | |
| Phone number | international +.., US, UK and Turkish formats | |
| Payment card | 13 to 16 digit shapes, spaced or plain | Luhn |
| IBAN | country code plus grouped alphanumerics | mod 97 |
| Turkish TC kimlik no | 11 digits | official checksum |
| US Social Security number | ddd-dd-dddd | |
| Date of birth | after a label such as DOB, date of birth, doğum tarihi | |
| Passport or ID number | after a label such as passport no, kimlik no, ehliyet | |
| Turkish licence plate | 34 ABC 123 | |
| Public IPv4 address | excludes private, loopback and link-local ranges and version strings | |
| Person name | after a title (Dr., Sayın), a cue (my name is, Regards,, Benim adım), a label (name:, "firstName":), or derived from a masked email | shape check |
| Street address | after a label (address:, adres:), US and UK street shapes, Turkish Mah. / Cad. / Sok. shapes with No: | shape check |
Detection is an ordered list of patterns in core/pii.js. Emails run first so their digits are not later read as phones; cards, IBANs and TC numbers run before phones for the same reason. Label, title and cue patterns capture only the value, and a shape check rejects values such as name: mask2ai or address: 0x7fff. After the static pass, the local part of every masked email is split into tokens, and each token is masked where it appears capitalised or in capitals, accent-insensitively, so jane.doe@ also hides Jane and DOE in a CSV column.
Files
| File | Claude Code | claude.ai and ChatGPT in Chrome |
|---|---|---|
| Text: .txt, .md, .csv, .json, .xml, .html, .yaml, .log, source code | masked in tool output | masked when uploaded |
| Office: .docx, .xlsx, .pptx | read through scripts, whose text output is masked | masked in place: the XML text inside the zip is rewritten, formatting and images untouched |
| the Read is redirected to a masked text extraction (PDFKit); the raw PDF is never read by the model. A PDF with no extractable text is withheld | uploaded uninspected, a toast says so | |
| Image: .png, .jpg, .gif, .webp | the Read is redirected to a copy with matching words blacked out, found by Apple Vision OCR | uploaded uninspected, a toast says so |

One prompt reads the sample PDF and the sample image in the real Claude Code terminal. The values in the reply are placeholders restored on screen.

The image on the right is the file the Read tool actually received. The original on disk is untouched.
PDF and image handling in Claude Code needs macOS with the Swift toolchain (xcode-select --install). The helper is compiled once on first use. On other systems PDFs and images pass through with a warning. Image redaction relies on OCR: text it cannot read, handwriting, or personal data that is not text, such as a face, is not redacted. Upload masking is verified on claude.ai; the ChatGPT two-step upload flow has not been verified.
Placeholders
A value becomes __PII_<TYPE>_<6 hex digits of a hash of the value>__. The same value yields the same placeholder in a prompt, a file read and a grep result without a lookup, hooks running in parallel cannot disagree, and after a resume a single re-read rebuilds the map. Underscores keep the token a single word for the model and harmless inside code.
Claude Code plugin
Claude Code runs hooks/mask.js once per event as a child process, with a JSON payload on stdin. The script prints a JSON decision on stdout, or nothing to leave the event untouched. hooks/hooks.json registers the same command for six events.
| Event | What mask2ai does |
|---|---|
SessionStart | Shows the status line and tells the model that __PII_*__ tokens are opaque literals to copy verbatim |
UserPromptSubmit | On a hit, blocks the prompt with decision: "block", shows the masked text and copies it to the clipboard on macOS. Hooks cannot rewrite a prompt, so you resend the masked copy |
PostToolUse | Masks every string in tool_response and returns the same structure as updatedToolOutput, so the model only sees placeholders |
PreToolUse | Restores placeholders in tool_input via updatedInput, so edits match the real file and commands run with real arguments |
MessageDisplay | Restores placeholders in the streamed reply on screen only; the transcript keeps them |
SessionEnd | Deletes the session's placeholder map |
The map is an append-only JSON Lines file under $CLAUDE_PLUGIN_DATA/<session_id>.jsonl, created with mode 0600. Small appends are atomic on POSIX, so parallel tool calls never lose an entry.
Chrome extension
The extension injects core/pii.js, extension/rewrite.js and extension/content.js into claude.ai, chatgpt.com and chat.openai.com at document_start in the page's main world. It wraps window.fetch. For requests to the chat endpoints it decodes the body, whether a JSON string, a form body, a byte array or a gzip-compressed byte array, masks the text fields, re-encodes it in the original form and forwards it. A MutationObserver restores placeholders in rendered text, skipping editable fields. The map lives in sessionStorage and is gone when the tab closes.
It has been verified against the current claude.ai and chatgpt.com request formats. A change in either site's client may require an update; the headless verification below catches that.
Verify
npm test # detection, restoration, request rewriting; runs in CI
node demo/prove.js # fake Anthropic API on localhost, real claude binary, every request inspected
node demo/verify-web.js # headless Chrome loads the extension on claude.ai and chatgpt.com
The proof drives two sessions: a prompt with an email must produce zero requests, and a file read must arrive as placeholders only. The web check sends a chat request from the page on each real site, including a gzip-compressed body as claude.ai uses, and asserts that only placeholders leave and that the page restores them. If you prefer your own instrument, point ANTHROPIC_BASE_URL at a logging proxy such as mitmproxy and read the traffic yourself.
Limits
Detection is pattern based, not a language model. A name in free text with no label, title, cue or matching email nearby is not detected, and labels such as name: can catch values that are not personal. Semantic personal data, for example health conditions, religion, ethnicity or income stated in prose, is not detected. In the browser, PDF and image uploads are not inspected. Image redaction in Claude Code depends on OCR. Claude Code hooks cannot rewrite a prompt, only block it, so a prompt containing personal data has to be resent in masked form.
What still leaves the machine
Placeholders, everything the patterns do not recognise, file paths, and your prompt once you resend it in masked form. See Privacy.