Redact personal data
Strips personal details out of text before it goes anywhere it does not need them. The FTC's guide for business puts it simply: if there is no legitimate business need for sensitive personal information, don't keep it. A redacted copy is the default input for drafting, summarising and testing.
This is a pattern tool. It is reliable for things with a fixed shape and weak for everything else. Say so every time you hand over.
Inputs
- The text file to redact (plain text, CSV or pasted email body).
- A names list, one per line: the customers, staff and family members the business knows appear in the text. Patterns cannot find names; the list can.
- What the redacted copy is for (a model prompt, a knowledge base article, test data). That decides how strict to be.
If the text comes from the owner's mailbox, read it only through the AHL Action Gateway (gmail.search, gmail.read). The agent holds an agent token, never a password, and stores no credentials.
Steps
-
Run the script on a local copy:
node scripts/redact.mjs examples/ticket.txt --names examples/names.txtIt prints the redacted text, a
---line, and a count per kind. Compare with examples/expected-output.txt. -
Know what it catches (the markers):
[EMAIL]any address shapedx@y.tld[URL]links startinghttp://orhttps://[SSN]123-45-6789shape only[CARD]13 to 19 digits, spaced or dashed, that pass the Luhn check[PHONE]US 10-digit numbers with separators, optional+1[IP]IPv4 addresses[ADDRESS]a house number, up to three capitalised words and a common street suffix, with an optional apartment or unit[DATE]MM/DD/YYYYandYYYY-MM-DD[NAME]only names on the list, whole words, any case
-
Read the output yourself for what it misses, and redact by hand:
- names not on the list, nicknames, initials, handles and signatures
- order, account, policy, member, licence and plate numbers without a fixed shape (the example's order number survives)
- schools, employers, towns, and details that point to one person ("the only nurse on our street", "my son's school")
- dates written in words ("March 3rd"), ages, birthdays
- phone numbers with no separators or outside the US format
- health, money, legal or family details written as ordinary sentences
- anything inside images, PDFs or attachments: the script reads plain text only
For health information, the HIPAA de-identification list in 45 CFR 164.514(b)(2) has eighteen kinds of identifier, including ZIP codes, all dates except the year, and full-face photos; this skill does not meet that standard alone. Use
health-data-guardas well.
-
Decide the leftovers. Replace each by hand with the same marker style (
[ORDER],[SCHOOL],[PLACE]). Keep the meaning: "a customer in [PLACE] reports a leak" still works for drafting. -
Save the redacted copy beside the original with
-redactedin the file name. Never overwrite the original; the business may need it.
Output
The redacted text, the per-kind counts, and a short list headed Not caught by patterns naming each item you redacted by hand and anything you left in on purpose, with the reason.
Edge cases
- Re-identification by combination: town plus job plus date can identify someone even with every name gone. Generalise ("in September", "a tradesperson").
- A card number or a secret appears: redact it, and tell the owner; see
payment-card-guardandsecrets-scan, because the original now needs cleaning too. - Children's details: use
minors-data-guardbefore this skill. - False positives: part numbers that look like phone numbers will be marked. Restore them only if you are sure.
Never
- Send unredacted text to a hosted model or a shared place to "check" it first.
- Claim a file is anonymous because the script found nothing.
- Keep the names list or the original in the agent's memory, logs or prompts after the job.
- Put a password, token or key into the redacted copy, or into any file this skill writes.
Hand over
The person who owns the data reads the redacted copy against the original and the Not caught by patterns list before it leaves the business. They confirm the names list was complete, decide any item marked as left in, and delete working copies when the job is done.
The skill folder
Every file in the folder. Download each one into a folder with the same layout, or copy it.
redact-personal-data/ SKILL.md examples/expected-output.txt examples/names.txt examples/ticket.txt scripts/redact.mjs
SKILL.md
---
name: redact-personal-data
description: Replaces names, emails, phone numbers, street addresses, dates, card numbers, Social Security numbers, IP addresses and links in a text with markers before the text goes to a hosted AI model, a shared document or a knowledge base, and lists what it could not catch. Use when an agent is about to send customer emails, tickets, notes, reviews or transcripts to a model or a shared place, or when an owner asks to anonymise a file for testing.
metadata:
ahl-category: safety
ahl-jobs: support, front-desk, reviews, training, compliance
ahl-workflows: people--1948-it-services-ticket-notes-to-knowledge-base, front--1767-childcare-weekly-family-newsletter, operations--1503-hotel-reviews-to-fix-list, operations--2302-tutoring-session-notes-to-parent-update
ahl-human-check: A person reads every redacted file before it leaves the business, and decides anything the patterns cannot judge.
ahl-proposed-status: approved
ahl-version: 1
ahl-sources: |
FTC — Protecting Personal Information: A Guide for Business — https://www.ftc.gov/business-guidance/resources/protecting-personal-information-guide-business — 2026-10-06
Legal Information Institute (Cornell) — 45 CFR § 164.514 Other requirements relating to uses and disclosures of protected health information — https://www.law.cornell.edu/cfr/text/45/164.514 — 2026-10-06
---
# Redact personal data
Strips personal details out of text before it goes anywhere it does not need them. The FTC's guide for business puts it simply: if there is no legitimate business need for sensitive personal information, don't keep it. A redacted copy is the default input for drafting, summarising and testing.
This is a pattern tool. It is reliable for things with a fixed shape and weak for everything else. Say so every time you hand over.
## Inputs
- The text file to redact (plain text, CSV or pasted email body).
- A names list, one per line: the customers, staff and family members the business knows appear in the text. Patterns cannot find names; the list can.
- What the redacted copy is for (a model prompt, a knowledge base article, test data). That decides how strict to be.
If the text comes from the owner's mailbox, read it only through the AHL Action Gateway (`gmail.search`, `gmail.read`). The agent holds an agent token, never a password, and stores no credentials.
## Steps
1. **Run the script** on a local copy:
```bash
node scripts/redact.mjs examples/ticket.txt --names examples/names.txt
```
It prints the redacted text, a `---` line, and a count per kind. Compare with [examples/expected-output.txt](examples/expected-output.txt).
2. **Know what it catches** (the markers):
- `[EMAIL]` any address shaped `x@y.tld`
- `[URL]` links starting `http://` or `https://`
- `[SSN]` `123-45-6789` shape only
- `[CARD]` 13 to 19 digits, spaced or dashed, that pass the Luhn check
- `[PHONE]` US 10-digit numbers with separators, optional `+1`
- `[IP]` IPv4 addresses
- `[ADDRESS]` a house number, up to three capitalised words and a common street suffix, with an optional apartment or unit
- `[DATE]` `MM/DD/YYYY` and `YYYY-MM-DD`
- `[NAME]` only names on the list, whole words, any case
3. **Read the output yourself** for what it misses, and redact by hand:
- names not on the list, nicknames, initials, handles and signatures
- order, account, policy, member, licence and plate numbers without a fixed shape (the example's order number survives)
- schools, employers, towns, and details that point to one person ("the only nurse on our street", "my son's school")
- dates written in words ("March 3rd"), ages, birthdays
- phone numbers with no separators or outside the US format
- health, money, legal or family details written as ordinary sentences
- anything inside images, PDFs or attachments: the script reads plain text only
For health information, the HIPAA de-identification list in 45 CFR 164.514(b)(2) has eighteen kinds of identifier, including ZIP codes, all dates except the year, and full-face photos; this skill does not meet that standard alone. Use `health-data-guard` as well.
4. **Decide the leftovers.** Replace each by hand with the same marker style (`[ORDER]`, `[SCHOOL]`, `[PLACE]`). Keep the meaning: "a customer in [PLACE] reports a leak" still works for drafting.
5. **Save** the redacted copy beside the original with `-redacted` in the file name. Never overwrite the original; the business may need it.
## Output
The redacted text, the per-kind counts, and a short list headed **Not caught by patterns** naming each item you redacted by hand and anything you left in on purpose, with the reason.
## Edge cases
- **Re-identification by combination**: town plus job plus date can identify someone even with every name gone. Generalise ("in September", "a tradesperson").
- **A card number or a secret appears**: redact it, and tell the owner; see `payment-card-guard` and `secrets-scan`, because the original now needs cleaning too.
- **Children's details**: use `minors-data-guard` before this skill.
- **False positives**: part numbers that look like phone numbers will be marked. Restore them only if you are sure.
## Never
- Send unredacted text to a hosted model or a shared place to "check" it first.
- Claim a file is anonymous because the script found nothing.
- Keep the names list or the original in the agent's memory, logs or prompts after the job.
- Put a password, token or key into the redacted copy, or into any file this skill writes.
## Hand over
The person who owns the data reads the redacted copy against the original and the **Not caught by patterns** list before it leaves the business. They confirm the names list was complete, decide any item marked as left in, and delete working copies when the job is done.
examples/expected-output.txt
Ticket 4471 (invented example, Lakeside Plumbing Supply) From: [NAME] <[EMAIL]> Phone: [PHONE] Received: [DATE] Hi, the faucet I bought on [DATE] leaks. Please send the replacement to [ADDRESS]. My neighbour [NAME] has the same model. I paid with card [CARD] and my order number is 1000234561. My son's school is Hillcrest Elementary, if you need to reach me there. Tracking page: [URL] --- ADDRESS: 1 CARD: 1 DATE: 2 EMAIL: 1 NAME: 2 PHONE: 1 URL: 1
examples/ticket.txt
Ticket 4471 (invented example, Lakeside Plumbing Supply) From: Jordan Mirelli <jordan.mirelli@example.com> Phone: (555) 014-2290 Received: 2026-09-14 Hi, the faucet I bought on 09/02/2026 leaks. Please send the replacement to 418 Birchwood Lane, Apt 3B. My neighbour Tess Okafor has the same model. I paid with card 4111 1111 1111 1111 and my order number is 1000234561. My son's school is Hillcrest Elementary, if you need to reach me there. Tracking page: https://track.example.com/abc123
scripts/redact.mjs
#!/usr/bin/env node
// Replaces pattern-shaped personal data in a text file with markers.
// Usage: node scripts/redact.mjs <input.txt> [--names names.txt]
// Prints the redacted text, then a line "---", then a count per kind.
// Local files only; no network. Names are caught ONLY if listed in --names.
import fs from "node:fs";
const args = process.argv.slice(2);
const input = args[0];
if (!input || !fs.existsSync(input)) {
console.error("Usage: node scripts/redact.mjs <input.txt> [--names names.txt]");
process.exit(2);
}
let text = fs.readFileSync(input, "utf8").replace(/\r\n/g, "\n");
const namesIdx = args.indexOf("--names");
const names = namesIdx > -1 && fs.existsSync(args[namesIdx + 1])
? fs.readFileSync(args[namesIdx + 1], "utf8").split(/\r?\n/).map((n) => n.trim()).filter(Boolean)
: [];
function luhn(digits) {
let sum = 0;
for (let i = 0; i < digits.length; i++) {
let d = Number(digits[digits.length - 1 - i]);
if (i % 2 === 1) { d *= 2; if (d > 9) d -= 9; }
sum += d;
}
return sum % 10 === 0;
}
const counts = {};
const hit = (kind) => { counts[kind] = (counts[kind] || 0) + 1; return `[${kind}]`; };
// Order matters: longer and more specific shapes first.
const rules = [
["EMAIL", /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g],
["URL", /\bhttps?:\/\/[^\s)>\]]+/g],
["SSN", /\b\d{3}-\d{2}-\d{4}\b/g],
// 13-19 digits, optional single spaces or dashes; only redacted if it passes the Luhn check.
["CARD", /\b(?:\d[ -]?){12,18}\d\b/g, (m) => luhn(m.replace(/\D/g, ""))],
["PHONE", /(?:\+1[ .-]?)?\(?\b\d{3}\)?[ .-]?\d{3}[ .-]\d{4}\b/g],
["IP", /\b(?:\d{1,3}\.){3}\d{1,3}\b/g],
// House number + up to three words + a common US street suffix.
["ADDRESS", /\b\d{1,6}\s+(?:[A-Z][a-z]+\s+){1,3}(?:St|Street|Ave|Avenue|Rd|Road|Blvd|Boulevard|Ln|Lane|Dr|Drive|Ct|Court|Way|Pl|Place|Ter|Terrace|Cir|Circle)\b\.?(?:,?\s+(?:Apt|Unit|Suite|#)\s*\w+)?/g],
["DATE", /\b(?:\d{1,2}\/\d{1,2}\/\d{2,4}|\d{4}-\d{2}-\d{2})\b/g],
];
for (const [kind, re, ok] of rules) {
text = text.replace(re, (m) => (ok && !ok(m) ? m : hit(kind)));
}
for (const name of names) {
const re = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi");
text = text.replace(re, () => hit("NAME"));
}
process.stdout.write(text.endsWith("\n") ? text : text + "\n");
console.log("---");
const kinds = Object.keys(counts).sort();
if (!kinds.length) console.log("No pattern matches. Read the text yourself: names and free-text details are not caught by patterns.");
for (const k of kinds) console.log(`${k}: ${counts[k]}`);
Use it in Claude Code
Put the folder at .claude/skills/redact-personal-data/ in your project (or ~/.claude/skills/redact-personal-data/ to use it in every project). Claude Code finds SKILL.md there and uses the skill when a request matches its description.
Other agents that read the Agent Skills format take the same folder.
Sources
- FTC: Protecting Personal Information: A Guide for BusinessChecked 6 Oct 2026
- Legal Information Institute (Cornell): 45 CFR § 164.514 Other requirements relating to uses and disclosures of protected health informationChecked 6 Oct 2026