The Case for a .gitignore Allowlist
Flip it round: ignore /**/* then re-include with !**/ for directories and !path for files. A gitignore allowlist stops junk landing by default, at the cost of a starter template and one traversal gotcha.
A gitignore allowlist inverts the file: instead of listing what to ignore, you ignore everything and then re-include only what you want tracked. It’s /**/* to ignore the lot, !**/ to let git see into directories again, then a ! line per thing you actually commit. The payoff is that nothing lands by accident. The cost is a starter template and one gotcha about directories.
This did the rounds on Hacker News in September 2026 after Alex Pliutau argued for it on packagemain.tech. It’s a contrarian take, and I think it’s right more often than the internet does.
The problem with the denylist
The default .gitignore is a denylist. Everything is tracked unless you name it. That works right up until the moment it doesn’t, and the failure is always the same: a .env with real credentials, a 200MB build artifact, an editor’s .idea/ folder, all committed because nobody had added them to the ignore list yet. The denylist only protects you from the junk you already thought of. New junk sails straight through.
A committed secret is the expensive version of this. Once it’s in history it’s in history, and rotating the credential is the only real fix. The denylist had one job and it failed silently.
What an allowlist looks like
Flip the default. The pattern, drawn from the gitignore docs and the write-ups doing the rounds, is short:
# ignore everything
/**/*
# but let git descend into directories
!**/
# then allow only what you track
!/.gitignore
!/README.md
!/src/
!**/*.go
Read it top to bottom. Line one ignores every file at every depth. Line two re-includes directories so git will actually look inside them. The rest name what you keep: the ignore file itself, your readme, the source tree, all .go files anywhere. Add a language or a config type, add a line. Everything else is invisible to git by default.
The gotcha that makes people give up
Here’s where the first attempt usually fails. You write /**/* then !src/app.go and it does nothing. The file stays ignored.
The reason is a performance shortcut in git: it does not descend into an ignored directory at all. If src/ is ignored, git never opens it, so it never sees your negation for a file inside it. You cannot re-include a file whose parent directory is excluded. That !**/ line is not decoration, it’s the thing that makes the whole approach work, because it un-ignores directories so git will traverse them and reach the files you allowed. Miss it and the allowlist looks broken.
Where I land
The denylist is the right default for a scratch repo or a quick prototype, where you’ll commit anything and clean up later. For anything with secrets, build output, or more than one contributor, the allowlist is the safer shape and I’d take the extra maintenance every time. You pay a line of config per new file type. You stop paying for leaked credentials and 200MB commits you have to rewrite history to remove.
It’s more work, and the work is the point. An allowlist forces a decision, “does this belong in the repo”, at the moment you add a new kind of file, which is exactly when you have the context to answer it. A denylist defers that decision until something has already gone wrong. Pick the one that fails safe.