> This page is for NeMo Relay, version dev.
> For other versions, use one of these documentation indexes:
> - Latest (v0.10.4) (default): https://docs.nvidia.com/nemo/relay/latest/llms.txt
> - dev: https://docs.nvidia.com/nemo/relay/dev/llms.txt
> - v0.11.1: https://docs.nvidia.com/nemo/relay/v0.11.1/llms.txt
> - v0.10.4: https://docs.nvidia.com/nemo/relay/v0.10.4/llms.txt
> - v0.9.4: https://docs.nvidia.com/nemo/relay/v0.9.4/llms.txt
> - v0.8.4: https://docs.nvidia.com/nemo/relay/v0.8.4/llms.txt
> - v0.7.3: https://docs.nvidia.com/nemo/relay/v0.7.3/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/relay/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/relay/_mcp/server.

# PII Redaction Behavior

> Understand PII payload boundaries, actions, selectors, trajectory preservation, and match diagnostics.

Use this page to understand which values a policy selects and what Relay publishes
after sanitization. For configuration keys and complete before/after examples,
refer to [Configuration](/nemo/relay/dev/configure-plugins/pii-redaction/configuration).
For runtime placement and backend availability, refer to
[About](/nemo/relay/dev/configure-plugins/pii-redaction/about).

## Observability Semantics

### Payload boundaries and event publication

The plugin uses different payload boundaries for tools and LLMs:

* Tools use JSON-native payloads. Paths point into the emitted tool args
  directly. For built-in action policies, including detector-backed policies,
  tool response paths apply independently to the emitted
  `ToolExecutionResult.result` and `ToolExecutionResult.annotation` JSON
  documents. A path is relative to each document; it does not start with
  `/result` or `/annotation`.
* LLM requests use the active resolved codec for that managed call. LLM
  responses use the active built-in codec for normalized projection. Prefer
  normalized Relay paths such as:
  * `/headers/authorization` for an exact request header field
  * `/messages/0/content` for request message content
  * `/message` for the normalized assistant response text
* Marks and non-tool, non-LLM scopes sanitize `data`, `category_profile`, and
  `metadata` as separate JSON values. A target path is evaluated independently
  inside each value; it does not start with `/data`, `/category_profile`, or
  `/metadata`.

