# PII detectors in detail

All nine embedded pii.* detectors — what each one covers and, at equal length, what it misses.

Source: https://tokentriage.com/docs/reference/guardrail-detectors-pii

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';

Nine embedded detectors, all `embedded` locality, all declaring **input, output and stream**.
Every one of them runs incrementally on the streaming lane. The
[family-wide bounds](/docs/reference/guardrail-detectors#caveats-that-apply-to-a-whole-family) —
per-field scanning, the 128 KiB refusal, the de-obfuscation ceilings, the stream ladder — apply to
all nine and are not repeated here.

<Callout type="key">
**The nine cover structured formats only** — a shape with a checksum or a fixed grammar. Names,
street addresses, organizations and every other span-less entity are structurally out of reach.
That gap is the intended task of `sidecar.ner_pii`, whose stock activation remains rejected pending
runtime/model qualification. The registered detectors are **nowhere near "PII
detection"** — the detectors' own words.
</Callout>

<Callout type="warning">
**Read every figure below with its denominator.** Five of the nine have a corpus-measured recall,
and every one of those denominators is **under 20** — the floor below which the product's own
harness refuses to render a rate as a decimal. Four have no corpus measurement at all and say so.
A tally is not a rate.
</Callout>

## `pii.card` — payment card PANs

**Covers.** 13–19 digit PANs that pass **both** the ISO/IEC 7812-1 Luhn check digit **and** a
published issuer range at a length that range issues (Visa, Mastercard including 2221–2720, Amex,
Discover, JCB, Diners, UnionPay, Maestro 16–19). Spaced or hyphenated in any grouping when nothing
else shares that run; an unbroken run whatever abuts it; and a `4-4-4-4` or `4-6-5` grouping
followed by at most one 1–3 digit group that ends the run — the expiry and trailing-CVV forms.

**Not covered.**

- **Efficacy rests on 12 positives** (11 of 12 caught, 0 false positives on 101 hard negatives and
  0 on 42 clean controls). Below the quotable floor, so no decimal is offered.
- **Anything that fails Luhn or falls outside the issuer allowlist** — including a mistyped or
  truncated *real* PAN, which is missed rather than reported. A deliberate precision trade.
- **The precision floor is about 3 in 100 per candidate.** An arbitrary 16-digit run passes
  Luhn+IIN 3.145% of the time (629/20000, measured). Read `basis: exact` as *a deterministic
  checksum passed* — which is all it has ever meant — not as *this is a card*.
- **Non-canonical groupings sharing a run with another number.** `4111 1111 1111 1111 12/25` is
  caught; `41111 111111 11111 12/25` (5-6-5) is a miss. A spaced PAN followed by *more than one*
  short group (`… 12 25 123`) is given up on purpose — ending the tail at any non-digit would
  report a partial PAN as a complete one, which is worse than a miss.
- **Never spends a calibrated bound**, on either lane: every rule is an exact match.
- The expiry, the CVV, the cardholder name and the billing address. It finds the PAN and nothing
  else about the card.

## `pii.ssn` — US Social Security Numbers

**Covers.** The 3-2-4 shape separated by a hyphen or a space, group not `00` and serial not
`0000`; the same number obfuscated with zero-width or homoglyph characters; and a nine-digit run
recognized on the split-token projection when split across whitespace, a hyphenated line wrap or a
markup boundary.

**Not covered.**

- **Efficacy rests on 10 positives** (10 of 10, 0 false positives on both negative sets). Ten is
  below the floor; the harness refuses to render that as a decimal and neither does this page.
- **US-only.** No EU national identifiers, no Canadian SIN, no UK NINO.
  [`pii.nationalid`](#piinationalid--six-national-id-schemes) covers a handful of other countries.
- **The SSA never-issued area rules (000, 666, 900–999) are deliberately NOT applied.** A
  structurally never-issued area is still reported, because this is a redaction gate:
  over-inclusion on the area field costs a placeholder, under-inclusion costs the leak. The
  group-`00` and serial-`0000` rules *are* applied, and they refuse a blank-form template.
- **A bare nine-digit run on the primary view.** `SSN 123456789` written that way in prose is a
  miss — only the split-token projection recognizes an unseparated SSN. A precision choice: a
  nine-digit run is an order id far more often than an SSN.
- ITINs, EINs, a nine-digit run inside a longer run, and the name or date of birth that would make
  an SSN identifying.

## `pii.email` — email addresses

**Covers.** `local@label.tld` where the domain has at least one dot and a 2–24 letter final label,
including plus-tagged locals and addresses inside RFC 5322 headers. A split-and-rejoined address
is reported with a **lower raw score** (0.70) than one typed intact (0.85).

**Not covered.** Efficacy rests on **12 positives** (11 of 12). At-sign tokens with no dotted
domain (`user@host`, a CI runner name) are dropped — so a real address at a dotless intranet
domain is missed. Reserved and documentation TLDs (`.invalid`, `.example`, `.test`, `.localhost`,
`.local`, `.internal`, `.lan`, `.corp`, the bare `.tld`/`.domain` placeholders) are refused as
template text. Display names are excluded from the span. IDN/punycode domains and non-ASCII local
parts fold toward Latin and are then likely refused.

<Callout type="warning">
**RFC 2606's reserved second-level names — `example.com`, `example.net`, `example.org` — are NOT
refused, even though the TLDs above are.** That inconsistency is deliberate: the frozen efficacy
corpus puts every committed positive at `example.*`, so refusing them would score the detector at
zero recall on a corpus that cannot contain a real address. **A production deployment often should
refuse them, and this build offers no config knob to do so.**
</Callout>

## `pii.ip` — IP addresses

**Covers.** IPv4 as four dot-separated octets 0–255 with no leading zeros. IPv6 that carries real
structure: a `::` marker, or exactly eight colon-separated hex groups.

**Not covered.**

- **It cannot tell an IPv4 address from a four-part software version number, because they are the
  same string.** `1.24.3.7` is reported as an address. Measured: **4 false positives on the 101
  hard negatives — an FP rate of 0.0396**, all four the version-quad case. That denominator clears
  the floor, so unlike its recall the figure *is* quotable as a rate. Its raw score is set to
  **0.60**, below every other shape in the detector, so a binding can threshold above it
  deliberately.
- Three-to-seven-group uncompressed IPv6 — a `11:04:22` timestamp and a MAC address have that
  shape — and with it any genuinely truncated IPv6 fragment. CIDR suffixes, zone identifiers and
  ports sit outside the span. The IPv6 split-token pass is not run.
- **Whether an address is a person.** An IP is PII in some jurisdictions and infrastructure
  metadata in others; the detector reports the shape and leaves the judgment to the binding.

## `pii.iban` — international bank account numbers

**Covers.** The 79 country codes in this build's length table (78 from the ISO 13616 registry plus
`XK`), unspaced or in the conventional 4-character grouping, any case, at that code's exact length
and passing the ISO 7064 MOD-97-10 check.

**Not covered.** Efficacy rests on **6 positives** (6 of 6). A mistyped or truncated real IBAN is
missed rather than reported. The length table is a hand-maintained allowlist, so a newly registered
country is a miss until it is added. BIC/SWIFT codes, sort codes, routing numbers and domestic
account numbers are not reported — `BIC=COBADEFFXXX` beside a detected IBAN is not flagged.
Separators other than a single space between groups are not matched. And mod-97 is a *structural*
check: a well-formed IBAN for a closed or never-issued account is reported exactly like a live one.

## `pii.phone` — telephone numbers

**Covers.** E.164 international form only: a leading `+`, an assigned ITU-T country calling code,
8–15 total digits with at least five subscriber digits, with or without single spaces, hyphens or
dots.

**Not covered.** Efficacy rests on **6 positives** (6 of 6). **Any number without a leading `+`** —
`(202) 555-0142`, `202-555-0142`, `020 7946 0958` — is missed. That is a deliberate precision
scope: a bare ten-digit pattern fires on every ten-digit id column, and the clean controls this
build is measured against are CSV exports and JSON logs. Also missed: the parenthesised area-code
form even *with* a `+` (`+1 (202) 555-0142` stops the match at the `(` ), extensions, vanity
numbers, short codes, and the country the number belongs to.

## `pii.nationalid` — six national-ID schemes

**Covers.** Spain DNI and NIE (control letter verified), France INSEE NIR (mod-97 key, Corsica
2A/2B, birth-month range), Brazil CPF (both mod-11 digits, repdigits refused), India Aadhaar
(UIDAI Verhoeff, leading digit 2–9), Netherlands BSN (elfproef, repdigits and values under
10,000,000 excluded), and the UK NINO — **shape only**, because a NINO carries no check digit.

**Not covered.**

- **Efficacy is unmeasured.** The shared corpus contains no non-US national-ID positives and no
  national-ID hard negatives. The only evidence is unit fixtures built from each scheme's published
  check-digit specification.
- **Six countries out of roughly two hundred.** Germany's Steuer-ID, Italy's Codice Fiscale,
  Poland's PESEL, Sweden's personnummer, Japan's My Number, China's resident identity number and
  every other national scheme are misses.
- **The Canadian SIN and the South African ID number are deliberately absent.** Both are bare digit
  runs gated by Luhn alone, and Luhn passes one arbitrary run in ten — that would report roughly
  one in every ten nine-digit order numbers as a national identifier. Excluded rather than shipped
  weak.
- **The NINO's correctness.** No checksum exists to verify, so any string of that shape with an
  issuable prefix is reported. It mints a `reported` confidence, not `exact`, and the difference is
  visible on the row.
- Passport, driving-license, health-service and other tax identifiers.

## `pii.crypto` — cryptocurrency addresses

**Covers.** Legacy Bitcoin-family addresses (P2PKH `1…`, P2SH `3…`) whose base58check checksum
verifies over a 25-byte payload, and segwit addresses with a `bc`/`tb`/`ltc` human-readable part
whose BIP-173 bech32 or BIP-350 bech32m checksum verifies, with the witness-version-to-variant rule
enforced.

**Not covered.**

- **Efficacy is unmeasured** — no crypto positives and no crypto hard negatives in the shared
  corpus. Its only evidence is unit fixtures over published BIP-173 and Bitcoin Core test vectors.
- **Ethereum and every other `0x`-prefixed 20-byte address.** `0x` plus 40 hex is also every
  transaction-hash prefix in a blockchain log; the only thing separating them is the EIP-55
  mixed-case checksum, which is defined over Keccak-256 — not in the Go standard library, and this
  build admits no dependency for it. **A real gap, not a scoping preference.**
- Monero, Ripple, Cardano, Solana, Tron and every chain whose format is neither
  base58check-over-25-bytes nor bech32 with those prefixes; extended public keys, BIP-39 seed
  phrases and WIF private keys; and whether an address belongs to a person, an exchange or a
  contract.
- The split-token pass is not run: re-joining fragments would change the bytes the checksum covers.

## `pii.mac` — MAC addresses

**Covers.** Six two-hex-digit octets separated consistently by `:` or `-`, excluding the all-zero
and broadcast addresses.

<Callout type="warning">
**The shared corpus actively disagrees with this detector.** It labels every MAC address a **PII
hard negative**, so each of this detector's true positives scores as a *false positive* for the
`pii` category. Measured, on a denominator large enough to quote: binding `pii.mac` takes the
family's hard-negative FP rate from **0.0396 (4/101) to 0.0792 (8/101)**, and its one-vs-rest
precision from **0.9107 (51/56) to 0.8500 (51/60)**. Whether a MAC address is personal data is a
jurisdictional question this build has not settled — so arming it is your decision, not a default.
</Callout>

**Also not covered.** Its efficacy is otherwise unmeasured (no MAC positives in the corpus). The
dotted Cisco notation (`0000.1b44.113a`), EUI-64 and 20-octet InfiniBand forms. A MAC split across
whitespace is a deliberate miss — twelve hex digits have no checksum and no self-anchoring
structure, so a re-joined coincidence would pass every gate the rule has. And whether the address
identifies a person, a randomized privacy MAC, or a router in a datacenter.

<NextSteps items={[
  { title: "Redact PII before it reaches the model", href: "/docs/recipes/redact-pii-before-it-reaches-the-model", description: "An input-phase pii guard from monitor to enforce, with the false-positive check in between." },
  { title: "Secrets & injection detectors", href: "/docs/reference/guardrail-detectors-secrets-injection", description: "The secrets three-way split, the injection floor, and the two external tiers." },
  { title: "Confidence & the one rule", href: "/docs/guardrails/confidence", description: "Why exact means a checksum passed, and why an interval needs an artifact beside it." },
  { title: "Air gap and known limits", href: "/docs/guardrails/air-gap-and-limits", description: "Check activation restrictions, external calls, and inspection limitations." }
]} />
