Articles
Published · October 26, 2026

.gitignore Syntax Explained: Wildcards, Negation, and Directory Rules

A .gitignore is one pattern per line, and a few small rules decide what matches: wildcards, slashes, double asterisks, and negation. Here is how each one behaves.

Quick answer

A .gitignore holds one pattern per line. * and ? match within a single path segment and never cross a /. A slash at the start or middle of a pattern anchors it to the folder holding the .gitignore, and a slash at the end limits it to directories. ** spans folders, ! re-includes a path, and the last matching line wins. The one hard limit: you cannot re-include a file whose parent directory is excluded.

How it actually works

Git reads each line as a pattern, matched against paths relative to the .gitignore file. The gitignore reference lists the rules; the sections below walk through them with examples.

Comments, wildcards, and ranges

Blank lines match nothing, and a line starting with # is a comment. Trailing spaces are ignored unless you escape them with \. A backslash also escapes a leading # or !, and a literal *.

Three wildcards do the matching:

  • * matches anything except /.
  • ? matches exactly one character except /.
  • [a-zA-Z] matches one character from the range.
# compiled output
*.o
*.log
build-?.txt
temp[0-9].tmp

That ignores every .o and .log file at any depth, build-1.txt but not build-12.txt, and temp0.tmp through temp9.tmp.

Slashes: anchoring and directory-only matches

If a pattern has no slash at the beginning or middle, it can match at any level below the .gitignore. Put a slash at the start and it only matches next to the file itself.

hello.*     # hello.txt, src/hello.c, a/b/hello.java
/hello.*    # hello.txt and hello.c at the top level only

A slash at the end means "directories only." foo/ matches a directory named foo, but not a regular file or symlink with that name. A pattern with a slash in the middle, like doc/frotz, is already anchored, so a leading slash changes nothing there. And doc/frotz/ matches doc/frotz but not a/doc/frotz.

Mind the * here too. foo/* matches foo/test.json and the directory foo/bar, but not foo/bar/hello.c, because the * cannot cross the slash. Git doesn't need it to. Once foo/bar is ignored, everything inside goes with it.

Double asterisks

Two consecutive asterisks between slashes let a pattern cross directory boundaries:

**/logs      # a "logs" folder at any depth (same as plain "logs")
abc/**       # everything inside abc, at any depth
a/**/b       # a/b, a/x/b, a/x/y/b, and so on

Any other run of consecutive asterisks acts like a single *.

Negation and the parent-directory trap

A leading ! re-includes something an earlier pattern excluded. Order matters, because the last matching pattern wins:

# ignore generated HTML...
*.html
# ...except the one maintained by hand
!foo.html

Reverse those two lines and foo.html is ignored again.

The trap: Git doesn't look inside an excluded directory, so a ! rule for anything inside it has no effect, wherever you write it. This pair fails:

build/
!build/keep.txt

The way out is to ignore the directory's contents rather than the directory itself. The Git documentation shows the full recipe for excluding everything except foo/bar:

/*
!/foo
/foo/*
!/foo/bar

The /* is deliberate; without the slash, the wildcard would also exclude everything inside foo/bar. A nested .gitignore can also override a parent, since lower-level files take precedence. Adding !/vmlinux* in a subfolder un-ignores a file the root rule vmlinux* caught.

When to use it (and when to skip it)

Where a rule lives matters as much as how it is written. Git checks command-line patterns first, then .gitignore files (closest folder wins), then .git/info/exclude, then the file named by core.excludesFile, which defaults to ~/.config/git/ignore. Use .gitignore for rules every contributor should share. Use .git/info/exclude for rules that belong to one clone, and the global file for your editor's backup files in every repo. If you keep your configs under version control, a dotfiles repository is a good place to see how much a short global ignore list helps.

Two situations where a new rule seems to do nothing:

  • The file is already tracked. Ignore rules only affect untracked files. GitHub's guide says to untrack it first with git rm --cached FILENAME, then commit.
  • You can't tell which rule applies. Ask Git. `git check-ignore -v` prints <source>:<linenum>:<pattern> and the path, so you see the exact file and line responsible.
git check-ignore -v build/main.o
# .gitignore:3:*.o	build/main.o

The exit status is 0 if a path is ignored and 1 if not. Tracked files are skipped unless you add --no-index.

Note that a .gitignore won't help with a secret that's already committed. Scrubbing it from history is a separate job. For tidy repos in general, see writing a good commit message and choosing a LICENSE file.

How Carets fits in

A .gitignore is a plain text file with no extension, and it's easy to break with one stray space or slash. Carets is a native notes and code editor for iPhone, iPad, and Mac, so you can open the file, read the patterns with syntax highlighting, and fix a line without launching a full IDE. Because your files stay plain text, the same .gitignore, README, and config files sit in one project, organized with files, projects, and tags. Download Carets on the App Store for iPhone, iPad, and Mac.

Frequently Asked Questions

Why is my .gitignore not ignoring a file?

Usually the file is already tracked. Adding a rule does not untrack it; run git rm --cached on the file and commit. Otherwise a later pattern may re-include it, or an anchoring mistake means the pattern never matches. git check-ignore -v shows which rule applies.

What is the difference between foo and foo/ in a .gitignore?

A trailing slash makes the pattern match only directories. foo matches a file or directory named foo at any level, while foo/ matches only directories, not a file or symlink with that name.

Why does a negation pattern with ! not work?

Git cannot re-include a file if a parent directory is excluded, because it never looks inside excluded directories. Ignore the directory's contents with dir/* rather than dir/, then add the ! rule.

Where should I put patterns for my editor's files?

In your global ignore file (default ~/.config/git/ignore) or .git/info/exclude, so the shared .gitignore stays about the project.