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

# Writing an effective PII policy

> How to design rule content and priorities so the detection engine behaves accurately, illustrated with US-flavored example rules.

[How to add a customized PII policy](/en/v1.3/admin/how-to/add-pii-policy) covers the console **procedure** — creating a policy and applying it. This page covers the rule **content**: what to write in which field so the detection engine behaves accurately, and how to design priorities so rules don't fight each other. The examples follow the structure of the **Starfort default PII policy** (which ships tuned for Korean identifiers — resident registration numbers, Korean addresses and phone formats), adapted here to **US data** for familiarity. The writing principles are identical for any locale.

## Policy structure and processing order

A PII policy is made of three rule arrays. The allowed top-level keys are `ner`, `regex`, and `keyword`, and the engine always checks them in the order **Keyword → Regular Expression → NER**. Text settled by an earlier stage is **hidden from later stages**, so an earlier stage's decision is final for that text.

```mermaid theme={"dark"}
flowchart LR
    T["Input text"] --> K["1. Keyword<br/>exact match"]
    K -->|"remaining text"| R["2. Regular Expression<br/>pattern match"]
    R -->|"remaining text"| N["3. NER Entity<br/>AI-model detection"]
    K & R & N --> D["All detections"]
    D --> F{"Final action"}
    F -->|"any BLOCKING match"| B["BLOCK"]
    F -->|"else any masking match"| M["MASK"]
    F -->|"no match"| P["PASS"]
```

| Rule type              | Characteristics                                          | Best for                                                                      |
| ---------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------- |
| **Keyword**            | Case-sensitive exact match. Fastest, fully deterministic | Internal codenames, known-safe values (allowlist)                             |
| **Regular Expression** | Pattern match. Deterministic                             | Fixed-format identifiers (SSNs, phone numbers, card numbers, emails)          |
| **NER Entity**         | AI-model detection. Understands context                  | Context-dependent information (names, addresses), variants that patterns miss |

## Designing priorities

As rules multiply, their interactions decide the outcome. Keep three priority rules in mind.

1. **Rule-type priority: Keyword > Regular Expression > NER.** Text settled by an earlier stage is never re-judged by a later one.
2. **PASSING is an allowlist.** Text matched by a `policy_type: "PASSING"` rule is **settled as safe** — no masking or blocking rule can override it afterwards. When two rules of the same type match the same text, the rule with the **lower id** wins.
3. **Action priority: BLOCK > MASK > PASS.** If any rule in the request decides BLOCK, the **whole request is blocked**, even when every other rule only masks.

Put these principles to work by placing **PASSING test-pattern rules at lower ids** than the real masking regexes, so dummy values used in docs and QA never get masked:

```json theme={"dark"}
{
  "id": 0,
  "rule_id": "test_data:_ssn_test_pattern",
  "regex": "(?<!\\d)(?:000-00-0000|123-45-6789|078-05-1120)(?!\\d)",
  "description": "000-00-0000, 123-45-6789, 078-05-1120 (well-known sample SSNs)",
  "policy_type": "PASSING",
  "mask_word": "TEST_DATA",
  "alert_message": "test data pattern - allowed"
}
```

A keyword rule can serve the same purpose — preempt `John Doe`, the placeholder name every sample document uses, with PASSING so the NER name rule can't mask it. Because the Keyword stage runs before NER, the preemption is guaranteed.

## Writing Regular Expression rules

A US Social Security number rule:

```json theme={"dark"}
{
  "id": 10,
  "rule_id": "ssn:_us_social_security_number",
  "regex": "(?<!\\d)(?!000|666|9\\d{2})\\d{3}-(?!00)\\d{2}-(?!0000)\\d{4}(?!\\d)",
  "description": "219-87-3456, 457-55-5462 — excludes invalid areas (000, 666, 900-999) and invalid groups (00, 0000)",
  "policy_type": "MASKING",
  "mask_word": "SSN",
  "alert_message": "social security number detected"
}
```

Practices this rule demonstrates:

* **Anchor digit boundaries.** Without the surrounding `(?<!\d)` / `(?!\d)`, a **middle slice** of a longer digit run (a bank account, a barcode) gets misdetected as an SSN.
* **Encode known-invalid ranges in the pattern.** The lookaheads reject areas the SSA never issues (`000`, `666`, `900–999`) — false positives you can eliminate deterministically shouldn't be left to chance.
* **Record match examples in `description`.** The field plays no part in detection, but it saves the next reviewer from having to decode the pattern.
* **Resolve conflicts with other rules inside the pattern.** A phone-number rule should carry a trailing guard (e.g. `(?![-\.]?\d)`) so it doesn't claim the leading segment of a longer account-style number.

<Warning>
  Patterns that can match empty text (`\d*`, `secret|`) detect nothing and are skipped. Leading/trailing spaces are trimmed when the policy is read — express an intentional edge space inside the pattern itself (`\s`, `[ ]`).
</Warning>

## Writing NER Entity rules

Only four NER fields reach the AI model: **Name, Description, Positive Examples, Negative Examples**. **Policy Type, Mask Word, and Alert Message** decide what happens *after* a detection and never influence detection itself. Detection accuracy therefore lives entirely in the first four fields.

A US street-address rule shows what each field does. What makes it instructive is the **specificity boundary**: a location mention is sensitive only when it is specific enough to find a particular person or business — general location talk is not. Read the sections below with an eye on which part of the example each one points at:

