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 ID number (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 Turkish ID 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 on macOS, pdftotext elsewhere), honouring the pages argument; the raw PDF is never read by the model. A PDF with no extractable text, or one that cannot be converted, 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 OCR (Apple Vision on macOS, tesseract elsewhere). An image that cannot be inspected is withheld | uploaded uninspected, a toast says so |
| Image pasted into the prompt | the prompt is blocked when OCR finds personal data in it; a redacted copy is written beside the original and named in the message, attach that instead | not inspected |

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 uses macOS frameworks when the Swift toolchain is present (xcode-select --install); the helper is compiled at session start. Elsewhere, or on a Mac without the toolchain, PDFs use pdftotext from poppler and images use tesseract with ImageMagick when they are on PATH. When no converter is available the Read is blocked with a message naming what to install; the raw file never reaches the model. 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.
Configuration
Everything is on by default. One JSON object turns detector types off, adds your own patterns, or lists values that must never be masked. The same object works in Claude Code and in the Chrome extension.
{
"disable": ["TCKN", "PLATE"],
"extra": [{ "type": "EMPLOYEE_ID", "pattern": "EMP-\\d{6}" }],
"allow": ["support@acme.com"]
}
| Key | Meaning |
|---|---|
disable | Types to switch off. One of EMAIL, IBAN, CARD, TCKN, SSN, PHONE, ADDRESS, DOB, ID, PLATE, IP, NAME. Country-specific detectors such as the Turkish ID number or licence plate are the usual candidates. |
extra | Your own detectors: a type name that becomes the placeholder label and a JavaScript regular expression in pattern, with optional flags. Matches are masked like everything else and restored the same way. |
allow | Exact values that are never masked, such as a shared support address that is not personal. |
Claude Code reads, in this order, the file named in MASK2AI_CONFIG, then .mask2ai.json in the working directory, then ~/.mask2ai/config.json. The first one found wins, and the session start line names it. A file that does not parse is ignored and the line says so.
Chrome: right-click the mask2ai icon, choose Options, paste the JSON and save. It is kept in Chrome storage and applies after the tab reloads.
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.