Docs

One detection core, two integrations, three ways to verify it.

What is detected

TypeHow it is foundValidation
Email addresspattern
Phone numberinternational +.., US, UK and Turkish formats
Payment card13 to 16 digit shapes, spaced or plainLuhn
IBANcountry code plus grouped alphanumericsmod 97
Turkish TC kimlik no11 digitsofficial checksum
US Social Security numberddd-dd-dddd
Date of birthafter a label such as DOB, date of birth, doğum tarihi
Passport or ID numberafter a label such as passport no, kimlik no, ehliyet
Turkish licence plate34 ABC 123
Public IPv4 addressexcludes private, loopback and link-local ranges and version strings
Person nameafter a title (Dr., Sayın), a cue (my name is, Regards,, Benim adım), a label (name:, "firstName":), or derived from a masked emailshape check
Street addressafter 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

FileClaude Codeclaude.ai and ChatGPT in Chrome
Text: .txt, .md, .csv, .json, .xml, .html, .yaml, .log, source codemasked in tool outputmasked when uploaded
Office: .docx, .xlsx, .pptxread through scripts, whose text output is maskedmasked in place: the XML text inside the zip is rewritten, formatting and images untouched
PDFthe 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 withhelduploaded uninspected, a toast says so
Image: .png, .jpg, .gif, .webpthe Read is redirected to a copy with matching words blacked out, found by Apple Vision OCRuploaded uninspected, a toast says so
mask2ai in Claude Code: a PDF read is converted to masked text and an image read is redacted, and the reply shows what the model was given

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.

demo/customer.png before and after mask2ai: the name, email, phone and SSN are blacked out in the copy Claude Code reads

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.

EventWhat mask2ai does
SessionStartShows the status line and tells the model that __PII_*__ tokens are opaque literals to copy verbatim
UserPromptSubmitOn 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
PostToolUseMasks every string in tool_response and returns the same structure as updatedToolOutput, so the model only sees placeholders
PreToolUseRestores placeholders in tool_input via updatedInput, so edits match the real file and commands run with real arguments
MessageDisplayRestores placeholders in the streamed reply on screen only; the transcript keeps them
SessionEndDeletes 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.