A .gitignore file is one of the first things you should add to any new repository — and one of the most commonly misunderstood. Forget to ignore a node_modules/ folder and your repo balloons to a gigabyte. Forget to ignore a .env file and you ship credentials to GitHub. This guide walks through how .gitignore actually works and how to write one that protects you.
What Is a .gitignore File?
.gitignore is a plain text file that tells Git which files and folders to exclude from version control. Git reads it from the root of your repository (and from any subdirectory) when deciding which untracked files to surface in git status or include in git add.
The rules are simple but easy to get wrong:
- One pattern per line.
- Lines starting with
#are comments. - Blank lines are ignored.
- Patterns can use wildcards (
*,?,**). - Leading
!re-includes a previously ignored pattern.
Why .gitignore Matters
Repository size. Without node_modules/ ignored, a small Node project's repo can grow to 500 MB+ within months. Cloning becomes painful, CI gets slow, and GitHub starts charging for storage.
Security. Files like .env, credentials.json, and *.pem contain secrets. Once committed and pushed, you can't truly "delete" them — the history still has them, even after a git rm.
Cross-platform sanity. macOS leaves .DS_Store files everywhere, Windows scatters Thumbs.db, IDEs litter .idea/ and .vscode/. Without ignoring these, your diffs are full of OS noise.
Build artifacts. Compiled binaries, dist/, .next/, __pycache__/ should be regenerated, not committed. They cause merge conflicts and bloat the repo with content you can rebuild any time.
How Git Matching Rules Actually Work
This is the part most tutorials skip:
| Pattern | Matches |
|---|---|
foo |
Any file or folder named foo at any depth |
/foo |
Only foo at the repo root |
foo/ |
Only directories named foo, at any depth |
*.log |
All files ending in .log |
**/logs |
A logs folder at any depth (same as logs/ in modern Git) |
logs/** |
Everything inside logs/, but not logs/ itself |
!important.log |
Re-include important.log even if *.log is ignored |
\#literal |
Match a literal # character (escape special chars) |
The trailing slash matters. foo matches both files and folders. foo/ only matches folders. If you want to ignore a directory specifically, always include the trailing slash.
A Solid Starter .gitignore
This is a baseline that works for most projects:
# Dependencies
node_modules/
vendor/
.bundle/
# Build output
dist/
build/
out/
.next/
.nuxt/
.cache/
__pycache__/
*.pyc
# Environment & secrets
.env
.env.local
.env.*.local
*.pem
*.key
# OS files
.DS_Store
Thumbs.db
desktop.ini
# Editors
.vscode/
.idea/
*.swp
*.swo
.vs/
# Logs
*.log
logs/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Test coverage
coverage/
.nyc_output/
For framework-specific projects (Next.js, Django, Rails, etc.), add the framework's standard patterns on top.
The "Already Tracked" Problem
.gitignore only affects untracked files. Once Git is tracking a file, adding it to .gitignore does nothing — Git keeps including it in commits.
To remove an already-tracked file from version control while keeping it on disk:
git rm --cached path/to/file
git commit -m "chore: stop tracking file"
For an entire directory:
git rm -r --cached node_modules/
git commit -m "chore: untrack node_modules"
This removes the file from the index and from future commits, but leaves it in your working directory. After this, the .gitignore rule will work for new clones.
Re-Including Files With !
The ! operator re-adds an ignored file. The order matters — later rules override earlier ones.
# Ignore all logs
*.log
# But keep audit.log
!audit.log
Critical limitation: you cannot re-include a file inside an ignored directory. Once a parent directory is ignored, Git doesn't even look inside it.
# This does NOT work
build/
!build/keep-me.txt # Won't be re-included
# Use this instead
build/*
!build/keep-me.txt
Global gitignore (Per-User)
For files that are personal, not project-specific (editor configs, OS files, your shell history), use a global gitignore instead of polluting every repo:
git config --global core.excludesfile ~/.gitignore_global
Then add patterns like .DS_Store, *.swp, .idea/ to ~/.gitignore_global. Your teammates won't need them, and you won't need to add them to every project.
How to Generate a .gitignore Online
Use DevZone's Gitignore Generator to build a .gitignore for your stack:
- Pick your tech stack (Node, Python, Go, Rust, Java, etc.).
- Optionally add OS and editor templates (macOS, Windows, VS Code, JetBrains).
- Copy the merged
.gitignoreinto your repo root.
The templates are the same ones used by GitHub's official gitignore repository, deduplicated and merged so you don't get duplicate patterns.
Common Mistakes
Committing the secret first. Adding .env to .gitignore after you've already committed it doesn't help — the secret is in history. You must rotate the credential and consider rewriting history with git filter-repo.
Ignoring *.env instead of .env*. *.env matches staging.env but not .env.local. The pattern you usually want is .env* (or list each variant).
Forgetting the trailing slash on directories. node_modules (no slash) matches a file called node_modules too. Use node_modules/ to be specific.
Adding .gitignore to .gitignore. Don't — your team needs the file. It must be tracked.
FAQ
What's the difference between .gitignore and .git/info/exclude?
Both ignore files. .gitignore is committed and shared with the team. .git/info/exclude is local to your clone and not committed — useful for personal scratch files in a project where you can't change .gitignore.
Can I have a .gitignore in a subdirectory?
Yes. Each directory can have its own .gitignore, and rules apply relative to that directory. Useful for keeping per-folder rules (e.g., a dist/.gitignore that re-includes a specific file inside a generated folder).
How do I check whether a file is ignored?
git check-ignore -v path/to/file
This tells you which rule (and which .gitignore file) caused the file to be ignored. Indispensable when you can't figure out why a file isn't showing up in git status.
Should .gitignore be committed?
Yes. It's a project-level configuration shared with everyone working on the repo.
What happens if I have conflicting rules?
The last matching rule wins. So if you ignore *.log then re-include !debug.log, debug.log will be tracked. If the order were reversed, *.log would re-ignore it.