redact-pii · git:20260614.b6a05c8 · 2026-06-14 · sha256 200d22c5e671b89e
redact-pii git:20260614.b6a05c8A
Immutable. This exact content is served forever at /api/v1/blob/200d22c5e671b89e.
---
name: redact-pii
description: Scrub personal data out of content before it crosses a trust boundary, and refuse to pass content that cannot be scrubbed with confidence.
runx:
category: security
---
# Redact PII
Decide whether a piece of content is safe to let out, and make it safe when it can be.
## What this skill does
`redact-pii` is a boundary guard with a pass/hold verdict. Content is about to
cross a boundary: a log line headed to a third-party aggregator, a support
transcript pasted into a model prompt, a row exported to a partner, a draft a
teammate will read. The skill answers one question before the crossing. Does
this carry personal data, and if it does, can that data be removed without
destroying what the content is for.
It detects personal data, removes it under a chosen mode, and returns a verdict
the caller can branch on alongside the scrubbed content's digest. The verdict is
the gate: `ready` authorizes a pass, `needs_review` holds for a human, `blocked`
refuses the crossing. The output is a gate decision, not a tidied draft.
## When to use this skill
- An agent is about to send content past a trust boundary and must prove it is
clean first.
- A pipeline step needs a machine-checkable pass/hold gate on personal data
before export, logging, prompt injection, or sharing.
- A reviewer wants the detection evidence, classes and spans, without ever
handling the raw values.
## When not to use this skill
- To classify or label content for analytics. Use a classifier; this skill
holds a gate. A classifier tells you what is in the text and stops; this skill
emits a forward-looking pass/hold verdict that gates the crossing.
- To rewrite, summarize, or improve content. Removing personal data is its only
edit.
- To redact secrets and credentials specifically. Personal data is the target
here; a credential-bound run belongs with a vault or secret-handling skill
that returns a bound handle.
- To move the content anywhere. It has no egress by design; the boundary
crossing belongs to the caller.
Nearest neighbors are `receipt-auditor` and `least-privilege-auditor`. Those
read a sealed receipt after a run to judge authority. `redact-pii` runs before
the fact, on content rather than on a receipt.
## Procedure
1. **Set policy.** Resolve the target classes from `classes` and the treatment
from `mode` (`redact`, `tokenize`, or `block`). With no classes given,
default to a broad personal-data set: names, emails, phone numbers, postal
addresses, government and tax identifiers, payment instrument numbers,
account and record identifiers, precise geolocation, and dates of birth.
`locale` tunes the identifier and address grammars.
2. **Detect.** Scan the content for each target class. Record every hit as a
span (offsets, not the matched text) with a class label and a confidence.
3. **Treat.** Apply the mode. `redact` removes the span and leaves a class
placeholder. `tokenize` replaces it with a stable opaque token so structure
survives without the value. `block` marks the content as not passable and
skips emitting a usable residual.
4. **Score residual risk.** Weigh what could still identify a person after
treatment: low-confidence misses, quasi-identifiers that combine, free text
that resists span detection. Set `residual_risk.level` and the reason.
5. **Decide the gate.** Pick the verdict from the residual score. Low risk with
confident detections clears to `ready`. Uncertainty that could mask a leak
holds at `needs_review`. Residual risk above threshold, `block` mode, or
scrubbing that would gut the content's meaning forces `blocked`.
6. **Seal.** Return the report, the digest of the scrubbed content, and the
policy that governed the pass. The receipt carries the verdict, the detection
summary (counts and classes, no values), the policy, and the residual digest.
The receipt is safe to retain and audit because nothing in it reconstructs
the personal data it found.
## Edge cases and stop conditions
- **No content input:** return `needs_agent`; there is nothing to inspect.
- **Uncertain detections:** return `needs_review`. Detections uncertain enough
to mask a leak push the verdict to hold, never silently through.
- **Residual risk above threshold:** return `blocked`. `ready` is the only
verdict that authorizes a pass, and it requires residual risk below the
configured threshold.
- **Scrubbing destroys meaning:** return `blocked` when quasi-identifiers are
interleaved with the content's substance and removing them strips the meaning.
- **`block` mode:** the content is marked not passable and no usable residual is
emitted.
- **Raw value would be needed to be useful:** a report that would have to quote
the personal data is a report that failed; return `blocked` instead.
Scope is `content:read` only. No `net:*`, no `repo:write`, no store. The skill
inspects the supplied content and returns a report; it never moves the content
anywhere. The gate authority lives in the verdict: `blocked` is a hard refusal
to pass, `needs_review` requires a human or a stricter downstream skill before
the crossing, and `ready` is the only grant that lets the scrubbed content out.
Secrets, PII, and raw matched substrings never appear in the report or the
receipt. The `detected` array carries class and span offsets, never the value at
that span. The scrubbed content is referenced by `redacted_digest`, never
inlined. If the caller needs the scrubbed content itself, the runner returns it
out of band, keyed by `redacted_digest`, never folded into the auditable proof.
## Output schema
```yaml
redaction_report:
decision: ready | needs_review | blocked
detected:
- class: string # personal-data class label, no raw value
span: [int, int] # offsets into the input, not the matched text
confidence: number # confidence in [0,1]
redacted_digest: string # digest of the scrubbed content, never inlined; null under block
residual_risk:
level: low | medium | high
reason: string # the concrete residual concern, not a generic disclaimer
policy:
classes: array # target classes that governed this pass
mode: redact | tokenize | block
```
The runner may also return the scrubbed content out of band for the caller to
forward; it is keyed by `redacted_digest` and is never part of the auditable
report or receipt (`runx.redaction.v1`).
## Worked example
Input: the line "Customer Dana Whitfield wrote in from dana.w@example.com about
order #44120; callback number on file is 415-555-0188." with `classes: [name,
email, phone]`, `mode: redact`, `locale: en-US`.
Output: `decision: ready`. Three personal spans are detected and removed at high
confidence: the name, the email, and the phone number, each named by class and
offset with no value emitted. The order id is a record reference, not a personal
identifier, so it stays. `residual_risk.level: low`, and the scrubbed content is
referenced only by `redacted_digest`. The verdict grants the pass; the receipt
seals with the detection summary and policy.
## Inputs
- `content` (required): the content to inspect and scrub.
- `classes` (optional): JSON list of personal-data classes to target. Defaults
to a broad personal-data set when omitted.
- `mode` (optional): `redact`, `tokenize`, or `block`. Defaults to `redact`.
- `locale` (optional): locale that tunes identifier and address grammars, for
example `en-US` or `de-DE`.
- `operator_context` (optional): boundary context, threshold posture, or extra
constraints that focus the pass.