> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corepillar.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# How privacy screening works

> The detection layers behind ModelHub's privacy screening, every entity type and its severity, how placeholders are built and restored, exactly what is stored, and what has and has not been measured.

Before any message reaches a model provider, ModelHub finds personal and secret data in it and replaces each item with a placeholder. This page describes the mechanism in full. For the user view, see [Privacy screening](/modelhub/chat/privacy-screening).

## Where screening sits in a message's journey

```mermaid theme={null}
flowchart TD
  A[Draft in the composer] -->|every pause in typing, 350 ms| B[Live scan: pattern layers only]
  C[Attached file] -->|on upload| D[Extract text and scan]
  A -->|Send| E[Optimise, if on]
  E --> F[Join message and documents into one text]
  D --> F
  F --> G[Pattern layers 1 to 3]
  G --> H[Masked text]
  H --> I[Layer 4: small model reads the masked text for names and context]
  I --> J[Union of all findings, one rewrite]
  J --> K{Critical finding and not confirmed?}
  K -- yes --> L[Hold the message once]
  K -- no --> M[Send masked text to the model]
  M --> N[Restore non-critical values on your screen as the answer streams]
  M --> O[Store masked text and finding counts]
```

The message and all attached documents are masked **as one piece of text**, so one value gets one placeholder wherever it appears, and a critical value inside a document holds the message exactly as one typed in the composer would.

Earlier turns of the conversation are sent to the model in their masked form, and re-screened by the pattern layers on every send as a safety net. The real values masked on turn one are never sent on turn two.

## The four layers

No single technique works on its own. Structured identifiers follow rules a program can verify; names and addresses do not.

### Layer 1: a vetted subset of an open-source library

