What Must Never Reach the Repository
A .gitignore is a list of patterns matched against paths, and it has no effect whatsoever on a file Git is already tracking — which is the trap that leaks credentials. Toggle patterns against a real tree and watch which paths survive.
A .gitignore is a list of patterns matched against paths, and it has no effect whatsoever on a file Git is already tracking — which is the trap that leaks credentials. Toggle patterns against a real tree and watch which paths survive.
gitignore is patterns, matched against paths
Put a file called .gitignore in your repository root, list patterns one per line, and Git stops offering those files. They vanish from git status, they are skipped by git add ., and they can no longer be committed by accident.
# Comments start with a hashnode_modules/ # trailing slash: directories, at any depth*.log # no slash: matches the file name anywhere.env # a plain name, also anywherebuild/ # catches src/build/ as well as ./build/docs/*.pdf # contains a slash, so it is anchored to the root!docs/manual.pdf # a negation — re-include one fileThree rules do almost all the work, and the third one surprises everybody.
The pattern matches the file name at any depth. *.log catches app.log, logs/app.log, and deeply/nested/app.log alike.
The pattern is anchored to the repository root, and * never crosses a slash. docs/*.pdf catches docs/guide.pdf and NOT docs/api/spec.pdf.
Patterns are read top to bottom and the last one that matches decides. A negation only works if nothing below it re-excludes the file.
.gitignore
files in the repository
- index.html
- .env.env
- .env.example
- node_modules/react/index.jsnode_modules/
- logs/app.log*.log
- logs/keep.log*.log
- src/build/bundle.js
- src/app.js
- docs/guide.pdf
- docs/api/spec.pdf
4 of 10 ignored. Struck-through files are invisible to git status and can never be committed by accident.
Turn on the negation and watch logs/keep.log come back — because it is listed after *.log. Move it above and it would do nothing at all. Order is the whole rule: the last pattern that matches a path is the one that decides.
Dependencies and build output are noise
The first category to ignore is everything that can be regenerated. Committing it is not dangerous, just wasteful — and the waste is permanent, because history never shrinks.
# Dependencies — reinstallable from the lockfilenode_modules/vendor/.venv/__pycache__/ # Build output — regenerable from sourcedist/build/.next/*.o*.class # Editor and operating system litter.DS_StoreThumbs.db.idea/.vscode/*!.vscode/extensions.json # Logs and local databases*.log*.sqlite3 # Secrets — see the next section.env.env.local*.pemcredentials.jsonNote the pairing near the editor section. .vscode/* ignores the whole folder and then !.vscode/extensions.json brings one file back, because recommending extensions to your teammates is useful while sharing your personal window layout is not.
One important exception: lockfiles are not build output. package-lock.json, yarn.lock, poetry.lock and Cargo.lock should absolutely be committed — they are what makes an install reproducible. Ignoring them is a common and expensive mistake.
A committed secret is a leaked secret
This is the section that matters. If an API key reaches a commit, treat it as compromised immediately — not after you decide whether anyone saw it.
- 1
Deleting the file does not remove it
Deleting it makes a new commit in which the file is absent. Every earlier commit still contains it, and git show on any of them prints it back.
- 2
Pushing makes it public and permanent
On a public repository, automated scrapers find committed credentials within minutes. This is a well-documented, industrialised process, not a theoretical risk.
- 3
Even a private repository is not safe
Every clone has the full history. A contributor who leaves keeps their copy, and repositories get made public later by accident more often than you would think.
- 4
So the only real fix is to rotate the secret
Revoke the key at the provider and issue a new one. The old value stays in the history somewhere in the world; make it worthless instead of trying to erase it.
The prevention is boring and effective: commit an .env.example with the variable names and no values, ignore the real .env, and let GitHub's secret scanning watch your back — it is on by default for public repositories and will email you and the provider if a recognisable token lands.
DATABASE_URL=STRIPE_SECRET_KEY=RESEND_API_KEY=gitignore does nothing to an already tracked file
This is the rule that catches everyone, once, and it is the reason the section above exists.
.gitignore only applies to untracked files. Once Git is tracking a file — once you have committed it even a single time — adding it to .gitignore changes nothing at all. Git keeps reporting it, keeps staging it, keeps committing your changes to it.
# Stop tracking it, but keep it on disk. The --cached is the whole trick.$ git rm --cached .env # For a directory$ git rm -r --cached node_modules/ # Now .gitignore takes effect. Commit the removal.$ git commit -m "Stop tracking .env; it is in .gitignore"And note what that commit does to your teammates: it removes the file from the repository, so when they pull, the file disappears from their machines too. For a .env that is correct and they will each recreate their own. Say so in the commit message, or somebody will spend an hour wondering where their config went.
When you cannot tell whether a file is ignored and why, ask:
$ git check-ignore -v config/local.yml# .gitignore:7:*.yml config/local.yml# ^file ^line ^the pattern that matchedThree places to put an ignore rule
There are three ignore files and choosing the right one is a question about who the rule is for.
.gitignore, committed. For things nobody on the project should ever commit: node_modules, dist, .env. This is the one you will use.
.git/info/exclude, not committed. For your own mess in this one project — a scratch file, a local script. Nobody else is affected and nobody else has to agree.
~/.gitignore_global, set with core.excludesFile. For your editor and operating system, everywhere. .DS_Store belongs here, not in every project's .gitignore.
$ git config --global core.excludesFile ~/.gitignore_global$ printf '.DS_Store\n.idea/\n*.swp\n' >> ~/.gitignore_globalThe distinction is social, not technical. Putting .idea/ in a shared .gitignore asks every contributor to carry a rule about an editor they may not use; putting it in your global file solves it for you in every repository you will ever touch.
On day one of a new repository
- ✓Add a .gitignore before the first commit — retrofitting one means git rm --cached
- ✓Start from GitHub's template for your language rather than from memory
- ✓Ignore .env and commit .env.example with the keys and no values
- ✓Commit your lockfile; it is not build output
- ✓Put editor and OS litter in your global ignore file, not the project's
Key takeaways
- A pattern with no slash matches a name at any depth; a pattern with a slash is anchored to the root and * never crosses a slash.
- The last matching pattern wins, which is why a negation must come after the rule it undoes.
- Ignore anything regenerable — dependencies, build output, editor litter — but commit your lockfile.
- Start from github.com/github/gitignore rather than writing one from memory.
- A committed secret is compromised. Deleting the file adds a commit and preserves every earlier one.
- Rotate the key first. History rewriting is second, optional, and never reaches every copy.
- gitignore has no effect on an already-tracked file. git rm --cached stops tracking without deleting.
- git rm without --cached deletes the file from your disk too.
- git check-ignore -v names the exact file and line of the pattern that matched.
- Three ignore files: the project's, your private .git/info/exclude, and your global one for editor and OS noise.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.