← Gitignore Debugger

Gitignore Not Ignoring File? Here's How to Fix It

You have written the rule. The file is still there ingit status, in the staging area, in the diff. What follows is a diagnostic order of operations for one stubborn file — each step rules out a cause, so by the end you either have the answer or a single command that gives it to you.

Establish this first. Rungit ls-files --error-unmatch path/to/file. If it prints the path, the file is tracked and no ignore rule will ever apply to it — skip to step 1. If it errors, the file is untracked and you are genuinely debugging a pattern: start at step 2.

1. The file is tracked — which includes merely staged

.gitignore decides which untracked files Git pays attention to. Once a path is in the index, Git treats its fate as already settled and stops consulting the ignore rules for it. The rule you wrote is correct and inert.

The part that catches people out is that tracked does not meancommitted. A file staged with git add — or swept up by a git add . you ran before writing the rule — is in the index and therefore exempt, even though it has never been committed:

$ git add staged.log            # never committed, just staged
$ git check-ignore -v staged.log
                                # nothing. exit 1.
$ git check-ignore -v --no-index staged.log
.gitignore:1:*.log    staged.log

That pair of commands is the clearest possible diagnosis: the pattern matches fine, and the index is what's overriding it. Ingit status --short the same file shows asA rather than ??. Either way the fix is to take it out of the index and leave it on disk:

git rm --cached path/to/file
git commit -m "Stop tracking path/to/file"

If it was only staged and never committed,git rm --cached is enough on its own and there is nothing to commit. For a directory, add -r.

2. The path Git tests isn't the path you typed

Assuming the file really is untracked, the next question is what path the pattern is being compared against. Git matches patterns against the file's path relative to the directory holding the.gitignore, and whether a pattern is anchored to that directory or floats to any depth depends entirely on whether it contains a slash.

Four rules that all look like they targetdebug.log, against the same three files:

rule             debug.log   src/debug.log   src/logs/debug.log
debug.log        ignored     ignored         ignored
/debug.log       ignored     —               —
src/debug.log    —           ignored         —
**/debug.log     ignored     ignored         ignored

So a pattern with no slash at all matches by basename everywhere. A pattern with a slash anywhere but the endis anchored, and matches at exactly one depth. If your rule issrc/debug.log and the file is actually atsrc/logs/debug.log, the rule is doing precisely what it says and precisely not what you wanted.

3. The pattern doesn't match the filename

Once the depth is right, the pattern text itself is next:

4. A wildcard stopped at a directory boundary

* matches any run of characters except the directory separator, which is the usual reason a rule under-matches by one level:

src/*.log      → matches src/debug.log, but not src/logs/debug.log
src/**/*.log   → matches both

? is the single-character equivalent and stops at/ too. ** is only meaningful in three positions — leading (**/debug.log), trailing (src/**) and between two slashes (src/**/debug.log). Anywhere else it degrades to a plain*, so debug**.log gains you nothing.

5. A nested .gitignore is deciding instead

Rules from a .gitignore closer to the file take precedence over rules from further up, and their patterns are relative to their own directory. A leading slash therefore anchors to that directory, not the repository root:

src/.gitignore   contains  /debug.log

$ git check-ignore -v src/debug.log
src/.gitignore:1:/debug.log    src/debug.log
$ git check-ignore -v src/logs/debug.log
                               # no match — the rule is anchored to src/

The same mechanism lets a nested file re-include something the root ignored, which is worth checking when a rule seems to be working everywhere except one subtree. To list every ignore file in the repository at once:

git ls-files --cached --others --exclude-standard -- "*.gitignore"

Two more sources never show up in that listing:.git/info/exclude, which is repo-local and uncommitted, and your global excludes file — git config --get core.excludesFile.

6. A negation rule re-included it

If the file is ignored almost everywhere but not here, look for a!. The last matching pattern wins, so a negation placed after a broad rule carves an exception out of it:

*.log
!src/logs/debug.log

$ git check-ignore -v debug.log
.gitignore:1:*.log                 debug.log            ← ignored
$ git check-ignore -v src/logs/debug.log
.gitignore:2:!src/logs/debug.log   src/logs/debug.log   ← re-included

