I audited three of my repos before making things public. The audit was after one class of thing, anything that tied the code to the private world behind it:
- absolute paths from my machine
- the names of projects that were never meant to be seen
- internal addresses
- the real names of people and places around the work
One test for every line: would it be safe, read by a stranger who knows nothing about me, my machine, or my other projects.
The audit ran five rounds, a round being one full pass with one scope, because every round found something new. Not because the searching was sloppy. Because each round searched only where it happened to look, and each fix pointed at the next place. Round one read the tracked files and removed a set of strings. Those same strings still sat in every old commit, so git history became round two. Round two rewrote the history, but the packages already built from it still carried the strings, so the published artifacts became round three. The leaks mapped onto blind spots, not onto effort, and that observation ended up shaping the whole system.
The worst find was not in git at all. A cache file, properly gitignored, carrying an absolute path from my machine, shipped inside three published releases of an extension, because the packager does not read gitignore. Every git-based check passed, forever, while the file went out to two marketplaces. I am deliberately not naming the extension, old releases tend to stay fetchable long after a fix ships, and that is exactly the point of this piece. There is no repo-scoped check that can catch that.
- Scope, not thoroughness, is the thing to get right.
So the setup I run now has four doors, one per exit, each one an ordinary git or packaging hook:
working tree ─[1][2]─▶ history ──[3]──▶ remote ──[4]──▶ marketplace
commit push publish
[1] staged content pre-commit the lines about to enter history
[2] commit message commit-msg messages are history too
[3] full tracked tree pre-push what a rebase or --no-verify let in
[4] built artifact package step the only door the cache file met
Staged content is screened at commit, a pre-commit hook running the pattern list, absolute paths from your home directory being the classic first entry. The commit message is screened separately by its own hook, messages are published history too and the staged check structurally cannot see them. The full tracked tree is screened at push, a pre-push pass over everything tracked, which catches whatever a rebase or a no-verify commit smuggled in. And the built artifact is screened at publish, wired into the package step itself, the only door that would have caught the marketplace leak, sitting at the irreversible moment on purpose. A commit can be amended. A published package cannot be recalled from the machines that already fetched it.
Two rules keep the doors honest. Every new class of finding becomes a rule, a pattern if it greps, a checklist line if it is a place nobody had inspected. The audit found something new five rounds in a row precisely because each round's lesson used to die with its fix. And when there is a choice, remove the surface instead of adding a rule. The single biggest win of the whole audit was an ignore file that cut a published package from thirteen files to five. Content that never ships cannot leak, and no pattern list will ever be as reliable as absence.
One more, because it stings. Patterns only catch what you already thought of. A sentence can disclose a fact about your employer while containing no banned string at all. For that class there is no grep, only a periodic fresh read of everything, asking one question, what does this repo reveal about its author. I treat that read as part of releasing, the same as the version bump.
If you take one thing, take the framing. Publishing is not showing your current code. It is showing every decision, path, and careless message you ever committed, to everyone, forever. Build the doors before the first push, it is genuinely miserable to retrofit.
Top comments (0)