← Gitignore Debugger

Gitignore Folder Not Working? Fix Directory Rules

Folders behave differently from files in Git, and one decision drives everything else: whether your rule excludes the directory itself or only what is inside it. Get that wrong and the rule can look correct while quietly refusing to let you keep anything, or refusing to apply at all.

The one thing to internalise. Git does not walk into a folder it has excluded. Every folder-specific surprise —.gitkeep vanishing, a ! line that does nothing, a nested .gitignore that is never read — follows from that. Ignore the folder when you want it gone entirely; ignore itscontents when you need to keep anything inside.

Two rules that look alike and are not

build/ and build/* both stop the folder's files from showing up. They differ in whether the directory itself is excluded, and that difference decides everything downstream:

rule        build/ itself   files inside, any depth
build/      excluded        excluded
build/*     not excluded    excluded
build/**    not excluded    excluded

All three hide the contents, so a quick git status makes them look interchangeable. The distinction only surfaces later, when you try to re-include something: with build/ the directory is gone and nothing inside can be reconsidered, while withbuild/* the directory is still walked and exceptions are possible.

Between build/* and build/** there is no practical difference. * cannot cross a slash on its own, but it does match the subdirectories, and once those are excluded their contents go with them — so both end up covering the whole subtree.

Where the rule matches: name, depth, anchor

A folder rule with no slash other than the trailing one matches by name at any depth. Add a slash elsewhere and it becomes anchored relative to the.gitignore holding it:

rule            build/   a/build/   a/b/build/
build/          yes      yes        yes        ← name, any depth
**/build/       yes      yes        yes        ← identical to the above
/build/         yes      —          —          ← top level only
a/build/        —        yes        —          ← exactly this path
*/build/        —        yes        —          ← exactly one level down
a/**/build/     —        yes        yes        ← under a/, any depth

Two takeaways. **/build/ buys you nothing overbuild/ — the unanchored form is already recursive, which is the opposite of what most people assume. And */build/ meansexactly one directory in between, whereasa/**/build/ means zero or more, so it also matchesa/build/ itself.

If your folder rule is matching more than you intended, a leading slash is usually the fix. If it is matching less, you have probably anchored it without meaning to by including a slash mid-pattern.

Git never looks inside an excluded folder

This is the rule that explains the most confusing folder failures. Once a directory is excluded Git stops there — it does not read the files inside, and it does not read any .gitignore inside either:

# .gitignore
build/

# build/.gitignore
!keep.txt          ← never read; build/keep.txt stays ignored

The negation is not wrong, it is unreachable. Move it to an ignore file Git still visits and it works, because the folder above is being walked normally:

# .gitignore
logs/

# a/.gitignore
!logs/             ← this DOES re-include a/logs/ and its contents

So the question to ask about any ! line is not whether the pattern is right, but whether Git gets far enough to read it.

A nested .gitignore governs only its own folder

Rules in a nested ignore file apply to that folder and its descendants, and nowhere else. A logs/ rule in a/.gitignorehides a/logs/ and leaves a logs/ folder at the repository root fully visible.

That is a feature when a subproject needs its own rules, and a trap when you half-remember writing a rule and cannot find why it does not apply. Repository-wide folder rules belong in the root.gitignore.

Keeping something inside an ignored folder

Because negation cannot reach into an excluded directory, whitelisting starts with ignoring the contents rather than the folder:

