.gitignore "not working" almost always means the file is already tracked — a file in the index is immune to any rule you write.
git check-ignore -v <path> decides it: printed rule = a rule problem; silence = tracked and no rule applies.
Fix: git rm --cached <path>, commit — the rule works from that commit forward.
One command, two outcomes — git check-ignore -v sends you through one of them:
DOOR 1 · it prints a rule
$ git check-ignore -v debug.log
.gitignore:3:*.log debug.log
a rule applies — the wrong one, or in the wrong scope. The problem lives in the .gitignore file: rule order, negation traps, or which ignore file owns the path.
DOOR 2 · silence
$ git check-ignore -v src/.env
$ # (nothing — no rule applies)
the file is tracked anyway. Confirm with git ls-files --error-unmatch <path>, then git rm --cached <path> and commit the removal. The silence is the diagnostic.
exit codes are scriptable: 0 = ignored, 1 = not ignored — git check-ignore -q debug.log && echo ignored || echo tracked-or-unruly
The negation trap, annotated. Why !build/keep.me never rescues the file:
build/ignores the directory wholesale — git never looks inside again!build/keep.medead letter: you cannot re-include a file inside an excluded directorybuild/*the fix — ignore the contents, not the directory!build/keep.menow alive: the parent path is walkable, the exception applies*.log then !important.loglast matching rule wins — the negation after the rule re-includes the one file| Symptom | Real cause | Fix |
|---|---|---|
Ignored file still shows in git status | Committed before the rule — in the index | git rm --cached + commit |
git add . keeps re-adding it | Same: tracking beats ignoring | Bulk: git rm -r --cached . → git add . |
!build/keep.me never rescues the file | Parent dir excluded — git skips it wholesale | build/* + !build/keep.me |
| A later rule overrides yours | Last matching rule wins | Reorder, or read the winner via check-ignore -v |
| Rule works for teammates, not you (or reverse) | Nested .gitignore / exclude / global mismatch | Match the scope table below |
| VS Code shows it modified after ignoring | Tracked — or a stale SCM view | git rm --cached; reload window first |
| Want edits quiet, file still tracked | Intentional local divergence | git update-index --skip-worktree |
Rules can live in three places, and "works for teammates, not for me" is almost always a scope mismatch:
.gitignore in the repo — committed, shared with everyone. The default home for build output and dependency dirs..git/info/exclude — per-clone, never committed. For your personal scratch files in this one checkout.core.excludesFile) — per-machine, across all repos. The classic home for .DS_Store and editor droppings.Reading check-ignore -v output names the source directly: the ignore file, then line-number:pattern, then the path — so when a surprise rule wins, the winner identifies itself.
verify: git check-ignore -v debug.log → .gitignore:3:*.log debug.log
# untrack one file (the local copy survives — --cached touches the index only)
git rm --cached .env
git commit -m "stop tracking .env"
# verify the ignore rule now applies
git check-ignore -v .env
From this commit forward, .gitignore owns the path: edits no longer show in git status, and git add . will not pick the file up again.
If it was a secret, untracking is not the fix. The file stays in history — removing it from the latest commit is not enough. Rotating the credential is the real fix; history rewriting is the cosmetic one.
end of the two doors · nothing fetched · everything local