Writing a .gitignore you will still trust in six months
Most ignore files start honest and turn into sediment. Someone pastes a Node template, someone else pastes a React template on top, a build output rule gets added twice with different spelling, and eighteen months later nobody wants to touch the file because nobody knows which line does what. The builder above solves the first half by writing each pattern once across every stack you pick. The tester solves the second half by naming the line responsible for any path you give it.
Stack templates overlap more than they look
Pick Node.js, React, Next.js and Webpack together from a template site and you get node_modules/ four times, three spellings of the build folder, and two blocks of debug logs. None of it breaks git. All of it makes the file harder to read and harder to edit safely, since changing one copy leaves the other three behind.
The merge here keeps the first occurrence of a pattern and records who else asked for it. The Repeats merged counter above shows how many lines the merge saved, and the panel under it names the patterns several stacks share. Treat those shared patterns as the spine of your file: they are the ones worth reviewing by hand.
Where the slash sits decides the scope
Three characters change the meaning of a rule, and they are the source of most confusion in ignore files.
| Pattern | Matches | Skips |
|---|---|---|
build | Any file or folder named build, at any depth | Nothing named differently |
build/ | Folders named build only | A file called build with no extension |
/build | build at the repository root | packages/ui/build |
docs/build | Only that one path, because the slash anchors it | site/docs/build |
**/build | build at any depth, written explicitly | Nothing extra, same result as the bare form |
*.log | Log files in every folder | logs/ as a folder |
The rule to remember: a pattern with no slash in the middle floats and matches by name anywhere. A pattern with a slash anywhere except the end is pinned to the repository root. Type both forms into the tester with a nested path and watch the verdict flip.
Last match wins, which makes order load bearing
Git walks the whole file for every path and keeps the final rule to match. Earlier lines lose silently. This is why an exception placed above its ignore rule does nothing at all:
!config/database.local.php config/*.local.php
Written in the other order the exception survives, because it matches last:
config/*.local.php !config/database.local.php
The flat layout in the builder sorts patterns alphabetically and pushes every exception to the bottom for exactly this reason. Grouped layout keeps stack sections intact, which reads better, at the cost of you watching where exceptions land when you edit later.
An excluded folder is a closed door
This trips up experienced people. Once a directory is excluded, git stops descending into it, so a negation aimed at something inside never gets evaluated. The file below fails to keep the asset:
assets/ !assets/logo.svg
Git never looks inside assets/, so the second line is dead text. Re-open the path level by level instead:
assets/* !assets/logo.svg
The tester reports this case directly. Paste assets/logo.svg against the broken version and the verdict names the folder rule as the blocker rather than the pattern you expected.
Deeper paths need every level reopened. To keep assets/brand/logo.svg alive you need assets/*, then !assets/brand/, then !assets/brand/logo.svg. Each step tells git to walk one layer further before it gives up.
An ignore rule has no power over a tracked file
The most common support question about ignore files is not a pattern problem at all. Git applies these rules to untracked files. A file already in the index keeps getting committed no matter what you write, so adding .env to a repository where .env was committed last March changes nothing.
git rm --cached .env git commit -m "Stop tracking local environment file"
That removes the file from the index while leaving it on your disk. Everyone who pulls loses their copy, so warn the team first. For a folder use git rm -r --cached storage/logs. To check the whole working tree at once, git status --ignored lists what git is currently skipping.
A secret in history stays in history. Untracking a leaked key hides it from future commits only. The old blobs remain reachable, and on a pushed branch, mirrored. Rotate the credential first, then clean history with git filter-repo or the BFG if the repository is worth rewriting.
Three ignore files, three different jobs
- .gitignore in the repository. Committed, shared, reviewed. Rules here belong to the project: build output, dependency folders, generated artifacts. This is what the builder above produces.
- .git/info/exclude. Local to your clone and never committed. The right home for a scratch folder or a scratch dump you keep beside the code without pushing the rule to teammates.
- A global file. Set with
git config --global core.excludesFile ~/.gitignore_global. Editor and operating system noise belongs here, since.DS_Storeis a fact about your laptop rather than a fact about the project. Plenty of teams still commit the macOS and Windows blocks for convenience, which is a defensible call when contributors run mixed machines.
Nested ignore files also work. A .gitignore inside packages/api/ applies to that subtree, with its rules taking priority over the root file for paths underneath. Monorepos read better with small local files than with one root file carrying every workspace.
What belongs in the file before the first push
Order the file by what hurts most when it escapes:
- Credentials.
.env, private keys, service account JSON, keystore files. The Secrets and keys chip covers the common shapes and keeps.env.examplecommitted so new contributors have a template. - Dependency folders.
node_modules/,vendor/,venv/. Reinstallable from a lock file and worth thousands of files in a diff. - Build output.
dist/,target/,.next/. Regenerated on every build and a permanent source of merge conflicts otherwise. - Machine noise. Editor state, thumbnail caches, swap files.
- Heavy junk. Database dumps and archives, which bloat clone size forever once committed.
One line deserves a second thought: lock files. The Rust template here ignores Cargo.lock, correct for a library and wrong for an application. Same tension applies to composer.lock and package-lock.json. Commit lock files when you ship a binary or a deployed service, drop them when you publish a library other projects depend on.
Reading the tester output
The tester answers a narrow question well. Give it a path relative to the repository root, with forward slashes, and it walks the path one segment at a time, applying every rule at each level the way git does. A verdict of Ignored names the winning line and its number. A verdict of Committed either names an exception rule or tells you nothing matched at all.
Add a trailing slash to test a folder: logs/ and logs get different answers when a directory-only rule is in play. That difference is the point of the trailing slash, and typing both is faster than reasoning about it.
Limits worth knowing before you rely on this
- Templates are opinionated starting points, not the upstream set. The github/gitignore repository carries more languages and more edge cases. These 34 cover what most repositories need, trimmed of rules almost nobody hits.
- Nested ignore files are not simulated. The tester evaluates the single file in the panel. A repository with per package ignore files behaves differently, since the nearest file wins for paths beneath it.
- Global and local exclude files are invisible here. If a path is ignored in your clone but shows as committed in the tester, check
git check-ignore -v pathto see which file the real rule came from. - Character classes are handled, locale collation is not. Ranges such as
[Dd]esktop.iniand*.py[cod]work. Exotic POSIX class syntax is rare enough in ignore files to be out of scope. - Case sensitivity follows the browser, not your filesystem. Matching here is case sensitive, matching git behaviour on Linux. On a default macOS or Windows volume git is less strict, so a rule written in the wrong case still hits locally and misses in CI.
Everything runs in this tab. The templates, the merge, the file, and the matcher are JavaScript on the page, so a path from a private repository never reaches a server. Close the tab and it is gone.
