.gitignore#

What is .gitignore?#

A .gitignore file is a text file that tells which untracked paths to omit from normal status listings and additions. It helps prevent accidental additions; it does not stop tracking files already in the index or prevent a forced add.

Why is it Useful?#

Without ignore rules, Git normally reports untracked files, and git add . can stage them for tracking. These may include:

Build Artifacts

Compiled binaries, object files, executable outputs

/dist/, *.exe, *.class

Local Configuration

Machine-specific settings, API keys, credentials

.env, config.local.json, *.secret

IDE/Editor Files

Editor-specific metadata and temporary files

.vscode/, .idea/, *.swp

OS-Specific Files

System-generated files that clutter repositories

.DS_Store, Thumbs.db, *.tmp

Logs & Databases

Generated data that shouldn’t be versioned

*.log, *.sqlite, *.db

Without .gitignore, these files would:

  • Bloat your repository with unnecessary data

  • Risk committing sensitive information (credentials, API keys)

  • Cause merge conflicts due to machine-specific differences

  • Slow down operations

  • Confuse collaborators with irrelevant files

Historical Context#

Early version control systems had limited or no ignore capabilities. As repositories grew in complexity, developers needed a way to exclude generated files, local configurations, and temporary data. The .gitignore file emerged as a simple, declarative solution to this problem, becoming a standard across the Git ecosystem.

When to Use .gitignore#

Scenario

Use .gitignore?

Reason

Generated files (build outputs)

✅ Yes

Should be regenerated, not versioned

Local development configs

✅ Yes

Machine-specific, not shared

Credentials and secrets

✅ Absolutely

Security risk if committed

IDE/Editor files

✅ Yes

Specific to your development environment

OS-generated files

✅ Yes

Not portable across systems

Source code

❌ No

This is what you want to version!

Documentation

❌ No

Should be versioned with the code

Test files

⚠️ Maybe

Depends on whether they’re hand-written or generated

Pattern Syntax#

The .gitignore file uses glob patterns to match files and directories. These patterns are similar to, but not exactly the same as, regular expressions.

Pattern scope starts at the directory containing this .gitignore.

Patterns in a repository .gitignore apply to that directory and its descendants. A leading / anchors a match directly there; it does not mean the operating system’s filesystem root. A slash in the middle also anchors the pattern: output/build/ matches that path beside the .gitignore, not nested/output/build/.

Without a slash other than an optional trailing /, a pattern matches names at any depth within this scope. The trailing / restricts matches to directories. Do not prefix patterns with ./: .gitignore patterns are not normalised like shell paths. Paths shown in the examples below are relative to the directory containing the .gitignore.

Basic Pattern Rules#

Matching Files#

*.log

Matches names ending with .log beside this .gitignore or in its descendants.

error.log, debug.log, logs/app.log

temp.txt

Matches the name temp.txt beside this .gitignore or in its descendants.

temp.txt ✅, subdir/temp.txt ✅

Use /temp.txt to match only directly beside the .gitignore.

/build/

Matches a directory named build directly beside this .gitignore, not deeper down.

build/ ✅, src/build/ ❌

build/

Matches directories named build at any depth within this .gitignore’s scope.

build/, src/build/, docs/build/ ✅

Matching Directories#

To match only directories, end your pattern with a forward slash /. Without it, build can match either a file or a directory named build.

# Ignore directories named node_modules beside this .gitignore or below it
node_modules/

# Ignore only node_modules directly beside this .gitignore
/node_modules/

Negation Patterns#

Prefix a pattern with ! to cancel an earlier exclusion for a matching path. This makes an untracked path eligible for adding; it does not track it automatically. An exception cannot reach a file inside an excluded parent directory: Git must be allowed to traverse every parent first.

Negate a Specific File
*.log
!important.log

Ignores .log names within scope, except names exactly important.log, including in descendants.

