Skip to main content

A short post titled “.gitignore everything by default” reached the Hacker News front page yesterday and pulled in the kind of argument that tells you a settled practice is under real pressure. The proposal, from Alex Pliutau, is one line long: open your .gitignore with *, then name the file types you actually want tracked.

The case for it is that blocklists have stopped scaling. The .gitignore in Microsoft’s typescript-go repository is 207 lines. GitHub’s own Node.js template is 143 lines before you add a rule of your own. Every one of those lines exists because somebody, at some point, was surprised by a file.

Which is the part worth sitting with. A denylist is not a policy, it is a prediction about which files will exist, and it held for twenty years because a small number of humans and a smaller number of build tools created every file in the repository. Neither of those things is true any more.

TL;DR

  • An allowlist .gitignore starts with * and negates back the file types you want. It inverts the default from “committed unless excluded” to “excluded unless named”.
  • The pattern as commonly published does not work. * excludes directories too, and Git will not descend into an excluded directory, so nothing in a subfolder can be negated back. You need !*/ on the second line.
  • GitGuardian counted 28.65 million new hardcoded secrets on public GitHub in 2025, a 34% year-on-year rise and the largest single-year jump on record. AI-assisted commits leaked secrets at 3.2% against a 1.5% baseline.
  • An allowlist governs file classes, not file contents. A key hardcoded inside an allowed .go file sails straight through. This is a blast-radius control, not a secrets control, and anyone selling it as the latter is wrong.
  • Push protection is free on public repositories. Private and internal repositories need paid GitHub Secret Protection, billed per active committer, which is exactly where most client work lives.

A denylist is a prediction about files that do not exist yet

Ignore templates were historically per-language because file classes were per-language. A Node project produced node_modules, a Python one __pycache__, and the prediction held for the life of the project. Now open a repository that has had coding agents working in it for six months and count the file types nobody planned for. Agent instruction files. Plan documents and scratch notes written mid-task. Generated fixtures. One-off benchmark scripts meant to be deleted. Tool caches, session transcripts, screenshots from a browser-driving agent. None of these were on a template, because templates describe what a language produces, not what an autonomous process decides to write down.

The published pattern does not work as written

Here is the detail almost every write-up of this idea skips, and it is why people try it and quietly give up. The naive version:

*
!.gitignore
!*.go
!go.mod

Run that in any project with subdirectories and your source tree vanishes. The * pattern does not only match files. It matches directories, and the Git documentation is blunt about what follows: “It is not possible to re-include a file if a parent directory of that file is excluded. Git doesn’t list excluded directories for performance reasons, so any patterns on contained files have no effect, no matter where they are defined.”

So internal/ is excluded, Git never looks inside it, and !*.go never gets the chance to match. The fix is one line, and it has to come second:

*
!*/
!.gitignore
!*.go
!go.mod
!go.sum
!README.md

!*/ re-includes directories so Git walks into them, at which point the file-level negations apply at every depth. Two further edges matter. Ignore rules have no effect on files Git already tracks, so converting an existing repository changes nothing until you run git rm --cached. And git add -f overrides them entirely, which is how the file you carefully excluded gets committed by someone in a hurry. When a file then refuses to appear in git status, git check-ignore -v path/to/file prints the rule and line responsible. It is the only efficient way to debug an allowlist and most developers have never run it.

The numbers behind the anxiety

This would be a tidiness argument if the stakes were build artefacts. GitGuardian’s State of Secrets Sprawl 2026, published in March, found 28.65 million new hardcoded secrets added to public GitHub commits during 2025: a 34% year-on-year increase and the largest single-year jump the report has recorded. Secrets belonging to AI services hit 1,275,105, up 81%.

The figure that matters most here is narrower: commits made with AI coding assistance showed a secret-leak rate of 3.2% against a 1.5% baseline across all public GitHub commits, slightly more than double. Treat that as a correlation rather than an indictment, because the teams reaching for agents are also the teams shipping fastest and some of the gap is velocity. The direction of travel is not in dispute, though. Files created per unit of human attention have gone up sharply, review per file has gone down to match, and a denylist sits precisely on that seam.

What an allowlist actually buys you

It governs file classes and does not read file contents. An API key hardcoded in main.go sits in an allowed file type and will be committed without complaint, as will a connection string in a config file you deliberately track. What it removes is the other accident, where an unanticipated file type carries the payload: a .env written by a setup script, a .pem left behind during a certificate rotation, a local SQLite database of production data pulled down for debugging, a Terraform state file, an agent transcript containing every token the session touched. In our experience that is where the genuinely frightening leaks come from, precisely because those files were never in anyone’s mental model of the repository.

The failure mode inverts, and that is the real trade

A denylist fails by silently including. An allowlist fails by silently omitting. A developer adds a Dockerfile, git status reports nothing to commit, and the file never leaves their machine. The next person to clone the repository gets a build that does not work and no clue why.

That objection is fair, and it is still the cheaper failure by a wide margin. Recovering from a forgotten file is one commit. Recovering from a committed private key is a rotation, a history rewrite, a coordinated force push across every fork and clone, and an honest assumption that the key was harvested anyway, because public repositories are scraped within minutes of a push.

The practical read: an allowlist suits repositories where the file set is stable, such as a backend service in one language. It suits a front-end application mid-migration between build tools far less, because you will spend the week negating patterns back in.

What we would put in place instead of a debate

  1. Ignore dotfiles globally rather than per repository. Put .* in ~/.config/git/ignore with negations for the handful you want (!.gitignore, !.github, !.editorconfig). Agent tooling writes dot-directories almost exclusively, so one line also covers the tool you install next month. It is the highest-value version of this idea and needs no per-project maintenance.
  2. Keep agent scratch space outside the repository tree, not ignored inside it. Ignored is not invisible. A .gitignore entry does nothing for docker build, which reads .dockerignore, a separate file most teams never wrote. That is how an excluded .env ends up baked into an image layer and pushed to a registry.
  3. Know where push protection stops. GitHub secret scanning with push protection is free on public repositories. Private and internal repositories require GitHub Secret Protection, billed per active committer. Almost all client work is private, so the protection developers assume is ambient is absent exactly where the commercial risk is highest. A pre-commit scanner such as gitleaks closes that gap for nothing.
  4. Scan history, not just the current tip. Of the credentials GitGuardian confirmed valid in 2022, more than 64% still worked when retested in January 2026. The key committed eighteen months ago and removed in the next commit is still in the object store, and it still opens the door.
  5. Make the agent instruction file question an explicit decision. A good part of the Hacker News thread went on whether CLAUDE.md and AGENTS.md belong in a repository at all: shared engineering standard, or a dumping ground for one developer’s preferences that the agent itself keeps editing. Both positions are defensible. Committed means it gets reviewed like code, ignored means it stays personal. Not deciding means whoever last ran git add . decided on behalf of the team.

Defaults that assume a human reviewed every file before staging it were built for a workflow that has changed underneath them. REPTILEHAUS builds and audits development workflows for teams running coding agents in production, down to the boring parts: secret scanning, pre-commit gates and CI policy that survives contact with a deadline. If you have not checked what your agents have been writing into your repositories, get in touch and we will start there.

📷 Photo by Hyundai Motor Group on Unsplash