Skip to content

Ignoring Files in Git with .gitignore

Every Git repository accumulates files that should never be committed: compiled binaries, dependency directories, editor configuration, operating system metadata, and local environment files that contain secrets. Without a mechanism to exclude them, every git status output and git add . command becomes a manual filtering exercise. The .gitignore file is Git's built-in solution to that problem, and understanding how it works end to end, including its pattern syntax, scope model, and debugging tools, eliminates a class of frustration that affects developers at every experience level.

What .gitignore Does

A .gitignore file tells Git to leave certain files untracked. When a file matches a pattern in .gitignore, Git behaves as if the file does not exist for the purposes of git status, git add, and git commit. The file remains on disk; it is simply invisible to Git unless you explicitly force it with git add -f.

The key constraint is that .gitignore only affects untracked files. If a file is already tracked in Git's index, adding it to .gitignore has no effect on it. To stop tracking a file that was previously committed, you need to remove it from the index first:

git rm --cached path/to/file

After that, the file will be ignored as expected.

Pattern Syntax

.gitignore uses a glob-based pattern syntax. Each line in the file is one pattern. Blank lines are ignored, and lines that begin with # are comments.

Wildcards and Matching Rules

Pattern Matches
*.log Any file ending in .log in any directory
build/ The directory named build and all of its contents
build Any file or directory named build
**/logs A file or directory named logs anywhere in the tree
**/logs/*.log Any .log file inside any logs directory in the tree
logs/** Everything inside logs/, but not logs/ itself
doc/*.txt .txt files directly inside doc/, not in subdirectories
doc/**/*.txt .txt files in doc/ or any of its subdirectories
?atch Files with any single character before atch, for example patch or catch
[abc].txt Files named a.txt, b.txt, or c.txt
[0-9].txt Files named 0.txt through 9.txt

A trailing / means the pattern only matches directories. A leading / anchors the pattern to the root of the repository, preventing it from matching in subdirectories.

# Only matches build/ at the repository root
/build/

# Matches any directory named build anywhere in the tree
build/

Negation

A pattern that begins with ! negates a previous rule. This is the mechanism for exceptions: ignore a broad category, then re-include specific items.