Negate a Directory
/*
!/.gitignore
!/src/

Ignores other entries directly beside this .gitignore, but keeps the ignore file itself and files under src/ eligible for adding.

Complex Negation
*.js
!*.test.js
!config.js

Ignores all .js files except test files and config.js

Order Matters#

Within one .gitignore, the last matching pattern determines the result, provided an excluded parent directory has not already prevented traversal.

# This works: ignore the contents of docs/ except its README.md
docs/*
!docs/README.md

This does not work, even though the exception comes last:

*
!docs/README.md

Here * excludes the docs directory itself. Allow Git to traverse it before excluding its contents and making the file exception:

*
!docs/
docs/*
!docs/README.md

Similarly, * followed by !src/ allows traversal into src, but * still ignores its files and other child directories. Git does not track empty directories: allowing traversal is different from making files eligible for adding.

Special Characters#

Comments

Lines starting with # are comments and are ignored:

# This is a comment
*.log

Explanations must be on separate comment lines; a trailing # explanation is part of the pattern, not an inline comment.

Escaping Special Characters

To match a file starting with !, escape it with \:

\!important.txt
Character Classes

Use [abc] to match any single character in the brackets:

# Matches file0.txt, file1.txt, ..., file9.txt
file[0-9].txt

Directory-Specific .gitignore Files#

You can place .gitignore files in subdirectories to apply ignore rules locally:

my-project/
├── .gitignore          # Root ignore rules
├── src/
│   ├── .gitignore      # Rules for src/ and its descendants
│   └── temp.txt
└── docs/
    └── .gitignore      # Rules for docs/ and its descendants

Both the repository-root .gitignore and src/.gitignore can supply rules for src/temp.txt. Nested rules take precedence over matching parent rules; whether a file is ignored depends on the actual patterns, not just the presence of these files.

For example, temp.txt in the repository-root .gitignore excludes matching names throughout its scope. An exception !temp.txt in src/.gitignore makes src/temp.txt and src/deep/temp.txt eligible for adding, provided their parent directories are not excluded. Files outside src/ are unaffected by that exception.

Platform Considerations#

Case Sensitivity

Case sensitivity depends on core.ignoreCase and the filesystem, not just the OS. With core.ignoreCase=false, File.txt and file.txt match differently; with it enabled, an ignore pattern can match both spellings.

For cross-platform compatibility, list both:

Thumbs.db
thumbs.db
Line Endings

Patterns match candidate paths, not the contents of candidate files. Git accepts ordinary LF and CRLF line endings in .gitignore.

Practical Examples#

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

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

Pattern Testing#

You can test your patterns before committing using:

git check-ignore -v path/to/file

This shows which pattern (if any) is causing a file to be ignored.

Best Practices#

Security Best Practices#

NEVER commit API keys, passwords, private keys, or other credentials.

Recommendation: maintain a global ignore file as a personal safety net. Use it for recurring sensitive or local file patterns across your repositories.

Create the file, register it with Git, and then edit it to add your patterns:

mkdir -p ~/.config/git
touch ~/.config/git/ignore
git config --global core.excludesFile ~/.config/git/ignore

The global ignore file is a personal security safety net to help avoid accidentally adding credential files across your projects. It remains local to your machine: collaborators do not receive it when they clone a repository.

Although the global ignore file is stored outside your repositories, its patterns are interpreted relative to each repository’s working-tree root, not relative to the directory containing the global ignore file.

For example, in ~/.config/git/ignore:

  • /.secrets/ matches a .secrets directory directly at each repository’s working-tree root.

  • .secrets/ matches directories named .secrets at any depth within each repository.

The leading / anchors the pattern to the repository’s working-tree root; it does not refer to the operating system’s filesystem root.

A global ignore file does not replace a repository-specific .gitignore. Commit and track the project’s .gitignore so every collaborator receives its shared exclusions when cloning. Project-specific build outputs, dependencies, and local configuration paths belong in that tracked file.

Example contents for ~/.config/git/ignore:

# Local environment files, but keep safe templates available for versioning
.env
.env.*
!.env.example
!.env.template

# Personal credential files and a dedicated secrets directory
credentials.local.*
.secrets/

# Common private-key filenames, not their public-key counterparts
id_rsa
id_ed25519

Curate these patterns for your own workflow, and keep templates free of real secrets. Personal editor and OS temporary-file patterns can also go in this global file; project-specific rules still belong in the repository.

Ignore rules match file and directory paths, not secret contents. They do not remove already-tracked files, and repository rules or git add -f can override this safety net. Check staged changes before committing; once exposed, credentials must be revoked or rotated, not merely added to an ignore file.

✅ DO
  • Maintain a curated global ignore file for recurring personal exclusions.

  • Commit project-specific .gitignore files and review their changes in PRs.

  • Keep patterns organised and commented; test them with git check-ignore -v.

  • Commit safe configuration templates such as .env.example.

  • Inspect staged changes with git diff --cached before committing.

  • Use environment variables or secret management services instead of hardcoding credentials.

❌ DON’T
  • Treat your global ignore file as a substitute for a tracked project .gitignore.

  • Expect ignore rules to hide or remove secrets that are already tracked.

  • Blindly bypass ignore rules with git add -f.

  • Ignore source code or safe configuration templates.

  • Use broad patterns that hide important files; globally ignoring formats such as .csv or .json may hide legitimate inputs or test fixtures.

  • Forget to update project rules when adding new file types.

Project-Specific Patterns#

IDE and Editor Files#

Different development environments generate different files. It’s generally safe to ignore these, but coordinate with your team.

VS Code
.vscode/
*.code-workspace
JetBrains (IntelliJ, PyCharm, etc.)
.idea/
*.iml
*.ipr
*.iws
Vim/Emacs
*.swp
*.swo
*~
.core

Common .gitignore Templates#

Instead of writing from scratch, you can use community templates:

How to use gitignore.io:

# Install the gitignore CLI
npx gitignore

# Or generate online at https://www.toptal.com/developers/gitignore
# Select: Python, Node, macOS, Windows, etc.

Example combined template:

# From gitignore.io for Python+Windows+macOS
> python
> windows
> macos
> visualstudiocode

Handling Different Environments#

Development vs. Production#

Some files should be ignored in development but are needed in production:

# Development only - ignore
.env
dev-config.json

Keep non-sensitive shared configuration such as config/production.json versioned rather than listing it as an ignore pattern. A comment saying “DON’T ignore” does not change the meaning of the pattern on the following line.

Personal vs. Shared Configuration#

Use a template pattern:

# Template file (committed)
config_template.json

# Personal copy (ignored)
config.json

# .gitignore
config.json

Then each developer copies the template:

cp config_template.json config.json
# Edit config.json with personal settings

Performance Considerations#

  • Pattern complexity: Simple patterns are faster to evaluate

  • Directory depth: Deep .gitignore files in many directories can slow down git status

  • Number of patterns: Hundreds of patterns may impact performance

  • Solution: Use broad patterns at the root when possible, rather than many specific patterns in subdirectories

Debugging & Common Issues#

The Tracked Files Problem#

⚠️ IMPORTANT: This is the most common .gitignore misunderstanding

Adding a file to .gitignore does NOT remove it from the repository if it’s already being tracked by Git.

Why This Happens#

When you run git add file.txt, Git starts tracking file.txt. If you later add file.txt to .gitignore, Git continues to track it because:

  1. Git already knows about the file

  2. .gitignore only prevents untracked files from being added

  3. Already-tracked files remain in the index

The Solution#

To stop tracking a file that’s already in the repository:

# Remove from index (staging area) but keep locally
git rm --cached file.txt

# Or remove completely (from both index and working directory)
git rm file.txt

⚠️ CAUTION: This doesn’t remove the file from Git history. To completely purge a file from history, you need git filter-repo or similar tools.

Example Workflow#

# Oops! I committed my API key
$ echo "API_KEY=secret123" > .env
$ git add .env
$ git commit -m "Add config"

# Now I add .env to .gitignore
echo ".env" >> .gitignore
git add .gitignore
git commit -m "Add .gitignore"

# But .env is STILL tracked!
$ git status
# On branch main
# nothing to commit, working tree clean

# Fix: Remove .env from tracking
$ git rm --cached .env
$ git commit -m "Remove .env from tracking"

# Now .env is ignored and untracked
$ git status
# On branch main
# nothing to commit, working tree clean

# Explicitly show ignored files
$ git status --porcelain --ignored
# !! .env

Common Mistakes & Gotchas#

Wrong Directory

Problem: Putting .gitignore in the wrong directory

Fix: Put repository-wide rules in a repository-root .gitignore, or scope them to a subdirectory using a .gitignore there. Rules apply beneath the directory containing the ignore file; a leading / anchors a match there. Repository-wide is not the same as a user-global ignore file.

Missing Slash

Problem: Forgetting the trailing / for directories

build matches files and directories named build within scope. build/ restricts the match to directories; /build/ restricts it further to a directory directly beside this .gitignore.

Overly Broad Patterns

Problem: * matches untracked names throughout this .gitignore’s scope, including directories that Git would otherwise traverse.

Fix: Be specific. Use *.log not *

Incorrect Negation Order

Problem: Negation pattern doesn’t work because of order

Fix: Put exceptions after the patterns they negate, and allow traversal into any excluded parent directories before making exceptions for their files.

Case Sensitivity

Problem: Matching differs with filesystem case handling and core.ignoreCase.

Fix: Check the repository’s configuration and use explicit spellings where needed.

Not Committing .gitignore

Uncommitted .gitignore files work locally, but collaborators do not receive those rules when cloning.

Fix: Commit the repository’s .gitignore so its shared rules are distributed.

Debugging Tools#

git check-ignore#

The most important debugging tool for .gitignore:

# Check why a specific file is ignored
git check-ignore -v path/to/file

# Example output:
# .gitignore:1:*.log    path/to/debug.log
# The file is ignored by line 1 of .gitignore with pattern *.log

# Check multiple files
git check-ignore -v file1.txt file2.log dir/file3

# Inspect ignore rules even when the file is already tracked
git check-ignore -v --no-index path/to/file

Flags:

  • -v / --verbose: Show the deciding pattern and its source file/line.

  • --no-index: Check patterns without skipping already-tracked files; this does not untrack them.

If the reported pattern starts with !, it is an exception: the path is not ignored. Verbose output can report such exceptions, so read the pattern rather than assuming that any output means the path is ignored.

git status#

Use git status to see which files are tracked vs. ignored:

# Changes and untracked files; ignored paths are omitted by default
git status --porcelain

# Also include ignored files, marked with !!
git status --porcelain --ignored

# To see ONLY ignored files:
git status --porcelain --ignored | grep '^!!'

Output codes:

  • !! = Ignored

  • ?? = Untracked (not ignored)

  • M = Modified

  • A = Added to index

git ls-files#

List all files tracked by Git (including those in index):

# List all tracked files
git ls-files

# List untracked, non-ignored files
git ls-files --others --exclude-standard

# List only ignored files
git ls-files --others --ignored --exclude-standard

Self-Assessment Questions#

Test your understanding with these questions:

Question 1

You add *.log to .gitignore but error.log is still tracked. Why?

A) .gitignore is case-sensitive B) error.log was already tracked before adding it to .gitignore C) The pattern should be *.LOG D) You need to restart Git

Answer: B) error.log was already tracked before adding it to .gitignore. Files already tracked are NOT automatically removed when added to .gitignore.

Question 2

How do you make Git stop tracking config.env but keep it on your local filesystem?

A) Delete the file, then add to .gitignore B) git rm config.env C) git rm --cached config.env D) Add to .gitignore and run git reset

Answer: C) git rm --cached config.env removes it from tracking but keeps the local copy.

Question 3

For a repository-root .gitignore, which pattern set ignores .tmp files at any depth except the single important.tmp directly beside that .gitignore?

A)

*.tmp
!important.tmp

B)

*/*.tmp
!important.tmp

