Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Redact PII before it reaches the model

Take an input-phase PII guard from monitor to enforce, with the false-positive check in between.

How-to guideLast updated

On this page

Goal: replace detected structured PII in supported prompt fields before provider dispatch, then measure false positives before enforcement. Successful inspection and transformation are required; this example inherits on_error: open, which may forward original bytes when protection fails. Use acknowledged closed enforcement when that continuation is unacceptable.

1. Pick the detectors deliberately

The nine embedded pii.* detectors are individually bindable, and arming all nine is not the safe default it looks like.

guardrails:
  enabled: true
  default_profile: standard
  local_budget_ms: 50
  guards:
    - id: pii_in
      category: pii
      phases: [input]
      detectors: [pii.card, pii.email, pii.iban, pii.ssn, pii.phone]
      mode: monitor
      action: redact
      rule_ref: POL-PII-1
  profiles:
    standard: [pii_in]

Read what each one misses before arming it. Three that catch people out: pii.phone needs a leading + and misses every national format; pii.ssn is US-only and misses a bare nine-digit run in prose; pii.email deliberately does not refuse example.com.

2. Dry-run it before it sees traffic

$ tokentriage guardrails test --config tokentriage.yaml \
    --content 'Please charge card 4111 1111 1111 1111 for [email protected]'

Read operation_kind, case inspection state and execution records. Literal text defaults to detector_diagnostics; it is not a serving-path proof.

Point it at authorized stored traffic with --req-id or --dataset to run over stored request bodies. Use --corpus if you have a labeled set — that is the only subject that carries ground truth.

3. Run it in monitor and read the rows

monitor runs the detectors on real traffic and changes no byte. Every fire lands on the row with effect: annotated.

import json, time, urllib.request

# The guard runs on every request; you read what it did from the decision row.
req = urllib.request.Request(
    "http://127.0.0.1:8787/v1/messages",
    data=json.dumps({
        "model": "claude-haiku-4-5",
        "max_tokens": 64,
        "messages": [{"role": "user",
                      "content": "Charge card 4111 1111 1111 1111 for [email protected]"}],
    }).encode(),
    headers={"content-type": "application/json"},
)
with urllib.request.urlopen(req) as response:
    response.read()  # Complete the response before looking for its row.
    req_id = response.headers["x-tt-request-id"]
if not req_id:
    raise RuntimeError("gateway request ID missing")

# Primary rows omit kind; companions may be appended afterward. Match this request.
deadline = time.monotonic() + 5
row = None
while time.monotonic() < deadline:
    try:
        with open("decisions.jsonl", encoding="utf-8") as fh:
            for line in fh:
                if not line.endswith("\n"):
                    continue  # An append may still be in progress.
                candidate = json.loads(line)
                if candidate.get("kind") is None and candidate.get("req_id") == req_id:
                    row = candidate
    except FileNotFoundError:
        pass
    if row is not None:
        break
    time.sleep(0.05)
if row is None:
    raise RuntimeError("primary decision row not available within five seconds")

print(json.dumps(row["disclosures"]["guardrails"]["fired"], indent=2))
{
  "binding": "pii_in",
  "detector": "pii.card",
  "detector_version": "pii.card-v1",
  "action": "redact",
  "effect": "annotated",
  "entities": [{ "type": "CREDIT_CARD", "count": 1 }],
  "reversible": false,
  "confidence": { "basis": "exact", "point": 1 },
  "rule_ref": "POL-PII-1"
}

4. The false-positive check — do not skip this

$ curl -s http://127.0.0.1:9090/tt/api/v1/guardrails/evidence |
    jq '{adjudicated: .quality_sources, silent: [.configured[] | select(.fired==false) | .id]}'

Three things to satisfy yourself about before climbing:

  1. Adjudicated outcomes above zero, with a denominator you would defend.
  2. suspended[] is empty on this binding. A guard that was silently not running is not a guard you have evidence about.
  3. The binding is not in the “fired on nothing” roster. If it is, you have proved nothing yet.

5. Enforce

      mode: enforce

When supported inspection and transformation complete, redact splices the detected span and the model sees [REDACTED:CREDIT_CARD] instead of the PAN. Verify the execution, transformation and delivery receipts as well as effect: mutated. The default open failure posture can still forward original content when protection cannot complete.

Rollback is one line back to monitor. Nothing else changes: the same detectors keep running and the same rows keep being written.