```json theme={"dark"}
{
  "id": 2,
  "name": "street address (US)",
  "description": "US addresses that identify the location of a specific person or business. Must contain a street number with a street name, optionally followed by unit detail (Apt/Suite/Floor). General location references — landmarks, transit stations, standalone building names, a city or neighborhood without a street number — are NOT addresses.",
  "policy_type": "MASKING",
  "mask_word": "ADDRESS",
  "alert_message": "address detected",
  "positive_examples": [
    "Our headquarters: 350 Fifth Avenue, Suite 3210, New York, NY 10118 →350 Fifth Avenue, Suite 3210",
    "I live at 1240 Maple Street, Apt 4B, Springfield, IL →1240 Maple Street, Apt 4B",
    "Please deliver the package to 77 Beacon St 2nd floor before noon →77 Beacon St 2nd floor"
  ],
  "negative_examples": [
    "Meet me at Grand Central Station",
    "See you at the Starbucks on Fifth Avenue",
    "Any good cafes in SoHo?"
  ]
}
```

<Note>
  The shipped default policy is authored for Korean traffic (Korean addresses, resident numbers, phone formats). The rule above adapts its address rule to US data — the field structure and writing principles are unchanged.
</Note>

### Description — a definition plus specificity conditions

The Description above carries four elements in order: the **definition** (identifies a person's or business's location), the **required specificity** ("a street number with a street name"), the **allowed detail variants** (Apt/Suite/Floor), and **exclusions** (landmarks, transit stations, standalone building names, city or neighborhood without a street number). The Description draws the sensitive/not-sensitive boundary in plain sentences — and exclusions written there genuinely work, because the model is instructed to answer NO for cases the Description rules out. Scenario 3 below shows this boundary splitting a single message.

### Positive Examples — the arrow convention

Write every positive example as `context →value`. The model learns that the part after `→` is the **exact text to detect**.

* The sentence before `→` teaches **when** to fire (recall); the value after `→` teaches the **exact output form** (precision).
* The value also teaches the **masking span**. In all three examples the value covers the street number through the unit detail and drops the city/state/ZIP — the examples tell the rule where sensitive text begins and ends.
* The value must be **locatable back in the input**. Writing it exactly as it appears is safest (whitespace differences are tolerated); a value reassembled in a different order — street name before the number, say — cannot be located and is discarded.
* The three examples each teach **one surface form that actually occurs**: a labeled headquarters line, a residential address with an apartment unit, and an address embedded mid-sentence with no label. Give each must-catch form its own example.
* Write examples in the language and format of the data you expect to catch.

### Negative Examples — the false-positive shield

Add **near-misses that lack the required specificity**. The three negatives each close a different false-positive path: `Grand Central Station` (a landmark meeting spot), `the Starbucks on Fifth Avenue` (a business on a named street — but no street number), and `SoHo` (a neighborhood). All three pair with the Description's exclusions. NER rules are screened together in small batches, so weak negatives on one rule can cause over-detection on its neighbors too.

## Writing Keyword rules

Keyword rules are case-sensitive **exact matches**. They see no context, so avoid short generic words.

* **Good BLOCKING keyword**: an internal codename like `PROJECT-ORION` — sensitive wherever it appears.
* **Good PASSING keyword**: `John Doe` — an allowlist that preempts the placeholder name before the NER name rule can mask it.
* **Bad keyword**: a generic noun like `account` — fires indiscriminately on every occurrence.

## Example scenarios

Behavior of a Guardian with rules like the above assigned.

### Scenario 1 — masking a mobile number

> "My number is (212) 555-0134. You can also reach me at 212.555.0199."

The phone regex matches both values despite the different separators. The result is `"action": "MASK"` with body `My number is [PHONE_NUMBER_1]. You can also reach me at [PHONE_NUMBER_2].` — mask words get indexed in order of appearance.

### Scenario 2 — test data passes, real data masks

> "The test account SSN is 123-45-6789; the real one is 219-87-3456."

`123-45-6789` matches the SSN masking rule's shape — but it is the canonical sample SSN, so the PASSING test pattern (id 0) **preempts it first** and it passes through unchanged. Only `219-87-3456` is masked: `The test account SSN is 123-45-6789; the real one is [SSN_1].` — this is why the allowlist rules carry lower ids than the real ones.

### Scenario 3 — only the specific address is sensitive

> "Let's meet at the coffee shop by Grand Central Station. Send the signed contract to 350 Fifth Avenue, Suite 3210, New York, NY 10118."

One message, two location mentions, two different verdicts. `Grand Central Station` is a landmark-level mention, so it passes — exactly what the Description's exclusion and the negative examples taught. Only the specific address masks, and the span starts and ends where the example values taught it to: `Let's meet at the coffee shop by Grand Central Station. Send the signed contract to [ADDRESS_1], New York, NY 10118.`

### Scenario 4 — BLOCK beats everything

If ten masking rules match and one BLOCKING rule matches, the result is `"action": "BLOCK"` and no masked body is returned. Reserve BLOCKING for values whose very transmission must be stopped (confidential codenames); use MASKING for the rest.

## Checklist

* Did you anchor digit/word boundaries (`(?<!\d)`, `(?!\d)`) in your regexes?
* Did you preempt test/sample values with PASSING rules first?
* Does each NER Description state the definition + specificity/context conditions + exclusions?
* Are Positive Examples written as `context →value`, with the value marking the exact masking span?
* Do Negative Examples contain near-misses that lack the required specificity?
* Is BLOCKING reserved for rules that truly need to stop the request?

<Tip>
  Starfort ships a comprehensive default PII policy (authored for Korean identifiers) that you can load through the console's **JSON** editor — use it as a structural template and adapt the rules to your locale, as this page did. The **TIP** button on the console policy page shows a summary of this guide.
</Tip>

<Warning>
  Rule count and example volume are detection cost. The full rule definitions travel with every call, so an overly verbose policy slows every request and, in the extreme, exceeds the model's input limit and fails the request. Five to ten sharp examples per rule beat an exhaustive list.
</Warning>
