PII redaction
ProA guardrail card where personal data is redacted before the prompt reaches the model. Scan marks each name, email, phone number and card with its type and swaps it for a placeholder like [EMAIL_1]. The safe prompt travels to Claude, and the reply comes back with the original values put back for the person.
- NameNot scanned
- EmailNot scanned
- PhoneNot scanned
- CardNot scanned
The reply shows here, with your details put back.
- Explaining or proving a privacy guardrail in an AI product: support assistants, health or finance copilots, enterprise chat.
- A settings or onboarding screen where admins see what the PII filter does before they turn it on.
- A playground where people paste text and see exactly what would reach the model.
- Use ai-chat or ai-composer for the conversation itself; redact behind the scenes and show this card only where the guardrail needs explaining.
- Do not rely on the built-in detector for compliance; pass a real DLP service through detect.
- Use tool-call to show an arbitrary tool step in a transcript.
Installation
Pro source and install commands unlock with a Pro plan.
import { PiiRedaction, detectPii, redactPii, restorePii } from "@/components/arc/pii-redaction/pii-redaction"; export function SupportGuard({ user }) { return ( <PiiRedaction autoPlay={false} defaultMessage="" names={[user.name]} model={{ name: "Claude Sonnet 5.5" }} detect={async (text, signal) => { const res = await fetch("/api/dlp/scan", { method: "POST", body: text, signal }); return res.json(); // [{ type: "email", start: 74, end: 100 }, ...] }} send={async (prompt, signal) => { const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ prompt }), signal }); const { text } = await res.json(); return text; // may contain [NAME_1], [EMAIL_1], restored by the component }} onRedact={({ entities }) => audit.log("pii.redacted", entities.map(e => e.type))} /> );} // Server side, without the card:const { prompt, entities } = redactPii(message, detectPii(message, { names: [user.name] }));const reply = restorePii(await model.complete(prompt), entities); // With no props it plays the simulated support conversation.<PiiRedaction />API reference
2 parts. The first is the root.
PiiRedaction
A guardrail card that shows personal data being redacted before a prompt reaches the model. Scan marks each name, email, phone number and card in the message with its type, then swaps each one for a numbered placeholder such as [EMAIL_1] in a single crossfade. Send carries the safe prompt along a small flow (you, the PII guard, the model), the reply streams back in quick chunks with its placeholders, and the guard restores the original values for the person. Show original flips the message between the safe prompt and what was typed. The message is editable before a scan, so people can paste their own text. With no props it plays a simulated support conversation with Claude Sonnet 5.5 on a loop while on screen.
defaultMessagestringpiiSampleMessageThe message to check. The sample is a double charge support request from Sofia Ramirez with a name, a Luhn-valid test card, an email and a phone number.editablebooleantrueLets people edit the message before a scan. Cmd or Ctrl plus Enter scans from the field.namesstring[]["Sofia Ramirez"]Names the built-in detector always flags, on top of introductions like "I'm", "this is" and "my name is". Pass the signed-in person's name and their contacts.detect(text: string, signal: AbortSignal) => PiiMatch[] | Promise<PiiMatch[]>–Replaces the built-in detector, for example with your DLP or NER service. Return spans as { type: "name" | "email" | "phone" | "card", start, end } with an exclusive end. The signal aborts when the person presses Stop or Reset.send(prompt: string, signal: AbortSignal) => Promise<string>–Sends the redacted prompt to your model and resolves with its reply, placeholders included. The component restores them. Without it the reply is simulated and the status says so. A rejection shows the error message with Retry.model{ name: string; mark?: ReactNode }{ name: "Claude Sonnet 5.5" }The model the safe prompt goes to, on the flow and above the reply. The mark draws at 16px; without one the Claude mark shows in its brand color.autoPlaybooleantruePlays scan, send and restore on a loop while at least a quarter of the card is on screen. Stops for good as soon as someone edits, toggles or presses a button. Off with reduced motion.showOriginalboolean–Shows the original message instead of the safe prompt once it is redacted. Pair with onShowOriginalChange.defaultShowOriginalbooleanfalseInitial Show original state when uncontrolled.onShowOriginalChange(show: boolean) => void–Called when the Show original switch changes.onRedact(result: { prompt: string; entities: PiiEntity[] }) => void–Called once the message is redacted, with the safe prompt and every entity (type, start, end, value, token). Log the entities server side, never the prompt alongside them.onReply(reply: { raw: string; restored: string }) => void–Called when the reply is restored, with the model's raw reply and the text the person sees.onPhaseChange(phase: PiiRedactionPhase) => void–Called on every phase change: idle, scanning, redacted, sending, thinking, streaming, restoring, done or error.classNamestring–Class on the root card.detectPii, redactPii, restorePii
The pure helpers behind the card, safe on the server. detectPii(text, { names }) finds emails, card numbers (13 to 19 digits that pass Luhn), phone numbers (7 to 15 digits) and names. redactPii(text, matches) returns { prompt, entities } and gives a repeated value the same token. restorePii(reply, entities) puts the values back and leaves unknown tokens as they are.
textstring–The message to scan or redact.matchesPiiMatch[]–Spans from detectPii or your own detector. Overlapping spans after the first are skipped.entitiesPiiEntity[]–The entities from redactPii, used to restore a reply.- Tab
- Moves through the message field (before a scan), the Show original switch once there is a safe prompt, Reset when it shows, and the primary action.
- Cmd+Enter, Ctrl+Enter
- Scans the message from the field. Focus moves to the primary action, which now reads Stop.
- Space, Enter
- Toggles Show original, or activates Scan, Stop, Send, Run again, Retry or Reset.
- A polite status region announces each phase ("Scanning for personal data", "4 items redacted", "Reply restored") and, when the reply is simulated, adds "Simulated, nothing was sent".
- Status never relies on color: every phase pairs an icon with a short label, each type cell says Name, Email, Phone or Card with its token or None found, and marks in the original message carry labels such as "Email, redacted as EMAIL_1".
- Scan, Stop, Send, Run again and Retry are one button whose label changes, so keyboard focus stays put through the whole flow. Reset returns focus to the message field.
- Only the visible text layer is exposed to assistive tech; the hidden layers that keep the height steady are aria-hidden.
- The flow strip is decorative and hidden from screen readers, since the status region already narrates each hop.
- The Show original label is a real label for the switch, and the switch is disabled with a muted label until there is a safe prompt to compare.
- Scan highlights each entity in turn, 120ms apart, with a 160ms tint in its type color while the matching type cell crossfades from Checking to its token.
- The swap to placeholders is a single crossfade between two stacked text layers (old out in 100ms, new in over 160ms), so no words slide or rewrap mid animation and the height never changes.
- Each hop of the flow is one dot traveling the connector in 460ms on an ease in out curve; the active node's label shimmers while it works.
- The reply streams three to six words every 34ms, each chunk only fading in, then crossfades once to the restored text.
- Labels, statuses and the Reset button crossfade in place; nothing bounces and nothing changes size between states.
- Reduced motion drops the travel, the stagger, the streaming and the autoplay loop, and keeps every state, label and announcement.
- The card fills its container up to 640px. Below 520px the four type cells fall into two rows of two.
- Below 400px the card tightens its gaps and hides the Simulated tag (the status region still announces it); the model name truncates with an ellipsis in the flow.
- Text layers stack in one grid cell and the reply reserves the sample reply's height, so nothing jumps at 360px, 390px or desktop widths.
- Detection is a handful of regular expressions and a Luhn check, linear in the message length.
- The loop runs only while the card is at least a quarter on screen (IntersectionObserver) and stops for good on the first interaction.
- Highlights and layer swaps are CSS transitions on opacity and background color; the dot animates a transform; every pending step aborts through one AbortController.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Redact on the server in production: run detectPii and redactPii (or your DLP) before the prompt leaves your backend, keep entities in the request scope, and call restorePii on the reply. Use this card to show people what happens, or as the UI over that pipeline through detect and send.
- The built-in detector is a baseline. Names beyond the names list and simple introductions need an NER model or a DLP service passed through detect.
- Keep the same token for a repeated value within one conversation so the model can refer back to it; redactPii does this per call, so persist the entity map across turns if you need it.
- Instruct the model to keep placeholders verbatim (for example "Refer to [EMAIL_1] exactly as written") so restorePii can put the values back.
- Set autoPlay={false} in product UI and pass defaultMessage="" for a blank field.
The full library index for assistants is at /llms.txt.
