Skip to content

PII redaction

Redaction happens at the runner’s edge, after the connector answers and before the result crosses the boundary. A single enforcement point: not scattered, not repeated on the other side.

It is active when RUNNER_CONNECTORS=real. On the synthetic path the data carries no PII, so only the declared-field layer runs.

Masks the paths the connector’s manifest declares in sensitiveFields. It is pure dot-path, by design.

An operation-keyed pass, for the shapes a regex cannot see: arbitrary property-bag values and raw end-user identifiers.

It covers exactly five Amplitude operations:

Operation Masks Redacts
analytics.searchUser userId properties
analytics.getUserActivity none properties, deviceId
replays.listSessionReplays userId none
replays.getSessionReplay userId none
replays.getSessionReplayEvents none properties

In property bags, keys are preserved and values are redacted, and the shape of the data stays legible for diagnosis, the content does not.

For every other operation, this layer is a no-op.

A final, content-aware regex pass over whatever strings are left: email, CPF, IPv4, credit card with Luhn validation, and phone numbers, plus a set of sensitive field names.

It is not dot-path driven, so it complements the two layers above and acts as defense in depth.

What else leaves the Agent, and how it is redacted

Section titled “What else leaves the Agent, and how it is redacted”

An operation’s result is not the only data that crosses the boundary. These are all the channels, and what each one goes through:

Channel Redaction
Operation result and error All three layers.
Credential probe detail, which reaches the Fleet The same three layers, through the same redactor, before the message is truncated.
Collector warnings from a capture that succeeded, and the reason a capture failed (the collector’s and aggregator’s stderr), which reach the Profiling page The three layers, through the same redactor. Warnings are cut to 400 characters and 20 entries after redaction.
Learned state (topology, DNS records, load balancer routes) The content engine, on its way out of the Agent, with one exception for IPs, described below.
Profiler signature None. It carries code symbols and network peers, and what leaves is listed under Profiler.

An infrastructure IP is not PII; an IP in free text is. In a result, the content layer masks every IPv4, because in arbitrary text it cannot tell a customer’s address from a server’s. The learned state can: a DNS record is configuration of your infrastructure, and the IP inside it (an A record target, a load balancer frontend, an SPF ip4:) is the address of a resource of yours, and it is what answers “which service does this address land on?”. So an IP passes only inside the name and target of a DNS record, and in any other learned-state field it is masked as in a result. It is the same line the content layer already draws for CIDR blocks. Emails, CPFs, cards and phone numbers are redacted in every field, including inside a DNS record: a DMARC record’s rua=mailto: leaves as [EMAIL].

The trap: sensitiveFields doesn’t cross arrays

Section titled “The trap: sensitiveFields doesn’t cross arrays”

This is the trap that most often produces a false guarantee, so let’s be direct.

FieldRedactor is pure dot-path. Declaring in the manifest a field that only exists inside an array (say a field of a RumErrorSample[]) redacts nothing. And that is worse than not declaring it, because it produces the appearance of a guarantee that isn’t there.

For that case, the rule belongs in redactStructured, which is operation-keyed and does cross arrays.

The kill switch, and what it doesn’t disable

Section titled “The kill switch, and what it doesn’t disable”
Terminal window
ROOTPILOT_PII_REDACTION=off

There is a legacy alias, INFRAINTEL_PII_REDACTION. When both are present, the new name wins.

Be precise about its reach:

Layer With ROOTPILOT_PII_REDACTION=off
1. Declared field still runs
2. Structural disabled
3. Content disabled

The kill switch applies to every channel in the table above that goes through the content layer, including the learned state.

With the layers disabled there is no redaction to audit, so the trace is not an audit line. There are three others:

  • In production, the Agent refuses to boot. With off, RUNNER_CONNECTORS=real and NODE_ENV=production (the image’s value), boot fails naming the variable.
  • Otherwise, it boots with a warning in the boot log.
  • And it announces the state to the control plane when it connects, and the Fleet shows the Agent as running without redaction.

Divergent classification: the edge exposes, it doesn’t rewrite

Section titled “Divergent classification: the edge exposes, it doesn’t rewrite”

One case that shows up in practice and is worth understanding before filing it as a bug.

Datadog sends all stderr to status:error. So a search for errors can return [INFO] Worker exiting. The source classified it as an error, the text says otherwise.

The edge exposes the divergence rather than rewriting the field: alongside the source’s level, it emits textLevel and levelMismatch. The principle is that the source’s classification is a fact about the source, and silently correcting it would erase information.

What Where
The three-layer composition apps/runner/src/redact/pii-redactor.ts
Declared-field layer apps/runner/src/redact/field-redactor.ts
Structural layer, per operation packages/core-edge/src/policy/structured-redactor.ts
Content engine packages/core-edge/src/policy/pii-redactor.ts
Kill switch packages/core-edge/src/policy/kill-switch.ts, and the boot refusal in apps/runner/src/boot.ts
Learned state leaving the Agent apps/runner/src/learned/redact.ts
Credential probe detail apps/runner/src/connectors/health.ts
Collector warnings and failed-capture reason apps/runner/src/profiling/push.ts