.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
Use .gitignore for:
- 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
Do not use it for:
- Files that should be tracked but are large (use Git LFS instead)
- Hiding mistakes (committing then ignoring doesn’t remove from history)
- 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
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
| 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-off Analysis
| 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 Considerations
.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
.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
Interview Questions
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.
git check-ignore -v <path> shows which pattern is ignoring a file and from which line. The verbose output shows the .gitignore file path, line number, and the pattern that matched. This is the primary tool for troubleshooting why a file is or isn't being ignored.
.gitignore is committed to the repository and shared with the team. .git/info/exclude is local-only (not versioned) and applies only to your working copy. Use .gitignore for project-wide rules; use exclude for personal files you never want tracked.
No — once a file is tracked, .gitignore has no effect on it. You must explicitly untrack it with git rm --cached <file>, then commit the removal. The file stays in your working directory but Git stops monitoring it. Only then will .gitignore prevent re-addition.
A leading slash / anchors the pattern to the directory containing the .gitignore file. For example, /dist matches dist/ only in the repository root, not src/dist. Without the slash, dist matches dist in any directory.
The negation operator ! re-includes a file that was previously ignored. However, it only works if a broader pattern has already matched the file. Negation patterns must appear after the pattern they override. If a negation pattern matches a file that was never ignored by a preceding pattern, it has no effect.
A trailing slash / means the pattern matches only directories, not files. For example, build/ ignores the build directory and all its contents, but does not ignore a file named build. Without the trailing slash, build matches both files and directories named build.
** matches any number of directories, including zero. *.log matches files in any directory (implicit recursive search). logs/**/*.log matches .log files inside logs/ and any subdirectory of logs/. The pattern **/ at the start matches from the repository root; /** at the end matches everything after the anchored path.
Use a broad ignore followed by a negation: *.log then !important.log. The broader pattern ignores all .log files, and the negation re-includes important.log. For directories, you must negate each level: *\n!logs/\nlogs/*\n!logs/important.log.
core.excludesFile points to a user-wide Git ignore file (typically ~/.gitignore_global) that applies to all repositories on your machine. Configure it with: git config --global core.excludesFile ~/.gitignore_global. This is where you put OS-specific files (e.g., .DS_Store) and editor artifacts that should be ignored everywhere.
They serve different purposes: .gitignore prevents files from being tracked; .gitattributes controls how tracked files are handled (line endings, merge strategy, binary detection). A file ignored by .gitignore is never staged. A file managed by .gitattributes can still be ignored if it is also listed in .gitignore, but the attributes apply once the file is tracked by other means.
No — .gitignore uses glob patterns, not regex. The special characters are: * matches any sequence of non-slash characters; ** matches any sequence including slashes; ? matches any single non-slash character; [abc] matches any single character in the set. For advanced filtering, use pre-commit hooks or git check-ignore --stdin with external tools.
.gitignore is a convenience feature, not a security control. It only prevents untracked files from being staged. If a secret file is accidentally committed before being ignored, it remains in Git history forever. Use pre-commit hooks (e.g., detect-secrets, git-secret) or secret scanning tools to actively prevent secrets from entering the repository.
Git reads all applicable .gitignore files from the repository root down to the file's directory, and each one takes precedence over parent directories. Patterns in a subdirectory's .gitignore override conflicting patterns in parent .gitignore files. This allows localized ignore rules that don't affect the rest of the project.
.gitignore prevents files from being staged; sparse checkout controls which parts of the working tree are checked out at all. With sparse checkout, files exist in the repository but are not present in your local working directory. This is useful in monorepos where you only need a subset of packages.
Git checks patterns in this order: (1) Command-line flags like git add -f override everything; (2) .git/info/exclude for local personal ignores; (3) core.excludesFile for global user ignores; (4) .gitignore in the same directory as the file; (5) .gitignore in parent directories up to the repo root. Within each file, the last matching pattern wins.
It depends on the ecosystem. For npm/yarn, package-lock.json is typically committed because it ensures deterministic installs across machines. For Cargo (Rust), Cargo.lock should be committed for binaries but ignored for libraries. The key is establishing a team convention and documenting it. Inconsistent lock file handling causes merge conflicts and unpredictable builds.
Further Reading
Additional Resources
- Gitignore Documentation — Official Git documentation on pattern format and behavior
- Gitignore Template Collection — Community-maintained templates for 500+ languages and frameworks
- Git Book: Ignoring Files — Official Pro Git book section on ignores
- Atlassian: .gitignore Tutorial — Visual walkthrough with examples
- GitHub Help: Ignoring Files — GitHub-specific patterns and repository setup
Related Posts
- Git Objects: Blobs, Trees, Commits, Tags — Understanding how Git stores ignored files internally
- Git Reflog: Recovery Guide — Recovering from mistakes when ignoring goes wrong
- Git Secrets Management — Comprehensive secret prevention beyond .gitignore
- GitHub Flow Branching Strategy — Clean repository hygiene with proper ignores
Conclusion
A well-crafted .gitignore is the first line of defense against repository clutter — it keeps build artifacts, dependencies, and secrets out of version control. The key is understanding the pattern syntax deeply enough to write precise rules that never accidentally exclude what you need.
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
Decision framework for selecting the right Git branching strategy based on team size, release cadence, and project type.