I was building a tool for a client, and to get the parsing right I needed to see the real thing: the document their team works with every day. They sent it over with one line in the email. "Top secret."
My usual move with a new document format is to drop a sample into Claude and work through the structure with it: which fields repeat, where the tables start, what's optional, what the edge cases look like. With this file I couldn't. It was full of names, phone numbers and account numbers, I'd promised to keep it private, and pasting it into a third-party service "just to look at the layout" wasn't something I was willing to do.
What I actually needed was boring. The same document, same layout, same labels, with the personal values blacked out. Claude gets the structure, nobody gets the secrets.
That turned out to be harder than it sounds.
Black boxes don't remove anything
First try: open the PDF in an editor, draw black rectangles over the sensitive bits, save. Then the check everyone should do and almost nobody does:
- Open the "redacted" PDF.
- Ctrl+A (Cmd+A on a Mac), copy.
- Paste into a plain text editor.
Everything was still there. A PDF stores text and drawings separately, so a black rectangle is just a shape painted on top. The letters underneath are untouched, and anything that reads the text layer (copy-paste, search, or an LLM you hand the file to) sees them fine. It's the same mistake behind the court filings and government reports that leak every few months.
Online redaction tools: same problem, different shape
The next option was a website that redacts PDFs for you. Which means uploading the top-secret file to a stranger's server in order to remove the secrets. That's exactly what I was trying not to do.
So I wrote my own, and made it run entirely in the browser.
How it works
1. Get the text with positions. PDF.js gives you every text item with its transform, so you can map each character back to a rectangle on the page. That's what lets you paint a box over "41870253" and nothing else.
2. Find the obvious stuff with patterns, then validate. Emails, phone numbers (libphonenumber-js), IBANs, card numbers, UK sort codes, US routing numbers. The validators matter more than the regexes. Without the mod-97 check, half the reference numbers on a document look like IBANs. Without Luhn, every long order number is a "card". Without the ABA checksum, every 9-digit invoice number is a routing number:
// US routing numbers: weights 3-7-1, the sum must be a multiple of 10.
export function abaRoutingValid(input: string): boolean {
const d = input.replace(/\D/g, '');
if (!/^\d{9}$/.test(d) || /^0{9}$/.test(d)) return false;
const n = (i: number) => d.charCodeAt(i) - 48;
return (3 * (n(0) + n(3) + n(6)) + 7 * (n(1) + n(4) + n(7)) + (n(2) + n(5) + n(8))) % 10 === 0;
}
Account numbers have no checksum at all, so those only get flagged when they follow a label like "Account number" or "Acct #".
3. Names are the hard part. No regex finds "Sophie Whitmore". Labels help ("Name:", "Transfer to", a capitalised line sitting right above a street address), and for the rest there's an optional small NER model (bert-base-NER for English, camembert-ner for French) running in a Web Worker with Transformers.js. It downloads once, gets cached, and runs on WebGPU with a WASM fallback.
4. Scans. If a page has no text layer, Tesseract.js reads it in WASM first, and the OCR words go through the same detectors.
5. A human decides. Nothing gets removed automatically. Every finding shows up with the reason it was flagged, and you keep or remove it. Detection will always miss something or overreach somewhere, so the review step isn't optional.
6. Export by redrawing, not editing. Each page is rendered to a canvas at 150 DPI with the boxes painted in, and a new PDF is written from those images, with no text layer and no metadata. The trade-off is that the output isn't searchable. For my use case that was fine: the layout survives, Claude reads page images without trouble, and there's no hidden text left for anything to find.
Making "nothing leaves your machine" checkable
Saying "we don't upload your file" is cheap. So the page ships with a strict Content-Security-Policy whose connect-src only allows its own origin, plus the model host for that one-time download if you opt into the AI step. The browser itself blocks any attempt to send the document anywhere else. You can also load the page, turn your Wi-Fi off, and do the whole thing offline.
Checking a redaction someone else did
Once that worked, I wanted the reverse too: drop in a PDF someone else "redacted" and find out whether the text is still there. The idea is to compare what the text layer says with what a human would see.
Render the page to a canvas, then for each word look at the pixels under its box. Real text always has contrast inside its own box: dark glyphs on a light background. A word whose area is one flat colour can't be read on screen, but it's still in the file.
// rgba: the pixels under one word's box on the rendered page.
const n = rgba.length / 4;
const lum = [];
for (let i = 0; i < n; i++) {
lum.push(0.2126 * rgba[i * 4] + 0.7152 * rgba[i * 4 + 1] + 0.0722 * rgba[i * 4 + 2]);
}
lum.sort((a, b) => a - b);
// Ignore the 2 % extremes: anti-aliased box edges or a stray dot.
const lo = lum[Math.floor(n * 0.02)];
const hi = lum[Math.min(n - 1, Math.ceil(n * 0.98))];
const visible = hi - lo > 40;
const mean = lum.reduce((s, v) => s + v, 0) / n;
const look = visible ? 'visible' : mean < 60 ? 'under a black box' : 'covered or invisible';
It also lists redaction annotations that were marked but never applied, and whatever metadata survived (author, title, the app that made it). Hidden strings show only their first two characters, so a screenshot of the result doesn't leak the data a second time.
What it still misses
- Text drawn as vector paths instead of real text.
- Black boxes with transparency.
- Names that are neither labelled nor recognised by the model, which is why the review step exists.
The redaction tool is at sumipdf.com and the checker is at sumipdf.com/check. Both are free to try. If you've had to show a sensitive file to an AI tool or a contractor, I'm curious how you handled it. And if the checker gets a file wrong, I'd like to know.
Claude helped me tidy up this write-up, which felt fair given how the whole thing started.
Top comments (0)