# Tokenize PII and get it back

A working reversible round trip — the config, the environment variable, a real request pair, both ledger rows, and the one trade you are making against redact.

Source: https://tokentriage.com/docs/recipes/tokenize-pii-and-get-it-back

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

**Goal:** replace detected email addresses in supported request fields before the provider call,
then restore this request's minted placeholders in a buffered answer.

The example uses the default `on_error: open`: failed inspection can forward original bytes.
For a requirement to withhold content when inspection cannot finish, configure acknowledged
`on_error: closed` and verify its failure cases as well as the successful round trip.

The trade in one sentence: **reversibility buys you a restored answer and costs you a vault holding
real values** for the life of the request. `redact` has nothing to leak because it kept nothing.

## 1. Configure the forward verb only

There is no re-identify binding to write. Bind `tokenize` on the input phase and the reverse leg runs
on the output phase by itself — a config naming `action: reidentify` is
[refused at boot](/docs/guardrails/reversible-deidentification).

```yaml
guardrails:
  enabled: true
  default_profile: standard
  local_budget_ms: 50
  vault:
    key_env: TT_VAULT_KEY
  guards:
    - id: pii_tokenize_in
      category: pii
      phases: [input]
      detectors: [pii.email, pii.card]
      mode: enforce
      action: tokenize
      rule_ref: POL-PII-REID-1
  profiles:
    standard: [pii_tokenize_in]
```

<Callout type="note">
`vault.key_env` names the variable; it never carries the key. In this build setting it makes the
vault record codec usable; production does not persist vault mappings. The vault stays in memory
for one request either way. Ordinary ledger and content sinks have separate retention policies.
Omitting this key does not break the round trip.
</Callout>

Validate before you arm it. `guardrails explain` loads the config, so a bad file fails here rather
than at boot:

```console
$ tokentriage guardrails explain --binding pii_tokenize_in --config guarded.yaml

BINDING pii_tokenize_in
  category:   pii
  phases:     input
  mode:       enforce
  on_error:   open

  LADDER — the verb each severity resolves to (fall-down included)
    SEVERITY  ACTION    REVERSIBLE  THIS BUILD CAN DO IT
    low       tokenize  yes         yes
```

## 2. Start it with the key in the environment

```console
$ export TT_VAULT_KEY="$(head -c 32 /dev/urandom | base64)"
$ tokentriage run -c guarded.yaml
```

## 3. Send a request

```console
$ curl -s http://127.0.0.1:4790/v1/chat/completions \
    -H 'Content-Type: application/json' \
    -H 'Authorization: Bearer sk-…' \
    -d '{"model":"gpt-4o-mini","messages":[
          {"role":"user","content":"my email is ada@example.com"}]}'
```

What the **upstream** received:

```console
"my email is [TTsbjk:EMAIL_1]"
```

What your **client** received:

```json
{"choices":[{"message":{"role":"assistant",
  "content":"Sure — I have noted my email is ada@example.com and will follow up there."}}]}
```

The `TTsbjk` part contains a fresh random nonce drawn for each request; collisions remain possible.
Isolation comes from the request-local minted mappings. Within one request the same value always
maps to the same placeholder:

```console
"ping grace@example.com and grace@example.com again"
   ⇒ "ping [TTj5xz:EMAIL_1] and [TTj5xz:EMAIL_1] again"
```

## 4. Read both legs on the ledger

Two `kind: guardrail` rows, same `req_id`. The forward leg names the detector and its finding; the
reverse leg has **no detector**, because none runs on it:

```json
{"v":1,"kind":"guardrail","req_id":"tt_o2fjhgarhtsbm2xk67ekejljdi",
 "binding":"pii_tokenize_in","phase":"input","detector":"pii.email",
 "detector_version":"pii.email-v1","action":"tokenize",
 "findings":[{"entity":"EMAIL","spans":[{"field":"messages[0].content","off":12,"len":15}],
   "raw_score":0.85,"confidence":{"basis":"reported","point":0.85}}],
 "posture":"open","mode":"enforce"}

{"v":1,"kind":"guardrail","req_id":"tt_o2fjhgarhtsbm2xk67ekejljdi",
 "binding":"pii_tokenize_in","phase":"output","detector":"","detector_version":"",
 "action":"reidentify","posture":"open","mode":"enforce"}
```

The reverse leg carries the **forward binding's id**, which is what lets you join the two halves of
one round trip without inventing a correlation field.

## 5. If you want a realistic surrogate instead

Swap the verb. Same config, same round trip, different bytes upstream:

```yaml
      action: pseudonymize
```

```console
UPSTREAM SAW: my email is grace.jones.rlyn7gq0@example.net
```

Both verbs are equally reversible; pick `pseudonymize` when a bracketed marker would confuse the
model and `tokenize` when a realistic-looking value would.

<Callout type="warning">
**Check what your guard can actually reach before you trust this.** `tokenize` only fires on text the
extractor hands it. Supported decoded tool arguments, including
`messages[].tool_calls[].function.arguments`, are inspected. Unknown fields and media do not acquire
text coverage; incomplete supported inspection follows the effective mode and failure posture. See
[What gets inspected](/docs/guardrails/what-gets-inspected).
</Callout>

## What can go wrong

- **The model mangles the placeholder.** Exact match only, so the mangled marker reaches your client
  verbatim. Ugly, and the safe failure.
- **The response came from the cache.** The reverse leg never runs on replayed bytes — restoring
  another request's body is a cross-request leak, not a no-op.
- **You are streaming.** The reverse leg is the buffered output gate's; it is not implemented on the
  stream lane.
- **You expected `fuzzy_reid` to help.** It is accepted and read by nothing.

<NextSteps items={[
  { title: "Reversible de-identification", href: "/docs/guardrails/reversible-deidentification", description: "The full round trip, the vault's real scope, and every failure mode." },
  { title: "Redact PII before it reaches the model", href: "/docs/recipes/redact-pii-before-it-reaches-the-model", description: "The irreversible alternative — cheaper, and it keeps nothing." },
  { title: "Roll out a guard without breaking traffic", href: "/docs/recipes/roll-out-a-guard-without-breaking-traffic", description: "Get to enforce mode on real traffic without an incident." },
  { title: "Reversible de-identification", href: "/docs/guardrails/reversible-deidentification", description: "Understand the tokenization lifecycle, restoration, and operational limits." }
]} />
