Back to writing

The Guard Must Not Name What It Guards

AI Security Building

Git never forgets. That’s the whole point of git, and it’s a beautiful thing, right up until the day you want to hand your repository to someone outside the building.

I built my product by dogfooding it inside a real company, which is the best way to build and the worst way to share. The product’s code and the company’s operating history grew up tangled in one repository, and git keeps every version of every file, forever. Which means you cannot scrub your way to safety. Delete the sensitive file, rewrite the history, feel clever, the old versions are still recoverable. So you don’t scrub. You extract: copy only the genuinely product code into a brand-new repository with fresh history, transform every identifier at the copy step, and leave the private instance private forever as tenant number one.

Simple plan. Then came the audits, and the audits are where the education was.

The lesson: no single reviewer can clear a repository like this, because each kind of reader is blind to a different class of leak. It took a triangle. The builder knows the mechanics and catches the obvious, trapped data files, named leaks in fixtures. The insider knows the domain vocabulary and catches what reads as innocent to a stranger, an adjective that’s also a product family, a test regex whose literals happen to spell out the real product line. And the cold reader arrives with fresh eyes and one job: can I re-identify anyone from what’s left? In our case the cold reader was a fresh-context AI model run as an adversarial de-anonymization attacker, no memory of our vocabulary, every incentive to link what it saw to the public internet. That round found the hardest class, derivative fingerprints. Initials. A datestamp baked into an account name. “Synthetic” financial fixtures that were actually light perturbations of real books. No denylist catches those, because no single forbidden string names them.

Three findings I’ll carry around forever. First: “fresh git init” is necessary, not sufficient. An early pass left recoverable pre-scrub commits as orphaned objects in what we thought was the final repo, history that looked clean and wasn’t, caught before anything went public. Fresh history is a claim you prove against the object database with git fsck, not a checkbox you tick.

Second, my favorite: the obvious defense is a denylist of forbidden strings, checked in the build. But a plaintext denylist of the names you’re hiding is a reversible concordance, a map to exactly what you scrubbed, shipped alongside the scrubbing. So the public repo carries no list at all; the check decodes its patterns from a secret and fails loudly if the secret is missing. The guard is not allowed to name what it guards.

Third, the iron rule that made all of it hold: never hand-edit the copy. Fix the source, fix the script, regenerate. A fix that wouldn’t survive a re-run isn’t a fix, it’s a wish.

The full field note, including the parts we got wrong on the first pass, is at thinkfigaro.com/notes/the-extraction-gauntlet.