TL;DR: if .gitignore is not working, the file is almost always already tracked. Git ignores files it has never seen — a file in the index is immune to any rule you write. Find the truth with git check-ignore -v <path>: silent output means the path is tracked and no rule applies. Fix it with git rm --cached <path>, commit, and the ignore rule starts working from that commit forward. Rule order, negation traps and VS Code red herrings explain the remaining cases.
Why is .gitignore not working?
One mechanism covers the majority of cases: the file was committed before the rule existed. .gitignore is not a filter that hides files from git — it is a rule about what git add should pick up from an untracked state. Once a file is in the index, git tracks its content changes forever until you explicitly untrack it. Editing .gitignore after the fact changes nothing about tracked files, which is why the classic sequence “commit .env, panic, add .env to .gitignore, commit again” still ships the secret on every push.
This bites everyone because git is nearly universal — the Stack Overflow 2022 survey put Git usage at over 93% of professional developers (survey.stackoverflow.co) — and every one of those developers eventually writes a rule for a file they committed last week.
The rest of this post covers the minority cases: rule-order mistakes, negation traps, folder edge cases and the IDE question. But run the health check in the next section first — more than nine times out of ten it ends the investigation.
How do I check which gitignore rule is matching a file?
git check-ignore is the diagnostic tool, and its silence is the diagnostic:
# Prints the matching rule + file + line number when a rule applies
git check-ignore -v debug.log
# .gitignore:3:*.log debug.log
# Prints NOTHING when no rule applies — the file is tracked (or no rule exists)
git check-ignore -v src/.env
# (silence = .gitignore is not ignoring this path, whatever you wrote)
# Exit codes: 0 = ignored, 1 = not ignored — scriptable
git check-ignore -q debug.log && echo "ignored" || echo "tracked or unruly" Reading -v output: the ignore source (.gitignore, .git/info/exclude, or your global ignore file), then line-number:pattern, then the path. If a later rule surprises you, remember precedence: the last matching rule wins, so !important.log after *.log re-includes that one file.
Why is gitignore not working for files already committed?
Untrack the file, keep it on disk, commit the removal:
# 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 to it no longer show in git status, and git add . will not pick it up again. The file stays in history though — if it was a secret, removing it from the latest commit is not enough. Rotating the credential is the only real fix; history rewriting is the cosmetic one (and undoing the last commit only helps while the bad commit is still the tip).
For a repo full of previously committed junk — build output, editor droppings, node_modules that snuck in early — the bulk untrack is a two-liner:
git rm -r --cached .
git add .
git commit -m "apply .gitignore to tracked files" This rebuilds the index against the current rules: ignored paths drop out, everything else is re-added unchanged. The diff looks dramatic (thousands of deletions) but deletes nothing from disk. If your goal is deleting those files rather than just untracking them, that is git clean -fdx territory — see git remove untracked files, safely.
Git ignores files it has never seen. A file already in the index is immune to
.gitignore— no rule you write will unsee it.
Why is gitignore not working for a folder?
Three folder-specific traps:
1. Trailing slashes matter to intent, not matching. build and build/ both match a directory, but build/ documents that you mean a directory only — a file named build would survive it. Symmetry fails, though, on re-inclusion (next trap).
2. Negation cannot rescue files inside an excluded directory. The git docs are unambiguous: “It is not possible to re-include a file if a parent directory of that file is excluded” (git-scm.com/docs/gitignore). This is a performance decision — git skips excluded directories wholesale instead of walking them. So this does not work:
build/
!build/keep.me # dead rule — git never looks inside build/ The fix is to exclude the contents, not the directory:
build/*
!build/keep.me # works — build/ itself is still open for inspection 3. Nested .gitignore files win in their scope. Rules in subdir/.gitignore override the root file for paths under subdir. When git check-ignore -v names an ignore source you did not expect, that is usually why.
Why is gitignore not working in VS Code?
Almost never for VS Code reasons. The editor’s Source Control view reads the same index git does, so the symptom is identical: the file was already committed, and no IDE restart changes the index. Two VS Code-adjacent realities are worth knowing:
- Explorer greyed-out = ignored; orange/yellow = tracked with changes. A file showing as modified after you ignored it is your confirmation it is tracked — run the
git rm --cachedfix above. - A missing
.gitignorein the Explorer’s changed list means the rule works — it never appears as an untracked file in the first place. People often report “VS Code ignores my gitignore” when the CLIgit statusdisagrees with a stale SCM view; reload the window (Cmd/Ctrl+Shift+P→ “Reload Window”) before blaming git.
.gitignore vs .git/info/exclude vs global: which one when?
| File | Scope | Committed? | Use for |
|---|---|---|---|
.gitignore (repo) | Everyone who clones | Yes | Build output, dependencies, .env — shared rules |
.git/info/exclude | Only your clone | No | Personal clutter: .scratch/, editor droppings |
core.excludesFile (global) | All your repos | No | OS junk: .DS_Store, Thumbs.db, *.swp |
.gitignore + negation | Repo | Yes | Re-including tracked-config exceptions |
The global file is the one most developers never set and should:
git config --global core.excludesFile ~/.gitignore_global
printf '.DS_Store\nThumbs.db\n*.swp\n' >> ~/.gitignore_global House rule worth copying: if a rule benefits the whole team, it belongs in the repo; if it only benefits you, it belongs in exclude or the global file. Committing personal ignore rules is how .gitignore files end up 300 lines long and nobody knows which half still matters.
How do I ignore changes to a tracked file?
Sometimes you want a tracked file (a config template, an IDE settings file) but want local edits to stop showing in git status. Two flags on git update-index, neither of which belongs in a team workflow:
git update-index --skip-worktree config/local.dev # local edits go quiet
git update-index --no-skip-worktree config/local.dev # ...and back --skip-worktree is the defensible one — it says “my local version intentionally diverges.” Its cousin --assume-unchanged is a performance promise (“this file won’t change”), not an ignore mechanism, and git may silently break it. Both flags fail loudly at pull time when upstream also changed the file — the durable answers are a local-only config via exclude at creation time, or a template file (config.example) that git tracks and you copy.
The 30-second gitignore health check
git check-ignore -v <path> # which rule? (silence = tracked, no rule)
git ls-files --error-unmatch <path> # is it tracked at all?
git rm --cached <path> # untrack, keep on disk
git commit -m "stop tracking <path>"
git check-ignore -v <path> # rule now shows Diagnose, untrack, verify. The pattern behind every “gitignore not working” report is the same file wearing two hats — tracked on one side of the index, ignored on the other — and one --cached flag takes the second hat off.
FAQ
Why is .gitignore not working?
In most cases the file is already tracked. Git only ignores untracked files; a file in the index is immune to .gitignore until you untrack it with git rm --cached and commit.
How do I check which gitignore rule matches a file?
Run git check-ignore -v <path>. It prints the exact .gitignore line and rule number, or exits silently when the file is tracked and no rule applies.
Does git rm --cached delete my local file?
No. --cached removes the file from the index only; the copy on disk stays. Commit the removal and the file becomes untracked, after which .gitignore applies.
Why can't gitignore re-include a file inside an ignored folder?
Git skips excluded directories entirely for performance. Per the git docs, it is impossible to re-include a file if a parent directory of that file is excluded.
— mrsaynothing
Ollama Not Using GPU? Fix It on Linux, Windows and WSL
Enjoying the write-ups? I build like this for a living. hire me