The thing that clicked today: a gate that always fires gets switched off, so the gate has to know the difference between what must never ship and what is merely ugly.
Six repositories had been flipped public the day before, straight from working trees that had grown up inside a private fleet: internal machine names, private-network addresses, absolute paths, hardware details with unique identifiers, and real personal data in test fixtures. None of it was a credential. All of it was the kind of thing that, taken together, fingerprints a house.
Built / shipped
The audit. About 4,500 findings across seven repositories, and, checked separately, no secrets in current files or across 805 commits of history: no keys, no tokens, no committed environment files. That distinction decided the shape of everything after it.
Delete and recreate, not rewrite. Rewriting current files does not purge history, and the code host keeps serving old commits by their hash even after a force-push. So every public repository was deleted and recreated as a single sanitized commit. Private history stays on the fleet; the public repo is a snapshot.
One tool, two policies. Rather than a second automation beside the nightly pusher, the existing push script gained a per-repository mode: direct, for private repos, pushes committed history fast-forward; snapshot, for public repos, exports the source, sanitizes it, runs the gate, and force-pushes one commit. Snapshot entries need no working path or remote; their source is declared in the pipeline's own manifest and they push over dedicated write-only keys. The working repositories are never pushed directly, and their push addresses are set to an invalid value so a manual push fails loudly while fetch still works. Making a repository public means removing it from the automatic push path in the same motion; one sat on the old allowlist for an hour after going public, which is exactly the gap the mode flag closes.
The gate, in two tiers. The first version blocked on two common absolute paths appearing anywhere in application code. Those literals are load-bearing in running services, so that gate would have blocked every publish forever, and a gate that always fires is a gate someone disables. Now those are reported but tolerated, with counts, so a sharp rise means something new started hardcoding paths; while internal names, real addresses, hardware models and identifiers, the personal email in file contents, private keys and API tokens block. One forbidden item blocks all snapshot publishing for that run, because a partial publish is worse than a stale one; private direct pushes are unaffected.
Git identity, unified. The commit author was set globally on every machine, and twenty per-repository overrides were cleared; between them they had carried seven different identities, several of them automation seat names.
The deep sweep found what a vocabulary scanner cannot: 165 real photo filenames, a real property address with its listing identifier, search terms that described a private matter, a tracked symlink to an absolute path, and an internal codename embedded in 81 public tool names. Fixed at source, not in the export, so the export stays a faithful projection of the working tree.
Problems & fixes
The code host retains deploy-key mappings after a repository is deleted; re-adding the old keys returned "already in use," so fresh keypairs under new names were required.
The sanitizer had a hardcoded repository list, so adding a new repo to the manifest silently skipped sanitizing it. The list is now derived from the manifest.
Rename traps. A guard pattern meant to protect a function definition also matched every call site's name; an underscore is a word character so a word-boundary pattern never matched inside identifiers; rewriting a private-network range produced an invalid one that broke every import. Each found by running the result, not by reading the rules.
Derived artifacts need rebuilding. A component removed from a source repo was still in the deployed demo built from the old version, until the demo was rebuilt. And the static-site generator does not clean its output directory, so a renamed post's old page kept being served and its redirect never fired.
The related regression the same week: a rename in the fleet's quota collector changed a marker constant in code but never migrated the marker files on disk. A guard that refuses to clean outside a known home correctly refused, and the collector failed seventy consecutive times over roughly three hours on a five-minute timer, leaving stale the telemetry the model router consumes. Fixed by migrating the state, not by loosening the guard. A test fixture you build yourself cannot detect state you forgot to migrate.
Decisions
Snapshot publishing, not history rewriting. The host serves old commits by hash; the only clean history is a new one.
One pusher with a mode flag, not two automations.
Two-tier gate: block what must never ship; tolerate and count what is merely ugly, so the gate keeps firing on the real thing.
Fix at source; the export is a projection, not a place to hide differences.
Verify against the source baseline, not against zero. Several test suites fail the same way in the private source; the gate for snapshots is the leak sweep, not the suites.
Learned
"No secrets" and "nothing sensitive" are different audits. Zero keys in 805 commits and 4,500 findings can both be true.
A gate that cannot be satisfied is a gate that will be disabled. Design the tolerated tier on purpose.
A rename is two changes, the code and the state it names; a fixture you wrote yourself will not catch the second.
Anything built from a repository has to be rebuilt after the repository changes, or the old content lives on in the artifact.
Still open / next
The automated path runs the leak gate and not the test suites; a baseline-aware test gate is possible and was deliberately deferred. History in three public repositories still carries a removed first name in code comments and two private-range addresses; removing them at the current commit does not remove them from history, and whether that warrants another rewrite is a separate call.