.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: 16 min read author: Geek Workbench 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

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

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

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-off Analysis

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 Considerations

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

.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

Interview Questions

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

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

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

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

5. How does git check-ignore help debug .gitignore issues?

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.

6. What is the difference between .gitignore and .git/info/exclude?

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

7. Can you ignore a file that is already 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.

8. What does a leading slash mean in a .gitignore pattern?

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.

9. How do negation patterns (!) work in .gitignore?

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.

10. What is the purpose of the trailing slash in .gitignore patterns?

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.

11. How does the ** glob pattern differ from * in .gitignore?

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

12. How do you ignore all files of a specific extension except one?

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.

13. What is core.excludesFile and how do you configure it?

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.

14. How does .gitattributes interact with .gitignore?

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.

15. Can you use regex in .gitignore?

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.

16. Why should sensitive data like API keys not rely on .gitignore?

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

17. What happens if you have multiple .gitignore files in subdirectories?

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.

18. How does sparse checkout differ from .gitignore?

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

19. What is the order of precedence when multiple ignore patterns match?

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.

20. How do you handle lock files (package-lock.json, Cargo.lock) in .gitignore?

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

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.

#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

Decision framework for selecting the right Git branching strategy based on team size, release cadence, and project type.

#git #version-control #branching-strategy