.gitignore not working: why Git still tracks the file, and how to fix it
.gitignore is ignored because the file is already tracked, the pattern is wrong or the file has the wrong encoding. Find the cause with git check-ignore -v.
by OhSudo ··
Part of a learning path:Git for everyday work
You added node_modules/, .env or *.log to .gitignore, and git status still shows the file as modified — or it is still pushed with every commit. .gitignore isn’t broken. In almost every case it’s one of these:
- The file is already tracked.
.gitignoreonly affects files Git doesn’t track yet. - The pattern doesn’t match the path you think it matches.
- The
.gitignorefile itself can’t be read — wrong place, or saved as UTF-16 by PowerShell.
One command tells you which: git check-ignore -v shows the line that ignores a file, or nothing if no line does.
- A new fileuntrackedchecked against
- .gitignorepattern matches it(works)but not(fails)
- Already tracked filesnot affected: git rm --cached(breaks here)
TL;DR
git check-ignore -v path/to/file # which .gitignore line matches (no output = none)git ls-files --error-unmatch path/to/file # succeeds if Git tracks the filegit rm --cached path/to/file # stop tracking it, keep it on diskgit rm -r --cached node_modules # the same for a foldergit commit -m "Stop tracking ignored files"1. The file is already tracked
This is the cause nine times out of ten. Once a file has been committed, Git keeps tracking it — .gitignore only decides what new, untracked files to leave out. Check:
git ls-files node_modules | head -n 3 # any output: Git tracks files in theregit ls-files -ci --exclude-standard # every tracked file your .gitignore says should be ignoredRemove it from the index (Git’s list of tracked files) without deleting it from your disk, and commit:
From now on, .gitignore applies to it. To do this for everything at once — after a big .gitignore update — remove the whole index and add everything back; the files your .gitignore matches are left out:
Warning
git rm --cached deletes the file for everyone else. When your teammates pull that commit, Git removes the file from their working folders — it’s a deletion in the history. For something like a shared config/local.yml, warn them first, or commit a config/local.example.yml they can copy. And if the file contains secrets, ignoring it now doesn’t remove it from history: change the secret first.
2. The pattern doesn’t match
Patterns are relative to the folder of the .gitignore they’re in, and a few characters change their meaning:
| Pattern | Matches |
|---|---|
debug.log |
debug.log in any folder |
/debug.log |
only debug.log next to this .gitignore |
build/ |
folders named build, anywhere — not a file named build |
docs/*.md |
.md files directly in docs/, not in docs/api/ |
docs/**/*.md |
.md files in docs/ and every folder below it |
!keep.log |
re-includes a file an earlier pattern excluded |
Test a path instead of guessing — the file doesn’t even need to exist with --no-index:
.gitignore:3:*.log logs/app.logThat reads: line 3 of .gitignore, the pattern *.log, ignores logs/app.log.
One rule surprises everybody: you can’t re-include a file if its folder is excluded. With
config/!config/app.example.ymlconfig/app.example.yml stays ignored, because Git doesn’t even look inside an excluded folder. Exclude the folder’s contents instead:
config/*!config/app.example.yml3. The .gitignore file itself is the problem
- Wrong place or name. It must be named exactly
.gitignore(notgitignore, not.gitignore.txt— Windows Explorer likes to hide extensions) and live in the repository or in a folder of it. Patterns in it start from that folder. - UTF-16 from PowerShell. In Windows PowerShell 5.1,
echo "node_modules/" > .gitignorewrites the file as UTF-16, which Git doesn’t read as text — so none of the patterns match. PowerShell 7 writes UTF-8 and doesn’t have this problem. Check and fix it:
file .gitignore # "UTF-16" = the problem (Git Bash, WSL, Linux, macOS)(Get-Content .gitignore) | Set-Content -Encoding utf8 .gitignore # convert it (Windows PowerShell)- A rule elsewhere. Patterns can also come from
.git/info/exclude(this clone only) or a global file (git config --global core.excludesFile).git check-ignore -vshows which file the matching line is in.
4. Tracked, but you want to ignore your local changes
Sometimes the file must stay in the repository — a shared config with defaults — but you change it locally and don’t want to commit that. .gitignore can’t do that. Tell Git to leave your copy alone:
git update-index --skip-worktree config/settings.json # ignore my local changesgit update-index --no-skip-worktree config/settings.json # undoBetter long term: commit a template (settings.example.json), ignore the real file, and have everyone copy the template.
Start from a complete .gitignore
Most “it’s still tracked” stories begin with a .gitignore that was added after the first commit. Add it first, for your whole stack — the .gitignore & .dockerignore Generator writes one for your languages, editors and operating systems, with the right exceptions (.env.example, the Gradle wrapper, .terraform.lock.hcl) kept in Git.
Shell, we have a problem!
Similar, but different: The following paths are ignored by one of your .gitignore files appears when you run git add on an ignored file — Git is doing what you asked. Use git add -f file if you really want it tracked. And a rejected push after cleaning up is a different story: git push rejected (fetch first).
$ ./practice.sh
Practice this in the terminal
Open the Terminal Playground with this article’s scenario loaded. It is safe and runs in your browser.
Found this useful? Share it.
Found a mistake? Let us know at contact@ohsudo.com.