PII redaction
PII redaction scrubs personally identifiable information — emails, phone numbers, card numbers, government IDs, secrets — out of your traces before Neens stores anything. Turn it on per agent, choose how aggressive it is (tag, mask, or tokenize), and the raw values never land in any store, queue, or log. The same engine also sanitizes what leaves Neens: prompts sent to your configured LLM connection for judging, clustering, and enrichment are scrubbed too.
This is the PII Redaction tab on Data Governance — one of four tabs alongside Overview, Retention, and Erasure. See Data retention and Erasure for the others.
At a glance
| Where | Data Governance in the left nav (Admin group) → PII Redaction tab |
| Key API routes | GET/PUT /redaction/settings, POST /redaction/preview |
| Who can use it | Company admins only — the same role that manages retention. Reading, changing, and previewing are all admin-gated |
| Scope | Per agent, with an optional per-request header override (X-Neens-Pii-Redaction) |
| Modes | off · tag · mask · tokenize |
| When it runs | At the ingest edge, before anything is durably written |
Choose a mode
Given this span input:
Customer [email protected] called from 415-555-0187 about card 4111 1111 1111 1111.| Mode | What is stored | Use it for |
|---|---|---|
off | The text, unchanged. Nothing is scanned. | The default — zero cost when you don’t need redaction. |
tag | The text, unchanged — but Neens counts what it would redact and stamps the counts on the session. | Measuring your PII exposure before enforcing anything. |
mask | Customer [PII:email] called from [PII:phone] about card [PII:credit_card]. | Maximum privacy: the value is gone, only its type remains. |
tokenize | Customer [PII:email:9f2ab7c4d1e0] called from [PII:phone:3c1d0e5f7a2b] about card [PII:credit_card:b04e88a1c72f]. | Privacy plus correlation — the same value always produces the same token (see below). |
Placeholders are stable formats: [PII:<type>] for mask, [PII:<type>:<12-hex>] for tokenize.
Redaction is idempotent — a placeholder that is already in the text is never re-matched or
double-redacted, so re-sending an already-redacted payload is safe.
Tokenize: correlation without the raw value
In tokenize mode each detected value is replaced by a short, deterministic token derived from a
keyed hash that is unique to your agent. That means:
- The same customer email always maps to the same token within an agent — you can still filter, group, and cluster by “this same person showed up in 14 failing sessions” without Neens ever storing who that person is.
- The raw value is never stored and cannot be recovered from the token.
- Tokens don’t correlate across agents — the same email produces a different token in a different agent, so two agents’ data can’t be joined on a token.
Enable redaction
Open the PII Redaction tab
Go to Data Governance in the left nav and open the PII Redaction tab. (If you don’t see the page, you need the company admin role.)
Pick a mode
Select tag, mask, or tokenize. Redaction is off by default — nothing is scanned until
you choose a mode. A common rollout: start with tag to see counts appear on new sessions, then
switch to mask or tokenize once the preview looks right.
Try it in the live preview
Paste a representative payload into the live preview box on the tab. It shows the redacted result and a count per entity type, using your current allowlist and custom patterns — before you save anything. The preview text is never persisted.
Optionally check Enforced
Enforced makes your agent setting final: the per-request X-Neens-Pii-Redaction header is
ignored, so no client can weaken (or change) redaction on individual requests. See
What “Enforced” means.
Save
Changes take effect on new ingest within about half a minute. Redaction applies from this point forward — data ingested before you enabled it is not retroactively redacted (use erasure for data that must be removed).
The same, over the API
curl -X PUT https://your-neens-host/api/redaction/settings \
-H "Authorization: Bearer $NEENS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "tokenize",
"enforced": true,
"allowlist": ["[email protected]"],
"customPatterns": [
{"type": "employee_id", "pattern": "EMP-\\d{6}"}
]
}'GET /redaction/settings returns the saved settings plus the effective policy for the
agent. Settings changes are recorded in the audit log. Whether
you save from the tab or over the API, a policy change converges across every server in the
fleet within about 30 seconds — each process caches the resolved policy briefly.
Preview an input
The preview endpoint runs the agent’s full policy (allowlist and custom patterns included) over a text you supply and returns what would happen — without storing the text:
curl -X POST https://your-neens-host/api/redaction/preview \
-H "Authorization: Bearer $NEENS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Reach me at [email protected]", "mode": "mask"}'{
"mode": "mask",
"redacted": "Reach me at [PII:email]",
"entities": [{"type": "email", "start": 12, "end": 32}],
"counts": {"email": 1}
}mode in the request is optional — omit it to preview with the agent’s saved mode; the
response’s mode echoes the mode that was actually applied. Input is capped at 64 KB. This
is the same endpoint the tab’s live preview box uses, and like the settings routes it requires
the company admin role.
Per-request override
A client can request a different mode for a single ingest call with the
X-Neens-Pii-Redaction header (off, tag, mask, or tokenize):
curl -X POST "https://<your-neens-host>/ingest/raw" \
-H "Authorization: Bearer nk_live_your_key_here" \
-H "X-Neens-Pii-Redaction: mask" \
-H "Content-Type: application/json" \
-d '{
"session": {"id": "trace-001", "agent_name": "support-agent"},
"spans": [
{"id": "span-1", "name": "llm.call", "kind": "llm",
"input": "Customer email is [email protected]",
"output": "Got it, I will follow up."}
]
}'This is useful when one producer (say, a staging environment replaying real tickets) needs stricter redaction than the agent default, or when you want to trial a mode from one client before flipping the agent setting. An unrecognized header value is ignored, never an error.
What “Enforced” means
Precedence for any single ingest request:
- Enforced agent setting — the agent mode applies; the header is ignored entirely.
- Valid
X-Neens-Pii-Redactionheader — the header’s mode applies. One refinement: when the agent has no saved policy at all, the header is honored only if it does not weaken the deployment-wide default mode (on the orderingoff<tag<mask/tokenize, the last two equally strong) — a client cannot switch off a fleet-wide default with a header. A saved but un-enforced agent setting can still be weakened by the header; check Enforced to close that. - Agent setting — the saved mode applies.
- Otherwise — the deployment’s default mode applies (
offunless your operator changed it).
If redaction is a compliance requirement, check Enforced. Without it, any client holding an
ingest key can send X-Neens-Pii-Redaction: off and land raw values. Enforced closes that door:
the header becomes inert and the agent policy is the only policy.
Allowlist and custom patterns
Both live in the Advanced panel on the PII Redaction tab (and in the same
PUT /redaction/settings body) — kept collapsed by default since most agents never need them:
- Allowlist — values that must never be treated as PII, even when they match a pattern.
An entry matches exactly, or as a case-insensitive substring of the detected value —
which cuts both ways: a short or generic entry (say,
an) can act as a kill switch that suppresses every detection it happens to appear in, so keep entries as specific as the value you mean to allow. Entries shorter than 3 characters are rejected for exactly that reason. Typical entries: your own support address ([email protected]), a documented test card number, your office phone number. Up to 200 entries of 3–256 characters each. - Custom patterns — extra deny patterns of your own, each a
{type, pattern}pair. The type names the placeholder ([PII:employee_id]) and must match[a-z0-9_]{1,64}— lowercase letters, digits, and underscores, up to 64 characters; the pattern is a regular expression. Up to 50 patterns of up to 512 characters each; a pattern that doesn’t compile — or that uses constructs prone to catastrophic backtracking, like nested quantifiers — is rejected when you save, not at ingest time.
What gets detected
Detection is layered: format patterns with checksum validation where a checksum exists (so a random 16-digit order number isn’t mistaken for a card), plus field-name context for values whose format alone is too ambiguous.
| Entity type | Detected by |
|---|---|
email | Pattern |
phone | Pattern — international and US formats; never matched inside a longer digit run |
ssn, itin | Pattern, with known-invalid ranges excluded |
credit_card | Pattern + Luhn checksum — 13–19 digits with or without separators |
iban | Pattern + mod-97 checksum |
us_routing | Pattern + ABA checksum |
us_bank_account | Field name only (an account number has no distinguishing format) |
ipv4, ipv6, mac_address | Pattern — real octet ranges, so version strings don’t match |
date_of_birth | Date pattern only when nearby text or the field name mentions birth/DOB |
person_name | Field name (name, first_name, customer_name, patient_name, …) — plus free-text names when the NER capability is active |
street_address, zip | Street-address pattern; ZIP only adjacent to address context |
geo_coord | Latitude/longitude pairs in lat/lng-named fields |
passport, drivers_license | Field name only (formats vary too much for a safe bare pattern) |
| Secrets & API keys | Patterns for JWTs, bearer tokens, common API-key prefixes, URL-embedded credentials, and password=-style assignments |
Structured payloads are scanned recursively — every string inside nested objects and arrays.
A value under a sensitive field name (email, ssn, address, …) is redacted wholesale as that
type, regardless of its format. Pseudonymous identifiers your integration depends on —
session_id, trace_id, span_id, end_user_id — are deliberately not treated as PII.
What egress sanitization covers
Ingest redaction protects data at rest — what Neens stores. Egress sanitization protects
data in motion — anything Neens sends out of your deployment. The two are configured
separately: many teams keep ingest off (full-fidelity traces for their own debugging) but set
egress to mask or tokenize, so raw values still never leave for a third party. You set this in
Data Governance → PII redaction as the egress mode — leave it blank to reuse your ingest
mode, or pick a distinct off · tag · mask · tokenize.
Egress sanitization runs on every channel that carries your trace content off the deployment:
- LLM prompts — everything sent to your LLM connection: judge prompts, cluster labeling, root-cause analysis, enrichments, and the assistant. A model never sees a raw value, so it can’t echo one back into a score or summary.
- Fix bundles / coding-agent handoff — the failing-example excerpts and every heading, label, and diagnosis Neens wrote from those traces (the fix title, the failure-mode summary and root-cause narrative, the cluster label and description, and the drafted proof-eval checks) embedded in a fix bundle are scrubbed before the bundle leaves Neens, through every channel it can leave by: the Copy fix bundle button, a webhook handoff to your coding agent, and the headless CLI driver. This closes a gap where an agent relying on egress-only sanitization (raw ingest, scrubbed LLM prompts) could previously have had raw excerpts ride out inside a fix bundle.
The typed code fix itself — the before/after diff Neens proposes for your repository — is intentionally not scrubbed. That is your own code and must apply cleanly; redacting it would corrupt the patch. Everything else that Neens derived from your traces — failing-example inputs, outputs, and errors, and the narrative Neens wrote about them (the fix title, the failure summary and root-cause text, the cluster label and description, and the drafted proof checks) — passes through the egress engine.
Example: a failing example inside a fix bundle
A failing-example excerpt that was ingested raw looks like this:
Input: Refund request from [email protected] for card 4111 1111 1111 1111
Error: card on file for [email protected] did not matchWith egress mask, the excerpt embedded in the bundle carries only the entity types:
Input: Refund request from [PII:email] for card [PII:credit_card]
Error: card on file for [PII:email] did not matchWith egress tokenize, the same values become stable per-agent tokens — so both mentions of the
same email are still visibly the same person to whoever reads the bundle, without the address
itself ever leaving Neens:
Input: Refund request from [PII:email:9f2ab7c4d1e0] for card [PII:credit_card:b04e88a1c72f]
Error: card on file for [PII:email:9f2ab7c4d1e0] did not matchEgress sanitization fails closed. If Neens can’t resolve your agent’s redaction policy, it refuses to emit the fix bundle (and refuses to hand content to an LLM) rather than send raw content on a guess — you’ll see the bundle fail to build instead of leaking.
How it works
- Redaction happens at the edge, before anything durable. The payload is scrubbed before it
reaches the ingest queue, the durable staging buffer, or the dead-letter queue — so a raw value
never exists in any store, not even transiently in a failed batch. This covers every ingest
path:
/ingest/*, OTLP/v1/traces, pulled inlet connectors, and pre-production run capture. - Counts are stamped on the session. A session whose payload had anything detected carries a
compact per-type count in its metadata, and the session detail view shows a PII redacted (N)
badge — in
tagmode this is the whole effect, which is what makes it a safe dry run. - Egress is sanitized too — on every outbound channel. The same engine scrubs everything that
leaves your deployment: LLM prompts and fix bundles handed to a coding
agent. By default egress follows your agent’s redaction mode; you can set a separate
egressModeviaPUT /redaction/settings(for example, ingestoffbut egressmask, keeping stored traces raw while guaranteeing raw values still never leave for a provider or a bundle). See What egress sanitization covers. - It’s fast. Detection is a small number of precompiled pattern passes over the text, and the agent policy is cached in-process — no per-request database read on the ingest hot path. That cache is also why a settings change takes up to ~30 seconds to apply.
Detection limitations
Be honest with your DPO about what pattern-based detection can and cannot do:
- It’s pattern- and context-based, not semantic. Formats with checksums (cards, IBANs, routing numbers) are detected with very few false positives. Formatless identifiers (bank accounts, passports, driver’s licenses) are caught only when they sit under a telling field name — a passport number pasted into free prose will pass through.
- Free-text person names and addresses are the hard case.
customer_name: "Jane Doe"is caught by field context; “I spoke with Jane Doe yesterday” is not — a name is just words. The optional NER capability adds a named-entity-recognition model that detects person names and locations in free text, closing most of that gap at some latency cost. If free-text name detection matters for your deployment, ask your Neens contact about enabling it. - Redaction is forward-only. It applies at ingest; data stored before you enabled it stays as it was. That includes work already queued: envelopes staged in the durable ingest buffer or the dead-letter queue before the change replay with the redaction that was in effect when they were captured — there is no retroactive scrub. Use erasure to remove historical data.
- A custom pattern is only as good as its regex. Use the preview endpoint (or the tab’s live preview) against real payload shapes before enforcing.
For a defense-in-depth posture, prefer redacting at the source too — the less PII your agent puts into its own traces, the less there is to detect. Redaction here is the backstop that makes the guarantee, not a reason to log carelessly.
Permissions
All three routes — GET and PUT /redaction/settings (including the Enforced bit, allowlist,
and custom patterns) and POST /redaction/preview — require the company admin role: the same
privacy-admin role that manages data retention and
erasure.
Every settings change is recorded in the audit log. Settings are
per-agent and, like all tenant data, physically isolated per company — one agent’s policy
never affects another’s data.
Related
- Data retention — the scheduled purge, and the rest of the Data Governance page.
- Erasure — removing individual subject data that predates redaction.
- Audit log — the record of who changed redaction settings.
- Send traces — the ingest endpoints the
X-Neens-Pii-Redactionheader rides on. - LLM connections — the provider connection whose egress is sanitized.
- Fix bundles — the coding-agent handoff whose embedded trace excerpts egress sanitization also scrubs.