.gitignore Patterns
Comprehensive guide to .gitignore syntax, pattern matching rules, global ignores, negation, and curated patterns for every major tech stack and framework.
Introduction
The .gitignore file is Git’s first line of defense against committing the wrong things. Build artifacts, dependency directories, IDE configurations, environment files with secrets, OS metadata — all of these belong in .gitignore, not in your repository.
Yet .gitignore syntax is deceptively simple. Many developers struggle with patterns that don’t work as expected, files that should be ignored but aren’t, and the mysterious behavior of negation patterns. Understanding how Git matches ignore patterns is essential for maintaining clean repositories.
This comprehensive guide covers every aspect of .gitignore: the pattern syntax, precedence rules, global ignores, negation, and battle-tested patterns for every major technology stack.
When to Use / When Not to Use
When to use .gitignore:
- Excluding build artifacts and generated files
- Preventing secrets and environment files from being committed
- Ignoring OS-specific files (
.DS_Store,Thumbs.db) - Excluding IDE/editor configuration that’s user-specific
- Keeping repository size manageable
When not to use .gitignore:
- For files that should be tracked but are large (use Git LFS instead)
- To hide mistakes (committing then ignoring doesn’t remove from history)
- For files that every developer needs (those should be committed)
Core Concepts
Git checks ignore patterns in a specific order. The last matching pattern wins:
graph TD
FILE["File Path"] --> P1["1. Command-line flags\n(git add -f overrides all)"]
P1 --> P2["2. .git/info/exclude\n(local, not versioned)"]
P2 --> P3["3. core.excludesFile\n(global ~/.gitignore_global)"]
P3 --> P4["4. .gitignore in same dir"]
P4 --> P5["5. .gitignore in parent dirs\n(up to repo root)"]
P5 --> MATCH{"Pattern matches?"}
MATCH -->|yes, negated| TRACK["File is TRACKED"]
MATCH -->|yes, not negated| IGNORE["File is IGNORED"]
MATCH -->|no| TRACK
Architecture or Flow Diagram
flowchart TD
START["git add file.py"] --> TRACKED{"Already tracked?"}
TRACKED -->|yes| ADD["Added to staging\n(ignores .gitignore)"]
TRACKED -->|no| CHECK["Check ignore patterns"]
CHECK -->|pattern matches| IGNORED{"Negated pattern?"}
IGNORED -->|yes| ADD
IGNORED -->|no| SKIP["Skipped (ignored)"]
CHECK -->|no match| ADD
FORCE["git add -f file.py"] --> ADD
Key insight: once a file is tracked, .gitignore no longer affects it. You must git rm --cached to untrack it first.
Step-by-Step Guide / Deep Dive
Pattern Syntax
| Pattern | Matches | Example |
|---|---|---|
*.log | Any file ending in .log | debug.log, app/error.log |
build/ | Directory named build | build/, src/build/ |
/dist | dist only in repo root | /dist but not src/dist |
**/logs | logs in any directory | logs, a/logs, a/b/logs |
doc/*.txt | .txt files directly in doc/ | doc/readme.txt but not doc/api/v1.txt |
doc/**/*.txt | .txt files in doc/ or subdirs | doc/readme.txt, doc/api/v1.txt |
!important.log | Negation — un-ignore | Overrides a previous *.log |
temp/ | Directory and all contents | temp/, temp/file.txt, temp/sub/ |
*.class | All .class files anywhere | Anywhere in the repository |
The Trailing Slash Rule
A trailing slash means “directory only”:
# Matches directories named "build"
build/
# Does NOT match a file named "build"
# Use "build" (no slash) to match both
Negation Patterns
Negation (!) overrides previous patterns but ONLY if a broader pattern matched first:
# Ignore all log files
*.log
# But keep this one
!important.log
# This won't work — order matters!
!important.log
*.log
Global .gitignore
For patterns that apply to ALL repositories:
# Create global gitignore
cat > ~/.gitignore_global << 'EOF'
# OS files
.DS_Store
Thumbs.db
Desktop.ini
# Editor files
*.swp
*~
.project
.idea/
.vscode/
# Build artifacts
node_modules/
__pycache__/
EOF
# Configure Git to use it
git config --global core.excludesFile ~/.gitignore_global
Debugging .gitignore
# Check why a file is ignored
git check-ignore -v path/to/file
# Check multiple files
git check-ignore -v file1 file2 file3
# Test a pattern without creating files
git check-ignore -v --stdin << EOF
build/output.js
src/build/output.js
EOF
Production Failure Scenarios + Mitigations
| Scenario | Symptoms | Mitigation |
|---|---|---|
| Tracked file won’t ignore | File still appears in git status | git rm --cached <file> then commit |
| Negation doesn’t work | !pattern has no effect | Ensure broader pattern comes BEFORE negation |
| Global ignore not working | OS files still show up | Verify core.excludesFile path: git config core.excludesFile |
| Pattern too broad | *.log ignores wanted files | Use more specific paths: /logs/*.log |
| Case sensitivity issues | .DS_Store vs .ds_store | Use case-insensitive patterns or multiple entries |
Trade-offs
| Aspect | Advantage | Disadvantage |
|---|---|---|
| Per-directory .gitignore | Patterns close to what they ignore | Multiple files to maintain |
| Global .gitignore | One-time setup for all repos | May conflict with project needs |
| Negation patterns | Fine-grained control | Order-dependent, confusing |
| Tracked file exemption | Don’t accidentally ignore committed files | Requires git rm --cached to fix |
Implementation Snippets
# === Node.js / JavaScript ===
node_modules/
dist/
build/
.env
.env.local
.env.*.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm
.eslintcache
coverage/
.nyc_output/
# === Python ===
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
.venv/
env/
env.bak/
pip-log.txt
pip-delete-this-directory.txt
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.mypy_cache/
.pytest_cache/
# === Java / Kotlin ===
*.class
*.jar
*.war
*.ear
*.nar
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
.out/
.classpath
.project
.settings/
# === Go ===
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
vendor/
go.sum
# === Rust ===
/target/
**/*.rs.bk
Cargo.lock
# === Universal ===
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
*.log
*.tmp
*.swp
*~
Observability Checklist
- Monitor: Repository size for accidentally committed large files
- Verify: Run
git check-ignore -von new file types - Audit: Periodically review
.gitignorefor outdated patterns - Track: CI/CD failures caused by missing ignored files
Security/Compliance Notes
.gitignoredoes NOT prevent secrets from being committed — it’s a convenience, not a security control- Use pre-commit hooks (like
detect-secrets) for actual secret prevention - Environment files (
.env) should always be ignored - See Git Secrets Management for comprehensive secret prevention
Common Pitfalls / Anti-Patterns
- Ignoring already-tracked files —
.gitignoreonly affects untracked files - Putting negation before the pattern it negates — order matters
- Using
*instead of**—\*doesn’t match directory separators - Not ignoring lock files consistently — some teams commit
package-lock.json, others don’t; pick one - Over-ignoring — ignoring files that teammates need to reproduce builds
Quick Recap Checklist
-
.gitignoreonly affects untracked files - Last matching pattern wins (negation must come after)
- Trailing slash means directory only
-
/at start anchors to the directory containing the.gitignore -
**matches any number of directories - Use
git check-ignore -vto debug - Global
.gitignorefor OS/editor files - Already-tracked files need
git rm --cachedto stop tracking
Interview Q&A
The file is already tracked by Git. .gitignore only affects untracked files. To stop tracking it, run git rm --cached <file> and commit. The file will be removed from the repository but remain in your working directory, and .gitignore will then prevent it from being re-added.
*.log matches any file ending in .log anywhere in the repository (Git implicitly searches all directories). /**/*.log explicitly matches .log files in any subdirectory from the location of the .gitignore file. In practice, they behave identically, but ** is useful when combined with other path components like logs/**/*.log.
Use a broad ignore followed by negation: *\n!*.js\n!*.ts\n. This ignores all files except .js and .ts files. For directories, you need to negate the directory too: *\n!src/\nsrc/*\n!src/**/*.ts\n. The key insight is that you must negate each directory level to reach the files inside.
Yes, patterns without a leading / apply recursively. *.log in the root .gitignore ignores .log files in all subdirectories. Patterns with a leading / are anchored to the directory containing the .gitignore file. You can also place .gitignore files in subdirectories for localized rules.
.gitignore Precedence Rules (Clean)
graph TD
FILE["File Path"] --> CLI["Command-line flags\ngit add -f overrides all"]
CLI --> EXCLUDE[".git/info/exclude\nlocal, not versioned"]
EXCLUDE --> GLOBAL["core.excludesFile\n~/.gitignore_global"]
GLOBAL --> LOCAL[".gitignore in same dir"]
LOCAL --> PARENT[".gitignore in parent dirs\nup to repo root"]
PARENT --> MATCH{"Pattern matches?"}
MATCH -->|negated !| TRACKED["File TRACKED"]
MATCH -->|not negated| IGNORED["File IGNORED"]
MATCH -->|no match| TRACKED
Production Failure: Ignoring Critical Files
Scenario: Committing secrets and ignoring migrations
# === Problem 1: Accidentally committing secrets ===
$ git add .
$ git commit -m "Add config"
# Oops — .env with API keys was NOT in .gitignore!
# Prevention:
# Add to .gitignore BEFORE first commit
echo ".env" >> .gitignore
echo ".env.*" >> .gitignore
# If already committed, see "Removing Sensitive Data from History"
# === Problem 2: Ignoring migration files ===
$ cat .gitignore
*.sql # Too broad! Ignores database migrations
# Fix: Be specific
migrations/*.sql # Only ignore generated migrations
!migrations/001_initial.sql # Keep hand-written ones
# === Problem 3: Ignoring build output that should be committed ===
$ cat .gitignore
dist/ # But this is a library that ships compiled output!
# Fix: Use .gitattributes or a more targeted pattern
dist/*.map # Ignore source maps only
dist/*.min.js # Keep minified files
# === Debugging what's ignored ===
# Check which rule is ignoring a file
git check-ignore -v path/to/file
# Output: .gitignore:5:*.log path/to/file.log
# Check if a tracked file is being ignored (it won't be!)
git ls-files --cached | grep -f <(git check-ignore -v *)
# Tracked files are NEVER affected by .gitignore
Trade-offs: .gitignore vs .gitattributes vs Sparse Checkout
| Aspect | .gitignore | .gitattributes | Sparse Checkout |
|---|---|---|---|
| Purpose | Exclude files from tracking | Define file handling rules | Partial working tree |
| Scope | Untracked files only | All files (tracked + untracked) | Working directory only |
| Effect | Files not staged | Line endings, merge strategy, LFS | Files not checked out |
| Versioned | Yes (committed to repo) | Yes (committed to repo) | Local config only |
| Negation | Supported (!pattern) | Not applicable | Supported |
| Common use | node_modules/, .env, build/ | eol=lf, *.png binary | Monorepo subdirectories |
| Team impact | Shared across team | Shared across team | Per-developer |
| Security | NOT a security control | NOT a security control | NOT a security control |
Key insight: .gitignore prevents files from being tracked. .gitattributes controls how tracked files are handled. Sparse checkout controls which tracked files appear in your working directory.
Quick Recap: .gitignore Audit by Stack
# === Node.js / JavaScript ===
# Must ignore:
node_modules/
dist/
.env
.env.local
.env.*.local
.npm
.eslintcache
coverage/
# === Python ===
# Must ignore:
__pycache__/
*.pyc
*.pyo
.venv/
venv/
.eggs/
*.egg-info/
.pytest_cache/
.mypy_cache/
# === Java / Kotlin ===
# Must ignore:
target/
build/
*.class
*.jar
*.war
.gradle/
.idea/
*.iml
# === Go ===
# Must ignore:
vendor/ # (if not using Go modules)
*.exe
*.test
coverage.out
# === Rust ===
# Must ignore:
target/
**/*.rs.bk
# Note: Cargo.lock is typically committed for binaries, ignored for libraries
# === Universal (all projects) ===
# Must ignore:
.DS_Store
Thumbs.db
*.log
*.tmp
*.swp
*~
.vscode/settings.json # Personal settings
.idea/workspace.xml # Personal workspace
Resources
Category
Related Posts
Centralized vs Distributed VCS: Architecture, Trade-offs, and When to Use Each
Compare centralized (SVN, CVS) vs distributed (Git, Mercurial) version control systems — their architectures, trade-offs, and when to use each approach.
Automated Changelog Generation: From Commit History to Release Notes
Build automated changelog pipelines from git commit history using conventional commits, conventional-changelog, and semantic-release. Learn parsing, templating, and production patterns.
Choosing a Git Team Workflow: Decision Framework for Branching Strategies
Decision framework for selecting the right Git branching strategy based on team size, release cadence, project type, and organizational maturity. Compare Git Flow, GitHub Flow, and more.