build/*
!build/keep.txt          → keep.txt is visible, everything else stays ignored

The same reasoning applies again one level down, which catches people who already know the trick. build/* also excludes the subdirectory build/sub, so a rule pointing inside it is unreachable until that directory is re-included too:

build/*
!build/sub/              → re-includes the directory, and keep.txt with it

And for completeness, the two shapes that do not work — both blocked by the same traversal rule, whether the exception names a file or a pattern:

build/
!build/keep.txt          → still ignored
build/
!build/*.txt             → still ignored

Empty folders cannot be committed at all

Git records files, not directories. A folder with nothing in it does not appear in git status and cannot be committed, no matter what your .gitignore says — so if you are trying to preserve a directory's existence, that is a separate problem from ignoring it.

The convention is a placeholder file, typically.gitkeep. Combining the two goals means ignoring the contents and re-including the placeholder:

build/*
!build/.gitkeep          → the folder exists in the repo, its output does not

Note that build/* does match .gitkeep — unlike your shell, gitignore wildcards do not skip dotfiles — which is exactly why the second line is needed. Swap the first line forbuild/ and the placeholder disappears along with everything else.

What git status is actually telling you

Git collapses a directory whose contents are entirely untracked into a single entry, which is why an unignored folder shows up as one line rather than a list:

$ git status --porcelain
?? mixed/

$ git status --porcelain --untracked-files=all
?? mixed/a.txt
?? mixed/b.log

That matters for diagnosis. A folder appearing as one collapsed?? folder/ line means Git considers the whole thing untracked, so your rule is not matching the folder at all — check the anchoring first. Expand with --untracked-files=all to see whether some files are covered and others are not.

git add gives you a second signal. When a directory rule is what blocks a file, the error names the directory rather than the file you asked for:

$ git add build/x.txt
The following paths are ignored by one of your .gitignore files:
build
hint: Use -f if you really want to add them.

Seeing build there rather than build/x.txt tells you the decision was made at the folder level. git add -foverrides it for that one file if you genuinely need the exception.

A folder you already committed

Ignore rules only ever apply to untracked paths, so addingbuild/ does nothing to files inside it that are already in the repository. They keep being tracked, and the rule appears broken. The folder version of the fix needs -r to recurse:

$ git rm -r --cached build
$ git commit -m "Stop tracking build/"

--cached removes the files from Git's index while leaving them on disk, so nothing is deleted from your working tree — the commit records the removal, and from then on the ignore rule takes over. Anyone pulling that commit will lose the files locally, which is worth mentioning in the commit message.

Confirm which rule decided

Rather than reasoning about anchoring, ask Git. It accepts a folder path and reports the file, line and pattern responsible:

$ git check-ignore -v build
.gitignore:1:build/    build

$ git check-ignore -v build/sub/y.txt
.gitignore:1:build/    build/sub/y.txt

Querying the folder itself is the quicker test — if no pattern is reported for build but one is reported for a file inside it, your rule is a contents rule rather than a folder rule, and negation will behave accordingly. Silence for a file that is still showing up usually means that file is already tracked. Read the printed pattern rather than the exit code, which with -v is 0 whenever any pattern matched, including a negation that leaves the path visible.

For the wider set of reasons an ignore rule can fail to bite, including tracked files and ignore-file placement, seegitignore not working.

Check a folder rule before you commit it

Folder rules are hard to eyeball because the failure shows up later, in something you cannot re-include. Paste a path and your rules into IgnoreDebugger and it reports which line decided and why — useful for checking anchoring, for reviewing a.gitignore in someone else's pull request, or for reasoning about a directory that only exists on a colleague's machine. It runs entirely in your browser; nothing is uploaded.

Open the Gitignore Debugger

Frequently asked questions

What is the difference between build/ and build in .gitignore?

The trailing slash restricts the rule to directories. build/ matches the directory and everything in it, but leaves a file named build alone. Without the slash, build matches either one.

For a folder rule the slash is the better choice: it says what you mean, and it prevents a stray file of the same name from being ignored by accident.

Is **/build/ different from build/?

No — they behave identically. A pattern whose only slash is the trailing one is not anchored, so it already matches by folder name at every depth: build/ catches build/, a/build/ and a/b/build/ alike.

The **/ prefix is only worth writing when you want to be explicit for a human reader. What actually changes the behaviour is a leading slash, which pins the rule to the top level.

How do I ignore build/ at the top level but not src/build/?

Add a leading slash: /build/. That anchors the rule to the directory holding the .gitignore, so a nested folder with the same name is untouched.

If you want the opposite — one specific nested folder only — spell out the path instead, as in src/build/. Any pattern with a slash somewhere other than the end is anchored the same way.

Does a .gitignore inside an ignored folder do anything?

No. Git does not descend into a folder it has excluded, so a .gitignore placed inside one is never read. With build/ in the root ignore file, adding !keep.txt to build/.gitignore changes nothing.

A negation has to live in a .gitignore that Git actually reaches — that is, one in a folder that is not itself ignored. Putting !logs/ in a/.gitignore does re-include a/logs/ when the root ignores logs/, because a/ is still being walked.

Does a nested .gitignore in a subfolder affect the whole repository?

No. Its rules are scoped to its own folder and everything below it. A logs/ rule written in a/.gitignore ignores a/logs/ and leaves a logs/ folder at the repository root visible.

That scoping is what makes nested ignore files useful for per-package rules, and it is also why a rule you remember writing may not apply where you expect it to.

How do I commit an empty folder in Git?

You cannot. Git records files, not directories, so a folder with nothing in it never appears in git status and cannot be committed. The convention is to put a placeholder file inside it, usually named .gitkeep.

To keep the placeholder while ignoring everything else in the folder, ignore the contents and re-include the file: build/* followed by !build/.gitkeep. Writing build/ on the first line instead would ignore the folder itself, and the placeholder with it.

Why does git status show my folder as one line instead of the files inside it?

Because Git collapses a directory whose contents are all untracked into a single ?? folder/ entry, to keep the output short. It is reporting the folder, not any particular file in it.

Run git status --untracked-files=all to expand it. If a folder you have ignored still appears as one collapsed line, at least one file in it is not covered by your rule.

Why does git add say my file is ignored when I never named that folder?

Read the path in the error carefully — when a directory rule is responsible, Git names the directory rather than the file you asked it to add. git add build/x.txt reports “The following paths are ignored” followed by build.

That naming is a useful tell: it says the decision was made at the folder level, so the fix belongs in the folder rule, not in a file pattern.