.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:
Compiled binaries, object files, executable outputs
/dist/, *.exe, *.class
Machine-specific settings, API keys, credentials
.env, config.local.json, *.secret
Editor-specific metadata and temporary files
.vscode/, .idea/, *.swp
System-generated files that clutter repositories
.DS_Store, Thumbs.db, *.tmp
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#
*.logMatches names ending with .log beside this .gitignore or in its descendants.
error.log, debug.log, logs/app.log
temp.txtMatches 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.
*.log
!important.log
Ignores .log names within scope, except names exactly important.log, including in descendants.
/*
!/.gitignore
!/src/
Ignores other entries directly beside this .gitignore, but keeps the ignore
file itself and files under src/ eligible for adding.
*.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 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
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/
# R specific files
.RData
.Rhistory
.Rproj.user/
.install_r
# R Markdown and Shiny
.Rmd
.Rproj
# R Studio
.Rproj.user/
.Rhistory
Patterns such as .Rmd and .Rproj above are literal names, not extension
patterns: they do not match analysis.Rmd or project.Rproj. An extension
pattern would require a wildcard, but source documents and shared project
configuration should normally remain versioned.
# LaTeX auxiliary files
*.aux
*.bbl
*.blg
*.log
*.out
*.toc
*.acn
*.acr
*.alg
*.glg
*.glo
*.gls
*.ist
*.fls
*.idx
*.ilg
*.ind
*.lof
*.lot
*.nav
*.snm
*.vrb
*.thm
# BibTeX files
*.bib
# Synctex files
*.synctex.gz
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.secretsdirectory directly at each repository’s working-tree root..secrets/matches directories named.secretsat 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
.gitignorefiles 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 --cachedbefore 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
.csvor.jsonmay 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.
.vscode/
*.code-workspace
.idea/
*.iml
*.ipr
*.iws
*.swp
*.swo
*~
.core
Common .gitignore Templates#
Instead of writing from scratch, you can use community templates:
gitignore.io: Generate
.gitignorefor your tech stackGitHub’s collection: Official templates for popular languages
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.
Performance Considerations#
Pattern complexity: Simple patterns are faster to evaluate
Directory depth: Deep
.gitignorefiles in many directories can slow downgit statusNumber 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:
Git already knows about the file
.gitignoreonly prevents untracked files from being addedAlready-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#
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.
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.
Problem: * matches untracked names throughout this .gitignore’s scope,
including directories that Git would otherwise traverse.
Fix: Be specific. Use *.log not *
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.
Problem: Matching differs with filesystem case handling and core.ignoreCase.
Fix: Check the repository’s configuration and use explicit spellings where needed.
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= ModifiedA= 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 |
File was already tracked |
|
Pattern not matching expected files |
Wrong pattern scope or anchoring |
Check the containing |
Rules work locally but are missing for collaborators |
Project |
Commit the repository’s |
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 |
Slow |
Too many complex patterns |
Simplify patterns, use directory-specific files |