When using LLM selectors, supply the matching codec on the call, or configure a
[`codec` fallback](/nemo/relay/dev/configure-plugins/pii-redaction/configuration#how-to-choose-a-codec)
for calls without an active codec. If a required codec is unavailable or Relay
cannot safely sanitize and convert the payload, it omits the LLM payload and
its annotation from the recorded event.

The PII policy changes observability data only. It leaves inputs sent to
providers and tools, and results returned to your application, unchanged.
Subscribers and exporters receive sanitized events; values removed by the
policy are unavailable to them.

Metadata is sanitized separately from request and result payloads. In the
single-policy configuration, use the
[surface flags](/nemo/relay/dev/configure-plugins/pii-redaction/configuration#single-policy-settings)
to choose which event surfaces are sanitized.

## Action Semantics

### How each action changes a selected value

#### `remove`

`remove` is structural.

Configure at least one `target_paths` or `target_path_globs` selector when
using `remove`. Relay rejects an unscoped `remove` policy during validation to
prevent accidental removal of every string value in the selected surfaces.

When a target matches:

* Object fields are removed.
* Array elements become `null` on JSON-native surfaces. If a normalized LLM
  array removal cannot be safely re-encoded, Relay omits the event payload and
  its annotation instead.
* Targeted scalar or root values become `null`.

#### `regex_replace`

`regex_replace` applies the configured regex to matching string leaves and
replaces matches with the configured `replacement`.

If you set `detector` instead of `pattern`, the built-in backend uses the
detector's stock matcher regex.

#### `redact`

`redact` is the deterministic whole-match replacement lane.

It uses the same `pattern` or `detector` matcher flow as `regex_replace`, but
defaults the replacement token to `[REDACTED]` and is intended for cases where
you do not want to preserve any matched secret characters.

Use `redact` when you want:

* credential-style secrets fully replaced
* a consistent redaction token across detectors
* clearer policy intent than a custom `regex_replace`

#### `hash` (SHA-256)

`hash` uses SHA-256 on the UTF-8 bytes of the selected text, without a salt.
It returns the digest as 64 lowercase hexadecimal characters.

Without `pattern` or `detector`, the entire selected string is hashed.
With either matcher, each matching substring gets its own SHA-256 digest;
unmatched text is preserved.

#### `mask`

`mask` replaces the middle portion of each matching string leaf with the
configured `mask_char`.

Use `unmasked_prefix` and `unmasked_suffix` when you want to preserve a small
leading or trailing segment for correlation or debugging, such as the last four
characters of a token.

When `pattern` or `detector` is set, `mask` only masks matching substrings
inside the string leaf.

When `detector` is set and you do not specify `unmasked_prefix` or
`unmasked_suffix`, the built-in backend applies detector-aware defaults:

* `email`: Preserves the domain and the first local-part character
* `phone`: Preserves the last four digits while keeping separators intact
* `api_key`: Preserves the vendor-style prefix such as `sk-` and the last four characters
* `ip_address`: Preserves the last octet
* `ipv6`: Preserves the last segment
* `url`: Preserves the scheme and host, then collapses the path/query tail
* `uuid`: Preserves the last four characters
* `bearer_token`: Preserves the auth scheme and the last four characters
* `jwt`: Preserves the header segment and the tail of the signature
* `credit_card`: Preserves the last four digits while keeping separators intact
* `aws_access_key_id`: Preserves the provider prefix and the last four characters
* `aws_secret_access_key`: Preserves the last four characters
* `gcp_api_key`: Preserves the `AIza`-style prefix and the last four characters
* `azure_storage_account_key`: Preserves the last four characters

## Detector Matching

Built-in detectors are deterministic regex-backed matchers. Refer to
[Detector Presets](/nemo/relay/dev/configure-plugins/pii-redaction/configuration#detector-presets)
for the supported detector names.

`bearer_token` is heuristic rather than vendor-specific. It can still match
benign bearer-style values, so prefer a narrower detector when you know the
credential family.

For actions other than `remove`, if both target-selector lists are empty, the
built-in backend sanitizes every matching string leaf in the selected payload
boundary. The `remove` action requires at least one selector.

## Path Semantics

### Exact pointers, globs, and container selection

`target_paths` are exact RFC 6901 JSON-pointer matches. `target_path_globs`
adds an opt-in selector language: a whole `*` segment matches exactly one
object key or array index, and a whole `**` segment matches zero or more path
segments. Exact paths and glob paths are combined; a node selected by multiple
selectors is counted and sanitized once per document.

Wildcards are special only when they occupy the complete pointer segment.
Segments such as `content*`, `content**`, and `foo*bar` match those literal
object keys. Predicates and arbitrary JSONPath expressions are not supported.

For example, these selectors cover every message and provider-output text
block without capping the number of array items:

```toml
target_path_globs = [
  "/messages/*/content/*/text",
  "/output/*/content/*/text",
]
```

For example, `/messages/*/content` matches both
`/messages/A/content` and `/messages/B/content`. If either `content` value is
an array or object, `redact` does not redact its descendant strings: it acts
only on selected string values. For content parts such as
`{"messages":[{"content":[{"text":"alice@example.com"}]}]}`, use
`/messages/*/content/*/text` to select the `text` string. The same glob also
matches a `text` key in an object one level below `content`.

A single `*` does not cross path segments: `/output/*` selects only direct
children of `output`, not `/output/0/content/0/text`. Use
`/output/*/content/*/text` for that specific shape, or `/output/**/text` when
the nesting depth can vary.

Relay validates every configured path as RFC 6901 syntax before activating the
component. The empty string selects the document root. Every other pointer must
start with `/`, and `~` escapes must use `~0` for `~` or `~1` for `/`. Invalid
pointers prevent activation instead of silently leaving the intended values
unsanitized.

### Recursive globs

Use `**` when the same field can appear at different depths within a payload.
It matches zero or more complete segments, including object keys and array
indices. The entire path must still match the selector:

| Selector            | Example Matches                                               |
| ------------------- | ------------------------------------------------------------- |
| `/messages/*/text`  | `/messages/0/text`.                                           |
| `/messages/**/text` | `/messages/text`, `/messages/0/content/0/text`.               |
| `/**/email`         | `/email`, `/customer/contact/email`.                          |
| `/messages/**`      | `/messages` and every descendant.                             |
| `/messages/*/**`    | Every descendant of `/messages`; excludes `/messages` itself. |

`/messages/**/text` does not select `/other/text` or
`/messages/0/text/child`: the path must start with `messages` and end with `text`.

Use the narrowest subtree and field name that meet your policy. For example,
`/messages/**/text` selects `text` values at varying depths without selecting
fields outside `messages`. It does not select a plain-string `content` field;
use `/messages/**/content` for that field. LLM selectors still refer to the
active codec's representation, as described in
[Payload Boundaries](#payload-boundaries-and-event-publication).

> **Warning**
>
> `**` can match zero segments. With `action = "remove"`, `/messages/**`
> removes the `messages` field itself, including the container and all its
> contents. Use `/messages/*/**` to keep that container while removing its
> children. On ordinary JSON payloads, removed array elements become `null`.
>
> `/**` selects the document root and every descendant. With `remove`, an ordinary
> JSON payload becomes `null`; with a string action, every string leaf is selected.
> For broad trajectory removal that preserves supported analytics, use the
> [trajectory_context preset](#trajectory-context-preset).

Refer to the **Recursive content** example in
[Targeting and Composition](/nemo/relay/dev/configure-plugins/pii-redaction/configuration#targeting-and-composition)
for a complete configuration and before/after payloads.

### Literal wildcard keys and migration

`target_paths` always treats `*` and `**` as literal object keys. Use
`target_paths = ["/metadata/**"]` to select the field named `**` under
`metadata`; use `target_path_globs = ["/metadata/**"]` to select `metadata`
and every descendant.

Earlier versions treated a whole `**` segment in `target_path_globs` as a
literal key. **This behavior has changed.** Before upgrading, move selectors
intended to match literal `**` keys to `target_paths`. If a selector combines
wildcard segments with a literal `**` key, enumerate the required concrete
pointers in `target_paths`; glob selectors have no escape for a literal `**`
segment. The RFC 6901 escapes `~0` and `~1` still represent `~` and `/`.

## Trajectory Context Preset

### What the trajectory preset removes and preserves

`trajectory_context` is a fail-closed policy for metrics-oriented trajectory
export. It discards original LLM request and response payloads and generates
minimal provider-shaped projections from sanitized normalized annotations.
For supported built-in codecs, the published LLM request or response retains
supported structure and analytics, including message roles, tool names, and
tool-call correlation IDs. Conversational text is replaced with `[REDACTED]` by
default. Tool arguments and parameter schemas are emptied. Refer to the
[Trajectory removal worked example](/nemo/relay/dev/configure-plugins/pii-redaction/configuration#targeting-and-composition)
for the request, response, and separate tool events before and after sanitization.

A configured legacy codec can also provide this projection when no active codec
is present. Custom runtime and opaque codec identities publish `{}` instead,
even when their payload resembles a supported provider's JSON shape.

Unknown fields and unstructured payloads are discarded or emptied. This includes
provider extensions, additional choices or candidates, headers, provider-native
values, generic event data, and category-profile extras. Present opaque typed
JSON values become `{}`. Metadata retains only explicitly supported fields,
including recognized scope status values and valid `nemo_relay.log.severity`
values for marks. Unknown custom-mark payloads are emptied by default;
`custom_mark_payload_policy = "preserve"` explicitly retains them.

Scope metadata retains only recognized `otel.status_code` values (`OK`, `ERROR`,
`UNSET`) and, for `ERROR`, Relay's fixed `error.type` classifications or `_OTHER`.
Unrecognized error types are discarded; the `full` and `gen_ai` OpenTelemetry
projections use `_OTHER` for errors without a classification. Free-form status
descriptions, exception types, and other metadata remain discarded, so redaction
preserves success/error reporting without exporting error messages or application
exception names.

Typed LLM annotations retain message roles and content-part kinds; provider,
model, and tool names; request tuning settings; finish reasons; token and cache
usage; and normalized cost amounts, currency, and source classification.
Conversational content and other application identifiers use the configured
replacement, which defaults to `[REDACTED]`. Opaque typed fields become `{}`;
approved typed numeric and boolean analytics remain available. Routing metadata,
pricing provenance, optimization contribution evidence, and unknown fields are
discarded. Relay lifecycle identifiers remain unchanged.

Tool-call correlation IDs remain unchanged across LLM messages and tool
lifecycle events so exporters can associate results with the correct calls.
Response IDs and other application identifiers still use the replacement marker.

Provider, model, tool, and metric names are intentional free-form exceptions.
Do not place user content, secrets, or personal identifiers in these fields.
All redacted application identifiers collapse to the same marker and are not
suitable correlation keys. To retain an unknown custom mark unchanged, set
`custom_mark_payload_policy = "preserve"` explicitly and only for a trusted
producer.

## Typed Metric Marks

### Typed metric sanitization and allowed dimensions

Relay recognizes marks that use its `nemo.relay.metric_measurements` schema as
typed metric envelopes. PII sanitization preserves the measurement name, kind,
value type, numeric value, and histogram boundaries so the OpenTelemetry metric
exporter can validate and export the mark.

The plugin sanitizes optional text without invalidating the envelope:

* With neither `target_paths` nor `target_path_globs`, measurement descriptions and string-valued metric
  attributes follow the configured built-in action.
* With target selectors, those fields are sanitized only when their
  payload-relative JSON Pointer matches, such as
  `/measurements/0/description` or `/measurements/0/attributes/owner`.
* With `action = "remove"`, targeting a whole string-array attribute removes
  that attribute. Targeting an individual element removes that element and
  compacts the remaining array; Relay cannot use a `null` placeholder because
  typed OpenTelemetry metric attributes must remain valid homogeneous arrays.
* Outside `trajectory_context`, numeric and boolean metric attributes remain
  analytical telemetry.

The `trajectory_context` preset replaces descriptions with its configured
replacement while retaining required measurement fields. It drops every
metric attribute except an exact string value admitted by
`metric_string_attribute_allowlist`. That preset does not accept target
selectors. Its `custom_mark_payload_policy` applies only to opaque marks in the
`custom` category; it does not control Relay typed metric envelopes.

Matching is case-sensitive and requires both the attribute name and value. A
string-array attribute is retained only when every element is allowed; one
unexpected value drops the whole attribute. Values under an unlisted key,
unexpected values under a listed key, and numeric or boolean attributes are
dropped. This allowlist does not apply to descriptions, mark metadata, category
profiles, or non-metric marks. Configure fixed constants or bounded enums only;
do not list free-form identifiers or user-provided text.

On every mark, the preset preserves the reserved `nemo_relay.log.severity`
metadata field in canonical form when it contains a supported severity. For
opaque custom marks using `redact_all_leaves`, unsupported severity values and
all other opaque fields are discarded.

Refer to the **Metric dimensions** example in [Targeting and Composition](/nemo/relay/dev/configure-plugins/pii-redaction/configuration#targeting-and-composition) for a complete configuration and payload comparison.

## Selection Accounting

Enable [runtime diagnostics](/nemo/relay/dev/reference/diagnostics) to distinguish a valid
selector from a policy that changed content. Refer to
[Verify the Policy](/nemo/relay/dev/configure-plugins/pii-redaction/configuration#verify-the-policy)
for the diagnostic fields and setup.

With `action = "remove"`, a child string inside a selected container can count
as selected even though removing the container prevents the sanitizer from
visiting the child. Only the removed container counts as affected. This also
applies when one recursive selector matches both nodes. For example,
`/messages/**` applied to `{"messages":{"text":"alice@example.com"}}` with
`remove` reports `matched_paths=2`, `eligible_string_leaves=1`, and
`affected_values=1`.

For recursive `remove` policies, enabling match diagnostics adds a
selection-counting pass before removal. A selected string can also remain
unchanged when its detector finds nothing.

With neither selector list configured, `matched_paths` counts all visited JSON
nodes and `eligible_string_leaves` counts the eligible string nodes.

The `trajectory_context` preset uses its semantic sanitization path rather than
this selector diagnostic. Use the preset when the goal is broad payload removal;
refer to [Trajectory Context Preset](#trajectory-context-preset).