Gitignore Not Working? Here's the Real Fix

Gitignore Not Working? Here's the Real Fix

Gitignore not working? Tracked files bypass .gitignore. Untrack with git rm --cached, verify with git check-ignore, and stop the accidental commit loop.

HUMANby @mrsaynothing39d8 min read

If .gitignore is not working, run the health check in the next section first — more than nine times out of ten it ends the investigation in thirty seconds. Rule order, negation traps and VS Code red herrings explain the remaining cases.

The 30-second gitignore health check

.gitignore not working? git check-ignore -v <path> the silence is the diagnostic prints a rule silence a rule applies — the wrong one last matching rule wins negation inside ignored dir? exclude contents, not the dir rule in the wrong scope? repo vs exclude vs global no rule applies — tracked anyway git ls-files --error-unmatch <path> git rm --cached <path> commit — the rule now owns the path was it a secret? untracking is not enough — rotate the credential
fig 1 — the gitignore decision tree. check-ignore -v sends you left (rule problem) or right (tracked); click your branch.

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.

Each row is a cause; the third column is the fix.

all tracked negation rule order scope editor
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

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.

verify: git check-ignore -v debug.log → .gitignore:3:*.log debug.log

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.

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 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.

verify: git ls-files | grep node_modules → empty · git check-ignore -v .env → the rule line

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 --cached fix above.
  • A missing .gitignore in 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 CLI git status disagrees 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?

Scope decides where a rule belongs.

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.

verify: git ls-files -v config/local.dev → lowercase s = skip-worktree is on

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. The rest of the git recovery hub covers the moments gitignore arrives too late.