# Ignore everything in the logs directory
logs/*

# But keep this specific file
!logs/example.log

One constraint applies: you cannot re-include a file if its parent directory is ignored. Git stops descending into ignored directories entirely, so a negation pattern for a file inside a directory that is itself ignored will have no effect.

# This does NOT work: build/ is ignored entirely,
# so nothing inside it can be re-included
build/
!build/output.txt

To re-include specific files inside a directory, ignore the contents rather than the directory itself:

# Ignore everything inside build/
build/*

# Keep this specific file
!build/output.txt

Comments and Blank Lines

# This is a comment. Git ignores this line entirely.

# The blank line above is also ignored.

*.log  # Inline comments are NOT supported. The # and everything after it is part of the pattern.

Inline comments do not work in .gitignore. If a line contains text after the pattern, Git treats the entire line as the pattern, including the # character and everything that follows it.

Scope and Precedence

Git supports four places where ignore rules can live, each with a different scope.

Repository .gitignore

The most common case is a .gitignore file at the root of the repository. Patterns in this file apply to the entire repository relative to its location. This file is committed and shared with all contributors.

Subdirectory .gitignore

You can place a .gitignore file inside any subdirectory. Patterns in that file apply only to files within that subdirectory and its children. This is useful when a subdirectory has its own ignore requirements that do not belong in the root file.

src/
├── .gitignore     # Applies only to files under src/
├── main.go
└── vendor/

Global .gitignore

A global ignore file applies to every repository on your machine, regardless of project. This is the right place for patterns that are specific to your editor, operating system, or development environment and that you would not want to impose on other contributors.

Configure the path to your global ignore file:

git config --global core.excludesFile ~/.gitignore_global

Common candidates for the global file:

# macOS
.DS_Store
.AppleDouble
.LSOverride

# Windows
Thumbs.db
ehthumbs.db
Desktop.ini

# VS Code
.vscode/
*.code-workspace

# JetBrains IDEs
.idea/
*.iml

# Vim
*.swp
*.swo
*~

.git/info/exclude

Each repository has a file at .git/info/exclude that functions like a .gitignore but is never committed and never shared. It is the right place for machine-specific ignores that apply to a single repository but do not belong in the committed .gitignore. The format is identical to .gitignore.

Precedence

When multiple rules could apply to the same file, Git evaluates them in the following order from lowest to highest precedence, with later sources overriding earlier ones:

  1. The global file specified by core.excludesFile
  2. .git/info/exclude
  3. .gitignore files, from the repository root down to the directory containing the file; a .gitignore closer to the file takes precedence over one further up the tree

Within a single file, rules are evaluated top to bottom. The last matching rule wins.

Common Patterns by Ecosystem

Most language ecosystems and build tools have standard artifacts that should not be committed. A well-maintained .gitignore includes the right set from the start.

Go

# Compiled binaries
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test output
*.test

# Build output
/dist/

# Go workspace
go.work.sum

Python

# Byte-compiled files
__pycache__/
*.py[cod]
*$py.class

# Virtual environments
venv/
.venv/
env/

# Distribution and packaging
dist/
build/
*.egg-info/

# Testing
.pytest_cache/
.coverage
htmlcov/

Node.js

# Dependencies
node_modules/

# Build output
dist/
build/

# Environment files
.env
.env.local
.env.*.local

# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

Terraform

# Local .terraform directories
.terraform/

# Terraform variable files (may contain secrets)
*.tfvars
*.tfvars.json

# State files (never commit these; use remote state)
*.tfstate
*.tfstate.*

# Crash logs
crash.log
crash.*.log

# Generated override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json

Starting Point: github/gitignore

GitHub maintains a curated collection of .gitignore templates for dozens of languages and frameworks at github.com/github/gitignore. When starting a new project, grab the relevant template and adapt it rather than building from scratch. Many hosted platforms, including GitHub and GitLab, can pre-populate a .gitignore from these templates during repository creation.

Debugging Ignore Rules

When a file does not behave as expected, git check-ignore identifies which rule is responsible.

Check a Specific File

git check-ignore -v path/to/file

The output shows the file that contains the matching rule, the line number, the pattern, and the path that was matched:

.gitignore:3:*.log    logs/app.log

If the file is not ignored, there is no output and the exit code is non-zero.

Check a File That Should Be Ignored but Is Not

If you added a file to .gitignore and it still appears in git status, the most common cause is that the file is already tracked. Remove it from the index first:

git rm --cached path/to/file

Then confirm the rule is working:

git check-ignore -v path/to/file

List All Ignored Files

To see every file Git is currently ignoring in the repository:

git ls-files --others --ignored --exclude-standard

This is useful for auditing a .gitignore to confirm it is catching what you expect and not accidentally excluding files that should be tracked.

Dry-Run Before Cleaning

git clean removes untracked files from the working tree. Before running it, always use the -n flag for a dry run to confirm what will be deleted:

git clean -ndX

The -d flag includes directories, and -X removes only files ignored by Git. Review the output carefully before running without -n, as this operation cannot be undone.

Combining .gitignore with .gitkeep

A common pattern is to ignore the contents of a directory while preserving the directory itself in version control. Git does not track empty directories, so without a placeholder file the directory will not survive a fresh clone. The .gitignore negation mechanism handles this cleanly when combined with the .gitkeep convention.

# Ignore everything inside logs/
logs/*

# Keep the placeholder file so the directory is tracked
!logs/.gitkeep

Then create the placeholder and commit both files together:

touch logs/.gitkeep
git add logs/.gitkeep .gitignore
git commit -m "chore: track logs directory, ignore its contents"

Anyone who clones the repository gets a logs/ directory ready to use, and nothing written into it will appear in git status or be included in a commit. For more on the .gitkeep convention and why Git ignores empty directories in the first place, see Tracking Empty Directories in Git with .gitkeep.

References