C)

*.tmp
!/important.tmp

D)

**/*.tmp
!important.tmp

Answer: C) *.tmp followed by !/important.tmp. The leading / anchors the exception directly beside this .gitignore. A and D also allow nested/important.tmp; B additionally fails to exclude .tmp files directly beside the ignore file and at deeper levels than its single directory wildcard.

Question 4

True or False: .gitignore can be used to remove sensitive files from Git history.

Answer: FALSE. .gitignore only prevents untracked files from being added. To remove sensitive files from history, you need tools like git filter-repo, BFG Repo-Cleaner, or git filter-branch.

Question 5

With these rules in a repository-root .gitignore, you want to ignore the contents of temp/ except temp/keep.txt. Which works?

A)

temp/
!temp/keep.txt

B)

temp/*
!temp/keep.txt

C) Both A and B D) Neither

Answer: B) Only option B works. temp/ excludes the directory itself, so Git cannot traverse it to apply the file exception. temp/* excludes its children without excluding temp itself, allowing !temp/keep.txt to take effect.

Troubleshooting Guide#

Symptom

Possible Cause

Solution

File still tracked after adding to .gitignore

File was already tracked

git rm --cached <file>

Pattern not matching expected files

Wrong pattern scope or anchoring

Check the containing .gitignore and anchor with / if needed; use git check-ignore -v

Rules work locally but are missing for collaborators

Project .gitignore not committed/shared

Commit the repository’s .gitignore; a commit is not required for local matching

Negation pattern not working

Wrong order or missing parent

Put negation AFTER the pattern, ensure parent isn’t ignored

Different case-matching behaviour

Filesystem/configuration differences

Check core.ignoreCase and test the actual spellings

Slow git status with many patterns

Too many complex patterns

Simplify patterns, use directory-specific files