Both files match *.log; the second one also matches the negation, and because the negation is later it decides. If that exception is not what you want, move it above the broad rule or delete it. Ordering is the whole mechanism here — broad rules first, exceptions after.

7. Ask Git which rule decided

Every step above is a hypothesis. This is the answer, and it is one command:

$ git check-ignore -v src/logs/debug.log
.gitignore:3:*.log    src/logs/debug.log

The fields are the source file, the line number and the pattern, then a tab and the path you asked about. Three things make it far more useful than it first appears:

git status --ignored gives the complementary view, listing every path currently being excluded — useful for spotting a whole directory you never meant to catch. For the broader tour of why ignore rules misfire, including folder rules and whitelisting, seegitignore not working.

Check a rule without touching the repository

git check-ignore needs a repository, a terminal and a real path on disk. When you are reviewing a.gitignore in someone else's pull request, drafting a rule you have not committed, or reasoning about a path that only exists on a colleague's machine, paste the path and the rules into IgnoreDebugger instead. It reports the deciding line and why it won, and it runs entirely in your browser — nothing is uploaded.

Open the Gitignore Debugger

Frequently asked questions

Why does Git still show a file that matches my .gitignore rule?

Because the rule is almost certainly correct and simply not being consulted. Git only applies .gitignore to files it is not already tracking, so a file that is committed — or merely staged — is exempt from the ignore rules entirely.

Check with git ls-files --error-unmatch path/to/file. If it prints the path, untrack it with git rm --cached path/to/file and commit.

How do I tell whether a file is tracked or untracked?

git ls-files --error-unmatch path/to/file prints the path if Git is tracking it and exits with an error if not. In git status --short, an untracked file is marked ??, while a tracked one shows a staged or modified status such as A or M.

The distinction is the single most useful thing to establish first, because it decides whether you are debugging a pattern at all.

Why does .gitignore ignore some files but not others?

Usually because a later rule re-includes the ones that escape. Within a .gitignore the last matching pattern decides, so *.log followed by !src/logs/debug.log ignores every log except that one.

The other common cause is anchoring: a pattern containing a slash matches at one specific depth only, so it catches the file in the location you wrote and misses every copy of it elsewhere in the tree.

Does a staged file bypass .gitignore?

Yes. Staging with git add puts the file in the index, and anything in the index is tracked as far as the ignore rules are concerned — it does not need to have been committed yet.

This catches people who run git add . before writing the ignore rule. Unstage it with git rm --cached path/to/file and the rule starts applying immediately.

Can a difference in capitalisation stop a rule from matching?

Yes, and whether it does depends on the machine. Gitignore patterns are matched case-sensitively unless core.ignorecase is on, so the rule *.LOG will not match debug.log on a case-sensitive filesystem.

Git sets core.ignorecase automatically when the repository is created, based on the filesystem — typically on for Windows and macOS, off for Linux. That is why a rule can work locally and fail on a Linux CI runner. Write patterns in the same case as the files.

Which .gitignore wins when there is more than one?

The one closest to the file. Rules in a subdirectory's .gitignore override rules from any directory above it for the paths beneath it, so a forgotten nested file can quietly countermand the root ignore file.

git check-ignore -v path/to/file names the file and line number that actually decided, which is the fastest way to find the one you were not looking at.

Does .gitignore work on files in subdirectories?

It does, but the pattern decides how deep. A pattern with no slash matches by basename at any depth, so debug.log catches the file in the root and in every subdirectory alike.

Add a slash and you anchor it: /debug.log matches only the root copy, and src/debug.log only that exact path. Use **/debug.log when you want every depth explicitly.

Why does the same .gitignore behave differently on another machine?

Two reasons account for nearly all of it. The first is core.ignorecase, which makes pattern matching case-insensitive on Windows and macOS but not on Linux.

The second is that not all ignore rules live in the repository. .git/info/exclude is repo-local and never committed, and each developer has their own global core.excludesFile. A file ignored only on your machine is usually being caught by one of those.