.gitignore Patterns

Comprehensive guide to .gitignore syntax, pattern matching rules, global ignores, negation, and curated patterns for every major tech stack and framework.

published: reading time: 10 min read updated: March 31, 2026

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

PatternMatchesExample
*.logAny file ending in .logdebug.log, app/error.log
build/Directory named buildbuild/, src/build/
/distdist only in repo root/dist but not src/dist
**/logslogs in any directorylogs, 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 subdirsdoc/readme.txt, doc/api/v1.txt
!important.logNegation — un-ignoreOverrides a previous *.log
temp/Directory and all contentstemp/, temp/file.txt, temp/sub/
*.classAll .class files anywhereAnywhere 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

ScenarioSymptomsMitigation
Tracked file won’t ignoreFile still appears in git statusgit rm --cached <file> then commit
Negation doesn’t work!pattern has no effectEnsure broader pattern comes BEFORE negation
Global ignore not workingOS files still show upVerify core.excludesFile path: git config core.excludesFile
Pattern too broad*.log ignores wanted filesUse more specific paths: /logs/*.log
Case sensitivity issues.DS_Store vs .ds_storeUse case-insensitive patterns or multiple entries

Trade-offs

AspectAdvantageDisadvantage
Per-directory .gitignorePatterns close to what they ignoreMultiple files to maintain
Global .gitignoreOne-time setup for all reposMay conflict with project needs
Negation patternsFine-grained controlOrder-dependent, confusing
Tracked file exemptionDon’t accidentally ignore committed filesRequires 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 -v on new file types
  • Audit: Periodically review .gitignore for outdated patterns
  • Track: CI/CD failures caused by missing ignored files

Security/Compliance Notes

  • .gitignore does 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.gitignore only 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

  • .gitignore only 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 -v to debug
  • Global .gitignore for OS/editor files
  • Already-tracked files need git rm --cached to stop tracking

Interview Q&A

Why is a file still showing in `git status` even though it's in .gitignore?

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.

What's the difference between `*.log` and `/**/*.log` in .gitignore?

*.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.

How do you ignore everything except a specific file type?

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.

Does .gitignore apply to subdirectories automatically?

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.gitattributesSparse Checkout
PurposeExclude files from trackingDefine file handling rulesPartial working tree
ScopeUntracked files onlyAll files (tracked + untracked)Working directory only
EffectFiles not stagedLine endings, merge strategy, LFSFiles not checked out
VersionedYes (committed to repo)Yes (committed to repo)Local config only
NegationSupported (!pattern)Not applicableSupported
Common usenode_modules/, .env, build/eol=lf, *.png binaryMonorepo subdirectories
Team impactShared across teamShared across teamPer-developer
SecurityNOT a security controlNOT a security controlNOT 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.

#git #version-control #svn

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.

#git #version-control #changelog

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.

#git #version-control #branching-strategy