The first layer uses [`@openredaction/core`](https://www.npmjs.com/package/@openredaction/core) (MIT licensed), restricted to **54 named patterns** that are structurally verifiable. Its own name, address and date detection is switched off. When tested with its defaults, it flagged "My card is" and "Ship to" as people's names, read "Mountain View CA" as a person, consumed part of an IP address as a date, and let a live-format API key through.

The 54 patterns:

| Group | Patterns |
| - | - |
| Contact | Email; US, UK, UK mobile and international phone numbers; date of birth |
| Government identifiers | US SSN; US ITIN; UK National Insurance; Canadian SIN (two variants); NHS number; CHI number; US passport; UK passport; machine-readable passport lines (TD1 and TD3); Indian Aadhaar; Australian TFN; Japanese My Number; South Korean RRN; UAE Emirates ID; Saudi National ID; Israeli ID; Turkish ID; South African ID; Nigerian NIN; Chilean RUT; Czech national ID; Romanian CNP; tax ID; UK UTR |
| Payment | Credit card; CVV (two variants); card track 1 and track 2 data; IBAN; UK bank account IBAN; UK sort code and account |
| Credentials | AWS access key; AWS secret key; Google API key; Stripe API key; GitHub token; Slack token; OpenAI API key; Azure storage key; GCP service account; OAuth client secret; private key; SSH private key; database connection string; URL with embedded credentials |

### Layer 2: CorePillar's own patterns

These cover formats the library misses or gets wrong. They match against the original text with Unicode-aware character classes (dashes pasted from Word and Slack, non-breaking and zero-width spaces, full-width digits), so offsets never drift.

| Detector | What it finds |
| - | - |
| Credentials | Anthropic keys (`sk-ant-…`), OpenAI keys including project, service-account and admin keys, `Bearer` tokens, CorePillar tracing keys (`tk_live_…`, `tk_test_…`), JWTs, AWS access key IDs, Google API keys, GitHub tokens (classic and fine-grained), Slack tokens, Stripe keys, GitLab, Hugging Face, npm and SendGrid tokens, PEM private key blocks, URLs with a username and password |
| Secret assignments | The value in `password: …`, `api_key = …`, `client_secret: …`, `token is …` and similar. With "is", the value must contain a digit or symbol or be 12+ characters. |
| US SSN | `123-45-6789` with any dash, dot, slash or space separator. Without an SSN keyword nearby, the number must pass the allocation rules (area not `000`, group not `00`, serial not `0000`). Nine digits in any grouping are caught when "SSN", "social security", "ITIN", "taxpayer id", "TIN" or similar appears near them. "Last four" phrasings and partly masked forms such as `xxx-xx-6789` are caught too. |
| Email | Standard addresses (including Unicode and quoted local parts, spaces around `@`, full-width `＠`) and obfuscated ones such as `john [at] acme [dot] com` or `jdoe at acme.com`. "look at google.com" is not treated as an address. |
| Phone | North American numbers, international numbers starting `+` or `00`, UK mobiles starting `07`, and any 7 to 15 digit number after a word like "phone", "mobile", "call", "WhatsApp" or "fax" |
| Payment | 13 to 19 digit card numbers that pass the Luhn checksum, or that sit near a word like "card", "visa", "amex" or "debit"; CVV/CVC/security codes; card expiry dates after "exp", "expiry" or "valid thru" |
| Bank | IBANs that pass the mod-97 checksum; account, routing, ABA and sort-code numbers after those keywords |
| IP address | Public IPv4 and IPv6 addresses. Private, loopback, link-local, carrier-grade NAT and multicast ranges are skipped, as are dotted numbers after "version", "release", "upgraded to" and similar. |
| Date of birth | A date after "DOB", "date of birth", "birthday", "born" and similar |
| Address | Street addresses ending in a street word (Street, Ave, Rd, Blvd, Lane, Way and others) with optional unit and city/state/ZIP, PO boxes, UK postcodes, US state code plus ZIP, and postcodes after "zip" or "postcode" |
| Documents | Passport, driving licence and national ID numbers (Aadhaar, PAN, NINO, SIN, NRIC, CPR, BSN, personnummer, CURP, RFC, DNI, NIE, TFN, EIN, employee ID and others) following their keyword |

### Layer 3: deterministic name detection

Names are found without any AI call by combining:

* **Name lists** of given names and surnames spanning English, Spanish and Portuguese, South Asian, East and Southeast Asian, Arabic, Persian, Turkish, African and European naming, matched with accents stripped so "José", "JOSE" and "jose" all match.
* **Shape rules** for runs of capitalised words, initials ("J. Smith"), particles ("María de la Cruz", "al-Rashid") and lower-case names next to a listed surname.
* **Cues** that say a name follows: "my name is", "Dear", "Sincerely,", "cc:", "To:", "From:", "Attendees:", honorifics such as "Dr.", "Mrs", "Sr.", "Shri", relationship words ("my manager", "my wife"), verbs ("ask", "email", "met with"), and sign-off lines such as "— Priya".
* **Common-word filtering** so that products, languages, months and ordinary verbs ("Apple", "Ruby", "May", "Will you merge it", "Mark the ticket", "Ask Claude") are not masked.

### Layer 4: a small model reading for meaning

Some identifiers only meaning can reveal. A small model reads the message and returns the exact substrings it believes are identifiers.

| Detail | Value |
| - | - |
| Model | Claude Haiku 4.5, on CorePillar's platform key |
| Input | The message **after** layers 1 to 3 have masked it, so the model never sees what the patterns already caught. Up to 8,000 characters. Messages under 12 characters are skipped. |
| Types | `PERSON_NAME`, `STREET_ADDRESS`, `ORGANISATION` (a private company named as a customer, employer or counterparty), `JOB_TITLE_WITH_NAME`, `CUSTOMER_REFERENCE`, `HEALTH_CONDITION` (tied to a named person) |
| Matching | The model must quote each item exactly. Each quote is located in the original text by exact search, and **every** occurrence is masked. A quote that does not appear verbatim is discarded rather than fuzzy-matched, so a wrong answer fails safe instead of masking the wrong text. |
| Cap | 40 items per message |
| Confidence | Recorded as a fixed 0.6. A small model's self-assessed confidence is not treated as a measurement. |
| Time limit | 6 seconds. It runs alongside the routing second opinion. |
| Failure | Returns nothing. The pattern layers' result stands and the message is sent. |

The composer's live scan while you type uses layers 1 to 3 only, so it is fast and free. Layer 4 runs once, at send time.

## Combining findings

All findings from all layers are **unioned** before the text is rewritten. Where two findings overlap, they merge into one span covering both, which takes the most severe type of the two. (Keeping only one of two overlapping findings would leak whatever tail the dropped one covered.) Each finding records how it was found: `pattern`, `validated-pattern` (a checksum or rule confirmed it, such as Luhn for cards, mod-97 for IBANs, SSN allocation rules) or `model`.

Text that is already a placeholder is never re-detected, so re-screening masked history is harmless.

## Severity

| Severity | Types |
| - | - |
| **Critical** | Every credential type above (Anthropic, OpenAI, Google, Stripe, GitHub, Slack, AWS, Azure, GCP, OAuth secrets, private and SSH keys, database connection strings, URLs with credentials, bearer tokens, JWTs, tracing keys, other API keys, secret assignments), credit card numbers, card track data, CVV, IBAN, US SSN, US ITIN, passports |
| **High** | Person names, email addresses, phone numbers, street addresses, dates of birth, bank account numbers, driving licences, health conditions |
| **Medium** | Public IP addresses, card expiry dates, customer references, job titles tied to a name, and identifier types without an explicit severity (most national ID formats, NHS and CHI numbers, UK sort code and account, machine-readable passport lines) |
| **Low** | Named organisations |

### What critical means

A critical finding does two things:

1. **The message is held once.** The person sees what was found and confirms with **Send masked**. The model receives the masked version either way. The pause makes pasting a production key a deliberate act.
2. **The value never comes back to the screen.** Non-critical values are restored on the sender's screen and shown on hover. Critical values stay as placeholders in the sender's own bubble, in the answer and in every later turn.

If any pattern layer crashes, the message is also held. Reporting "nothing found" after a failed scan would be false.

## Placeholders

Each placeholder is `[` + type stem + `_` + four digits + `]`, for example `[EMAIL_4821]`, `[PERSON_NAME_0042]`, `[ANTHROPIC_KEY_2228]`. Stems are the type name, except `ANTHROPIC_KEY`, `DOB`, `IP` and `ADDRESS`.

The four digits are a hash of the type and the normalised value, so the same value gets the same placeholder on every turn and the model can tell two people apart. If two different values hash to the same number in one message, the second moves to the next free number.

The model is instructed that placeholders stand for real details removed for privacy, to treat each as a consistent identifier, to reuse it verbatim, and never to guess what it stands for.

### Restoring values on screen

The server holds no conversation state. After each answer, the browser receives the map of placeholders to values for that turn, minus critical values, keeps it for the conversation, and sends it back with the next message. As the answer streams, placeholders are swapped for the real values. A placeholder split across two chunks (`[PERSON_` then `NAME_0042]`) is held back until it is complete. The map is capped at 400 entries; past that the oldest placeholders stop resolving and show as placeholders. The map is never sent to a model and never stored.

## What is stored

Chat turns are stored in the organization's audit log, one record per turn.

| Field | Content |
| - | - |
| Message | The masked text, including the masked text of attached documents |
| Answer | The model's reply as received, with placeholders |
| Title | Generated from the masked first message |
| Usage, routing, fit, optimisation | Token counts, cost, latency, the routing decision (without per-signal detail), the verdict |
| Privacy counts | Total, count per severity, count per type, and whether the turn was held |
| Removed values | For administrators: non-critical values verbatim (names, emails, addresses, phone numbers, customer references), so an investigator knows which customer was exposed. Critical values only as a hint. |

How critical values are reduced:

| Kind | Stored as |
| - | - |
| Numeric identifiers (SSN, ITIN, card, IBAN, bank account, passport, national ID, driving licence, CVV, SIN, National Insurance, Aadhaar, tax ID) | Last four characters only, such as `…6789`, and only when the value is longer than six characters; otherwise `…` |
| Keys and other secrets longer than 16 characters | First six and last four, such as `sk-ant…mnop`. Enough to find and revoke the right key, not enough to use it. |
| Shorter secrets | `…` |

Writing live credentials into the log in the clear would make the record of a leak a second copy of it, read by more people than the original chat.

**Who sees what:**

* The employee's own [AI Usage](/modelhub/usage) page shows counts by type and severity, never values.
* The organization-wide view on the AI Usage page (Owners, Admins and super admins, on either hub) shows an itemised log with the stored values. See [AI Usage](/agenthub/ai-usage).
* When end-to-end encryption is on, the message, answer, title and stored values of each new turn are encrypted to the author and the organization's super admins. The log then shows "🔒 Encrypted" for those values. If encryption fails at write time, only the metrics are stored and the text is dropped.

Uploaded files themselves are never stored. They are read in memory during upload and discarded; only their extracted text travels with the message.

## Accuracy: what has been measured

Read this section literally.

**What is tested.** A regression suite runs the pattern layers (1 to 3) and document extraction against hand-written cases. At the time of writing it contains:

| Group | Cases |
| - | - |
| US SSN formats | 37 |
| Email formats | 30 |
| Names across cultures | 63 |
| Other identifiers | 46 |
| Documents (CSV, TSV, UTF-16, DOCX, PDF) | 6 |
| Generated combinations (names, emails, SSNs with varied separators) | 600 |
| **Must not be touched** (private IPs, version numbers, plain prose, a 16-digit order number, code, invalid SSNs, product and company names, cities, dates, money, UUIDs, existing placeholders and more) | 34 |

All of them pass. The suite also checks that a 50,000-character CSV is fully masked quickly, that a pathological 32,000-character input does not hang the scan, and that unreadable files are rejected.

The "must not be touched" cases matter as much as the detections. Over-masking is what makes people stop using the tool.

**What this is not.** These are cases CorePillar wrote. They prove the detectors still do what they were built to do. They are **not** a benchmark and do not establish a recall rate on real-world data. The detectors have not been run against an independent labelled PII corpus, so **no accuracy percentage should be quoted** for them. Layer 4 (the model) is not covered by the suite.

**Known limits:**

* **Names and addresses have no guaranteed recall.** Lists, cues and a small model all miss some.
* **Structured identifiers are high-confidence but not exhaustive.** A format with no pattern is not detected.
* **Free-form confidential text is not detected.** Unreleased financials, strategy or deal names are not personal identifiers. This is not a general confidentiality filter.
* **Layer 4 can fail open.** If it times out or the platform key is unavailable, the message is sent with pattern-layer masking only.
* **Layer 4 reads the first 8,000 characters** of the combined message and documents. Pattern layers read everything.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.