Language:English
DEV TOOL

Gitignore Debugger

Find out exactly why Git isn't ignoring your file.

Paste a file path and your .gitignore rules. We'll show you which rule matches and why.

Runs entirely in your browser. Your files are never uploaded.

What is a .gitignore file?

A .gitignore file is a plain text file that tells Git which files to leave out of version control. Each line is a pattern — *.log, node_modules/, .env — and any untracked path matching a pattern is hidden from git status, skipped by git add ., and never committed.

The word untracked is carrying most of the weight in that sentence, and it explains more cases of gitignore not working than every pattern bug combined. Git only consults .gitignore for files it is not already tracking. Once a file has been committed, Git keeps tracking it forever, no matter what you add to .gitignore afterwards.

What the Gitignore Debugger does

Paste a file path and your .gitignore contents, and this .gitignore debugger reports the same verdict Git would reach — ignored or not ignored — along with the specific rule and line number responsible. It then lists every rule that matched the path, in the order Git evaluates them, and marks the one that actually decided the outcome.

That last part is what a plain gitignore checker usually leaves out. Learning that a file is ignored is rarely the hard part. Learning which of your forty rules did it — and why the rule you expected to win lost — is. It works in both directions, too: why Git is ignoring your file when you want it committed, and why it is not ignoring one you want gone.

The matcher reimplements Git's own pattern rules: basename versus anchored patterns, * never crossing a /, the three ** forms, directory-only / suffixes, character classes, escaped trailing spaces. Its behaviour is checked against real git check-ignore output rather than guessed at.

How to use it

  1. Enter the path as it sits in your repository, relative to the repo root — src/config/local.json, not C:\projects\app\src\config\local.json. Use forward slashes; Windows backslashes are converted for you.
  2. Paste your whole .gitignore. Resist trimming it down to the one rule you suspect — precedence between rules is usually the actual bug, and cutting the file throws that information away.
  3. Read the verdict, then read the rule chain beneath it. The chain is where a gitignore rule not working stops being a mystery.

To test gitignore rules against a directory rather than a file, end the path with a slash — build/. That distinction matters, because a rule ending in / only ever matches directories.

Why gitignore rules sometimes don't work

Roughly in order of how often each one turns out to be the culprit:

Gitignore rule precedence: the last match wins

This is the one that surprises people. Within a single .gitignore, the last pattern that matches a path decides its fate — not the first. These two files contain identical rules and produce opposite results for important.log:

*.log
!important.log        → important.log is committed
!important.log
*.log                 → important.log is ignored

So put broad rules first and exceptions after them. Across files, a .gitignore in a subdirectory overrides one higher up for paths beneath it. Git also reads .git/info/exclude, which is repo-local and never committed, and your global core.excludesFile. A rule you cannot find in any .gitignore is often hiding in one of those two — git check-ignore -v prints the file and line that decided.

Negation rules, and why ! often fails

Prefixing a pattern with ! re-includes what an earlier rule ignored. It has two limits, and the second accounts for nearly every report of gitignore negation not working.

First, something earlier has to have ignored the path. !important.log on its own does nothing, because there was never anything to override.

Second, and far more surprising: you cannot re-include a file inside an ignored directory. This does not work, and no amount of reordering will fix it:

build/
!build/keep.txt

When build/ is ignored, Git never descends into it. It does not examine build/keep.txt, so it never reaches line 2. Ignore the directory's contents instead, which leaves the directory itself walkable:

build/*
!build/keep.txt

To rescue something deeper, every intermediate directory has to stay reachable as well — build/*, then !build/assets/, then !build/assets/logo.svg.

Ignored parent directories

Git's directory walk stops at the first ignored directory it meets, and everything below that point inherits the verdict without being examined. That is why an ignored parent beats any rule written for the file itself, however specific that rule looks.

When this happens, the debugger names the parent that blocked the walk rather than just saying the file is ignored, and it still shows the negation rules you wrote for the file — flagged as unreachable, because Git never got far enough to consider them. Fix the directory first and the rules below it start working on their own.

Common .gitignore mistakes

A few worked examples

Path src/logs/debug.log against these rules is not ignored. The negation is the last match, and no parent directory blocks the walk, so it is reached and it wins:

*.log
!src/logs/debug.log

Path build/keep.txt against these rules is ignored, decided by build/ on line 1 matching the parent directory. Line 2 never fires:

build/
!build/keep.txt

Path docs/api/notes.md against this rule is not ignored, because the pattern is anchored by its slash and * will not cross the one in api/notes.md. docs/**/*.md is what was meant:

docs/*.md

Paste any of these into the gitignore tester above to see the full rule chain. When you are back at a terminal, git check-ignore -v path/to/file is the built-in equivalent — it names the source file, line number and pattern that decided, and it is worth knowing.

Frequently asked questions

Why is Git ignoring my file?

Some pattern matched it, and it is often not the one you are looking at. The usual culprits are a .gitignore in a subdirectory, an ignored parent directory, or your global ignore file. Run git check-ignore -v path/to/file — it prints the exact source file, line number and pattern that decided. Pasting the path and rules into the debugger above shows the same thing plus the rules that matched and lost.

Why is my .gitignore rule not working?

Four causes cover nearly every case, in rough order of frequency: the file is already tracked, so the rule is skipped entirely; a later rule overrode it, because the last match wins; a parent directory is ignored, so Git never reached the rule; or the pattern is anchored differently than you assumed — a slash anywhere but the end anchors a pattern to the directory holding the .gitignore.

Why doesn't ! re-include my file?

Either nothing earlier ignored the path, so the negation had nothing to override, or — far more likely — the file sits inside an ignored directory. Git does not descend into ignored directories, so it never sees the file and never evaluates your ! rule.

Ignore the directory's contents rather than the directory itself: replace build/ with build/*, keep !build/keep.txt after it, and the negation is reached.

Does the last .gitignore rule win?

Yes. Within one file the last pattern that matches a path decides its fate — not the first, and not the most specific. *.log followed by !important.log keeps important.log; swap those two lines and it is ignored. Put broad rules first and exceptions after them.

Why does an ignored folder prevent a negation rule from working?

Because Git's directory walk stops at the first ignored directory. When build/ matches, Git marks the whole subtree as ignored without listing what is inside, so !build/keep.txt is not overridden — it is simply never reached. Reordering cannot fix that; only making the directory walkable again can, which is what build/* does.

How do I test a .gitignore rule?

Paste the file path and your rules into the debugger at the top of this page. It reports the verdict, the deciding rule and line, and every rule that matched along the way — including negations that could never take effect.

Against a real repository, git check-ignore -v path/to/file names the source file, line and pattern responsible, and git status --ignored lists everything currently being ignored.

Does .gitignore affect already-tracked files?

No. .gitignore applies only to untracked paths. Once a file has been committed, Git keeps tracking it and the rule is skipped — which is the most common reason a perfectly correct rule appears to do nothing. To stop tracking a file without deleting it, run git rm --cached path/to/file and commit that change.

One thing that trips people up here: git check-ignore will still report a match for a tracked file, because it tests patterns without consulting the index. A "yes, ignored" from that command does not mean Git has stopped tracking the file.

Can .gitignore rules contain spaces?

Yes, in the middle. my logs/ matches a directory whose name contains a space, with no escaping needed. Trailing spaces are a different matter: Git strips them, so *.log followed by a space behaves exactly like *.log. To match a filename that genuinely ends in a space, escape it — foo\ .

Leading spaces are significant and are never stripped, so an accidentally indented rule matches only paths that begin with that space. If a rule seems inert for no reason, check for stray indentation.

Where does Git look for ignore rules besides .gitignore?

Three other places, consulted in this order of precedence: .gitignore files themselves, with the deepest directory winning; then .git/info/exclude, which is repo-local and never committed; then whatever core.excludesFile points at, your global ignore list.

So a rule you cannot find anywhere in the repository is usually in one of the last two. git config --get core.excludesFile reveals the global path, and git check-ignore -v names whichever file actually decided.

What is the difference between * and **?

* matches any run of characters except /, so it never leaves a single path segment: src/*/test.js reaches exactly one level down.

** crosses separators, but only in the three positions Git recognises — a leading **/ for any depth, a trailing /** for everything inside a directory, and /**/ in the middle for zero or more directories. Anywhere else, as in a**b, the extra asterisks behave like a single *.