Redact a log before you share it: introducing the PII Sanitizer
Emails, cards, IPs, SSNs, API keys and secret-named values out of a log, trace or .env — checked rather than guessed, replaced in your browser, with a report of every hit.
The moment always looks the same. A vendor’s support form asks for “the relevant log output”. A colleague in a public channel asks what the request actually looked like. A model in a chat window asks you to paste the stack trace. And the file in front of you contains a customer’s email address, the IP they connected from, four digits of a card, and — because it came out of a crashed process — the connection string with the database password in it.
What people do at that moment is one of three things: skip it and describe the error in prose, spend fifteen minutes hand-editing the excerpt, or paste it and hope. The third is far more common than anyone admits, and it is how customer records end up in public issue trackers.
PII Sanitizer is the fourth option. Paste the text, and every email, phone number, card number, IP address, Social Security number, JWT, API key and secret-named config value comes back replaced — with a per-class count of what it found, so you can see whether it did something sensible before you copy anything out. It runs entirely in the tab, which is the only reason it is defensible to paste a file like that into a web page at all.
Key Takeaways
| The reflex | What the tool does instead |
|---|---|
| A regex for “16 digits” to catch cards | Luhn plus an issuer table — an order number with 16 digits is left alone |
A regex for \d{3}-\d{2}-\d{4} to catch SSNs | The Social Security Administration’s real ranges, and bare nine-digit runs need a nearby cue |
Replace everything with *** | Four strategies, including stable numbered tokens that keep the document readable |
| Hash the values to “anonymise” them | HMAC with a key you supply — an unkeyed hash of a phone number is reversible by enumeration |
| Trust that the tool caught everything | A per-class count, a per-finding report, and an explicit list of what is out of scope |
| Hand-edit each occurrence | Same value, same token, everywhere — including across two files, under a shared key |
- Eleven built-in detectors, each with a validator wherever the class has checkable structure.
- Your own patterns are first-class, not an escape hatch: a rule you write outranks every built-in and produces the same tokens and the same report rows.
- Nothing is uploaded. No request, no server component, no analytics carrying your input — verify it with the network disconnected.
A pattern is not a detector
This is the distinction the whole tool is built on, and it is the reason a redaction tool is worth writing carefully rather than as a page of regexes.
Sixteen digits in four groups is a pattern. It matches a Visa number. It also matches an order reference, an invoice number, a batch id and a byte count with a thousands separator. A tool that replaces all of them does not protect you — it corrupts the file, and it does so silently, because the report beside it still says “1 credit card found”.
So every class with a checkable structure gets checked:
- Cards must satisfy the Luhn checksum and carry a known issuer prefix. That one arithmetic step rejects roughly nine out of ten arbitrary digit runs. Reverse the last two digits of a real card and the tool stops reporting it — which is exactly the behaviour that leaves invoice numbers intact.
- SSNs are rejected when the area is
000,666or900–999, when the group is00, or when the serial is0000— none of which were ever issued. And nine bare digits with nossnorsocial securitycue nearby are not treated as an SSN at all, because in an operational file they are overwhelmingly something else. - Addresses are parsed, not matched: four numbers, none above 255. The same parser the CIDR Calculator uses, so
api:1.24.3-alpineand a semantic version never get read as one. - JWTs must have a first segment that base64url-decodes to JSON carrying an
alg. Three dot-separated base64 runs happen by accident; a decodable JOSE header does not. - API keys are matched by vendor prefix, length and charset —
ghp_,sk-ant-,AKIA,AIza,xoxb-,glpat-,SG.,npm_, and the rest. A prefix plus a length plus a charset is about as close to a zero-false-positive test as this domain offers.
The failure mode being engineered against here is not “it missed a token”. A tool that misses something is a tool you stop trusting once. A tool that eats the image: line out of your docker-compose.yml is a tool you close and do not reopen.
The class that catches the most real leaks has no shape at all
Nothing about the string Hs8!kQ2wPz marks it as a password. Nothing about abc123 marks it as a key. There is no pattern to write, because the value is arbitrary — that is what makes it a good secret.
What identifies it is the name of the thing it is assigned to. So one detector keys off the identifier rather than the value, recognising the three forms configuration is actually written in:
DB_PASSWORD=Hs8!kQ2wPz # assignment
password: hunter2 # YAML
"client_secret": "8Kd0pQ2rS4tU6vW8xY" # JSON
wherever the name contains password, secret, token, api_key, authorization, private_key, client_secret, connection_string and their common spellings. In practice that single rule finds more genuine exposures than all the shape-based classes put together, because it is how secrets are written down.
It is also careful about what it leaves alone. An unset variable, a ${DEPLOY_TIME} interpolation, a {{ template }}, a YAML null and anything the tool has already replaced are all skipped — the last one being what makes a second run a no-op.
Four ways to replace a value
The strategy is a segmented control, and the choice matters more than it looks. Given this input:
2026-09-07T10:14:03Z POST /v1/checkout from 93.184.216.34
user=jane.doe@acme.io phone=+1 (415) 555-0142 ssn=123-45-6789
card=4929 7226 5379 7141
authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiJ9.qX8_Zt
STRIPE_SECRET_KEY=sk_live_51QxAbCdEfGhIjKlMnOpQr
Tokenize — the default — gives each distinct value a stable numbered label:
2026-09-07T10:14:03Z POST /v1/checkout from [IPV4_1]
user=[EMAIL_1] phone=[PHONE_1] ssn=[SSN_1]
card=[CREDIT_CARD_1]
authorization: Bearer [JWT_1]
STRIPE_SECRET_KEY=[API_KEY_1]
The same address later in the file is [IPV4_1] again. That is what keeps a log readable as a story — you can still see that one session did five things — without any of the five naming anybody.
Redact replaces with a flat [EMAIL], [IPV4], [CREDIT_CARD]. Safest, least useful: two different customers become indistinguishable, which is sometimes precisely what you want.
Mask reveals the conventional tail — j•••@•••.io, Visa •••• 7141, •••-••-6789, 93.184.216.•••. Only classes with a conventional safe tail get one; a JWT, a PEM block and the value of a password: line redact whole, because there is no half of them that is safe to show.
Hash replaces with [EMAIL_a3f9c1] — the first six hex characters of HMAC-SHA-256 under a key you type. Unlike tokenize, it is stable across documents: the same address in two exports taken a week apart produces the same label in both, which is what makes two incidents correlatable without either file identifying anyone. Change the key and every label changes, which is also how you deliberately make two sets uncorrelatable.
Notice what happened to the authorization header in the tokenized output. The word Bearer survives, and the credential is reported as a JWT rather than as an unspecified bearer token. Both are deliberate: the scheme word is line structure, not secret, and where two detectors claim the same characters the more informative reading wins. That contest is resolved in a dedicated pass — longest span first, then by detector priority — because if two overlapping replacements both reach the output, the second is computed against text the first already rewrote, and the result is quietly corrupted while the report beside it looks fine.
The exception list is load-bearing
Every redaction tool that ships without one destroys the first configuration file it is given. Yours contains 127.0.0.1. Your README contains user@example.com. Your payment tests contain 4111 1111 1111 1111, which is a perfectly Luhn-valid Visa number that Visa themselves published.
The tool starts with a seeded exception list: the loopback and link-local ranges, the RFC 5737 and RFC 3849 documentation ranges (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32), the RFC 2606 example domains and .test / .invalid / .local, and every payment provider’s published test card. Entries are literals, domains or CIDR blocks, one per line, and you can add or remove any of them.
Private RFC 1918 ranges are deliberately not on that list. They are a separate toggle that defaults to redacting, on the grounds that internal topology — which subnet the database is on, how many replicas answered — is exactly what a log leaving the company should not disclose.
Your identifiers, your rules
The built-in set can only ever cover what is common. Every organisation has identifiers that exist nowhere else: a badge number in a format someone chose in 2011, a customer reference with an undocumented checksum, a convention for internal hostnames. A fixed detector list is structurally incapable of knowing them.
So user patterns are promoted to real detectors. Give a rule a pattern, a label and a token:
pattern EMP-\d{6} label Employee ID token EMPLOYEE_ID
pattern \b(?:db|api|web)-[a-z]+\.internal\b label Internal host token HOSTNAME flags i
and EMP-004417 becomes [EMPLOYEE_ID_1] under exactly the same machinery as a built-in — same overlap handling, same four strategies, same stable numbering, same report row. Where a rule overlaps a built-in, yours wins, because you wrote it precisely when a built-in was doing the wrong thing. An optional capture group lets a rule match employee=(\d{6}) for context and replace only the digits, rather than eating the key and leaving an unreadable line.
Rules export and import as JSON, so a team agrees on one set instead of six people each maintaining their own. If you are drafting the pattern itself, the Regex Tester is the better place to get it right first — it shows you every match with its groups, which is the question you actually have while writing one.
A user regex is untrusted input and the tool treats it that way: a pattern that fails to compile reports on its own row rather than failing the whole run, compilation is debounced so a half-written expression is never executed, and a rule that matches the entire document or its own output is rejected rather than turned loose on your file.
What it will not do
This section matters more than the feature list, and the tool states the same things at the point of use rather than burying them here.
It is not a compliance tool. No claim is made under GDPR, HIPAA or PCI-DSS. It reduces the chance of pasting a customer record into a public issue. It does not certify a file as safe, and it should not be the only thing between sensitive data and somewhere public.
Consistent labels are pseudonymisation, not anonymisation. Two records that shared a value still visibly share one — that is what makes the output useful for debugging, and it is also what keeps it linkable. Under European data-protection law, pseudonymised data remains personal data with every obligation still attached.
Partial reveals are reversible in practice. Four visible card digits plus a timestamp plus a merchant identifies a transaction. An area code plus four digits narrows a phone number to a small set. Use mask when a human needs to recognise a record, not when the output is leaving your control.
Classes with no lexical structure are out of scope. Personal names, street addresses, dates of birth, free-text notes. There is no pattern that separates a name from an ordinary word, and a tool that claimed otherwise would be guessing on your behalf. Treat an empty result as no matches, never as a guarantee.
The replacement is textual. "card": "4929..." stays valid JSON because the value was already a string; "card": 4929722653797141 does not, because [CREDIT_CARD_1] is not a number. That is visible the moment it happens, but worth expecting.
The token map is the dangerous artifact. Under tokenize, the mapping from [EMAIL_1] back to the original is a plaintext list of everything sensitive in the document. It sits behind an explicit reveal, it is never part of the copied or downloaded output, and its own copy button says what it is. The other three strategies discard the originals entirely — nothing on the page can restore them.
Where it runs
In your tab. There is no upload, no backend, and no request when you paste — the scan, the replacement and the mapping table are all computed in the page, and closing it is what disposes of them. Input is capped at 256 KB because the scan runs on the main thread and a larger file would freeze it rather than fail honestly.
Every setting persists locally except one: the HMAC key is never written to storage. Every other option is a preference; that one is a secret, and leaving it in the browser profile of a shared machine would undo the reason the keyed mode exists.
You can verify the whole claim the way you would verify anyone’s: open the tool, disconnect from the network, and use it. For the reasoning behind that constraint across the whole site, see the privacy-first manifesto; for the same argument applied to token debuggers, online JWT decoders that log your secrets.
A workflow that actually holds
- Paste, or drop the file. Four sample documents are built in — an access log, an API response, a stack trace and an environment file — if you want to see the shape of the output before trusting it with your own.
- Read the counts before the output. Each class shows how many times it fired. Three phone numbers in a file containing none is a false positive, and finding it here costs nothing compared with finding it after you have sent the text.
- Add what was missed. Your internal identifier formats, as rules. Export them once and the team shares the set.
- Add what should have survived. Values that must come through untouched go in the exception list.
- Reveal the mapping only if you need it, and keep it separate from anything you send. It is the one artifact that undoes the whole exercise.
- Read the output. Not skim — read it. The tool is a very good first pass and a very bad last line of defence.
If the thing you are about to share is a certificate rather than a log, the Certificate Decoder tells you what is in it without redaction being necessary. If it is an environment file you are converting between formats, Env File Converter is the tool for that half, and this one for the sanitising. All three sit in the DevOps collection.
Conclusion
The reason redaction gets skipped is not that people are careless. It is that doing it by hand takes fifteen minutes and doing it with a quick regex breaks the file — so the honest options are “don’t share” and “share it anyway”, and under deadline the second one wins.
PII Sanitizer exists to make the careful option the fast one: eleven validated detectors, your own patterns on equal footing, four replacement strategies, a report of every hit, and an exception list that keeps the file intact. It runs in your browser, it makes no compliance claims it cannot support, and it tells you plainly what it cannot see. That last part is the feature — a redaction tool that oversells itself is more dangerous than no tool at all.
Frequently Asked Questions
Does the text I paste get uploaded anywhere?
No. There is no upload, no request and no server component. Detection, replacement and the table of original values all happen in the page you are looking at, and closing the tab disposes of them. You can confirm it by disconnecting from the network and using the tool — it still works, because there was never anything to send.
Will it break my configuration file?
It is built specifically not to. Every class with a checkable structure is validated rather than merely matched, the reserved documentation ranges and example domains are excluded by default, published test card numbers are on the exception list, and each class reports its own count so anything unexpected is visible before you copy. Anything else that must survive can be added to the exception list as a literal, a domain or a CIDR block.
What will it miss?
Anything without a recognisable shape or a recognisable name: personal names, street addresses, dates of birth, free-text notes, and any internal identifier format it was never told about. Credential formats not yet in the vendor table also pass through untouched. Treat an empty result as an absence of matches, not as a guarantee of safety.
Is sanitized output anonymous?
No, and the distinction is legal as well as technical. Stable labels are pseudonymisation: two records that shared a value still visibly share one, which is what makes the output useful and also what keeps it linkable to a person given other information. Under European data-protection law, pseudonymised data remains personal data.
Why does the hash mode require a key?
Because an unkeyed digest of a small input space offers no protection. There are only ten billion possible ten-digit phone numbers, so anyone holding the output can hash every one of them and recover the original by lookup — the same is true of SSNs, card numbers and short employee ids. HMAC under a secret key makes that infeasible, and requiring the key stops a mode that exists to avoid the mistake from making it easy.
Can I undo the replacement?
Only in tokenize mode, and only with the mapping table — which is why it sits behind a deliberate reveal and is never included in the copied or downloaded output. Redact, mask and hash discard the originals entirely; nothing on the page can restore them.
Can I add detectors for my own identifiers?
Yes, and they are treated as first-class detectors rather than as a fallback. Supply a pattern, a label and a token, and the rule joins the built-in set: same overlap resolution, same replacement strategies, same stable numbering, same report row — and it outranks every built-in where they overlap. Rules export as JSON so a team shares one agreed set.
How large a file can it handle?
Up to 256 KB per run. Everything executes on the main thread of the tab, and past that ceiling the page would stop responding while it worked, so the tool reports the size rather than freezing. For a larger log, take the excerpt that matters — which is what you should be sharing anyway.