After 45 days of mastering Git workflows and collaboration patterns, you've probably committed a few things you wish you hadn't. A well-crafted .gitignore file is your safety net—it prevents build artifacts, secrets, and editor cruft from polluting your repository. .gitignore best practices: practical tips and trade-offs boil down to making deliberate choices about what your team considers "source" and what's merely ephemeral noise.
Use templates for your language
The worst way to build a .gitignore is from scratch. Every language ecosystem has well-established patterns for what should never be committed. GitHub maintains templates for virtually every popular language, and using them as your starting point saves you from learning by trial and error.
If you're working with Node.js, the template includes node_modules/, .env.local, and various build outputs. For Python, you'll see __pycache__/, .venv/, and *.pyc. For Java or .NET, it's compiled binaries and intermediate build directories. Rather than memorizing these, fetch the official template and customize it for your project's specific needs.
Start by visiting https://github.com/github/gitignore or use the GitHub CLI:
curl https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore -o .gitignoreThen review it with your team. You might want to add framework-specific ignores (Next.js cache, NestJS build output) or project-specific directories (local config files, generated reports). Keep it readable—add comments explaining non-obvious entries.
# Node.js
node_modules/
dist/
.next/
# Environment & secrets
.env.local
.env.*.local
# IDE (handled by global .gitignore)
# .vscode/
# .idea/
# Logs
*.log
logs/
# Generated documentation
docs/api-reference.mdThe trade-off here is between comprehensiveness and maintenance. A 100-line .gitignore is harder to reason about than a 20-line one. Add only what your team actually generates; don't copy entries you don't need.
Ignore build output and dependencies
Build outputs and downloaded dependencies should never be committed. They're reproducible, they bloat your repository, and they cause merge conflicts across branches. This is where most teams get it right, but understanding the reasoning helps you make good judgment calls on edge cases.
When you run npm install, yarn, pip, or Maven, you're creating a dependency tree that's fully reproducible from a lock file. Committing node_modules/ or .m2/repository/ means every contributor downloads hundreds of megabytes of identical files instead of running a single install command. For a growing repository, this becomes a performance nightmare.
Build outputs are even more critical. If your dist/, build/, or target/ directory ends up in version control, you're storing generated artifacts. This adds noise to diffs, makes history harder to read, and can hide real source-code changes behind rebuild artifacts.
# .gitignore example: ignoring build and dependency directories
node_modules/
dist/
build/
.next/
out/
# Python
venv/
env/
__pycache__/
*.egg-info/
dist/
build/
# Java / .NET
target/
bin/
obj/
*.dll
*.exeA practical scenario: your CI/CD pipeline builds and tests your code, then deploys the artifact to production. Your local dist/ directory shouldn't influence what gets deployed. The build is deterministic and controlled; checking in artifacts defeats that principle.
The trade-off is that junior developers sometimes think "I'll commit the build so deployment is faster." Resist this. Slow deployment from unoptimized builds should be fixed in your pipeline, not by committing artifacts.
Never ignore lock files
This is the single most important rule, and it's non-negotiable: always commit lock files (package-lock.json, yarn.lock, Pipfile.lock, Gemfile.lock, go.sum). If you see a lock file listed in someone's .gitignore, fix it immediately.
A lock file guarantees that every developer and every CI/CD run gets identical dependency versions. Without it, npm install might resolve a package to version 1.2.3 on your machine and 1.3.0 on a colleague's, causing subtle, infuriating bugs that only appear in production.
The confusion arises because lock files are generated automatically—they feel like build artifacts. They're not. They're configuration that must be version-controlled and reviewed like any other code change. When you upgrade a dependency, the lock file changes, and that change is part of your commit.
# Correct: commit lock files
package-lock.json # included in version control
yarn.lock # included in version control
Pipfile.lock # included in version control
go.sum # included in version control
# Incorrect: these should never appear in .gitignore
# package-lock.json
# yarn.lockReal-world consequence: you add a dependency, push a PR, your colleague merges it, and the build passes. Then QA reports a different behavior than your local environment. Without a committed lock file, you can't guarantee they're running the same versions. With it, you know exactly what's deployed.
Global gitignore for editor files
Your project's .gitignore should focus on code, dependencies, and build artifacts. Editor and OS files are personal—they belong in a global .gitignore that applies to all your repositories, not cluttering the project's shared ignore rules.
Every editor creates metadata: VS Code generates .vscode/settings.json, JetBrains IDEs create .idea/, Vim leaves *.swp, macOS adds .DS_Store. If you put all of these in your project .gitignore, it becomes a dumping ground of unrelated concerns. Worse, if your team uses different editors, the file grows constantly as people add their personal tool's output.
Instead, configure a global ignore file that Git checks for all repositories on your machine:
# Step 1: Create a global gitignore file
cat > ~/.gitignore_global << 'EOF'
# Editor files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
Thumbs.db
# OS
.Trash-*
.$RECYCLE.BIN/
# IDE
*.iml
.gradle/
.classpath
EOF
# Step 2: Configure Git to use it
git config --global core.excludesfile ~/.gitignore_global
# Step 3: Verify
git config --global core.excludesfileThis approach keeps your project .gitignore focused on language-specific and project-specific ignores. A new team member clones the repo and immediately has the right dependencies listed in ignore rules, without needing to merge dozens of editor preferences.
The trade-off is that it requires each developer to set up their global file—it's not automatic. Document this in your CONTRIBUTING.md or onboarding guide, and it becomes a non-issue.
Common mistakes
- Ignoring lock files: This is usually accidental. Someone sees the lock file as "generated" and adds it to
.gitignore. Always audit any new ignore rule before committing it. - Overly broad patterns: Using
*.logis fine; using*.tmpmight accidentally ignore a real source file. Be specific about directories. - Committing then ignoring: If a file is already in Git history, adding it to
.gitignoredoesn't remove it. Usegit rm --cached filenameand commit the removal separately. - Ignoring config files universally: Sometimes you do want to commit a default config. Use
config.example.jsoninstead of ignoringconfig.json, so new developers know the structure. - Making .gitignore too clever: Negation patterns (
!) can exclude files from being ignored, but they're confusing. Use them rarely and document them.
Key takeaways
- Start with an official language template from GitHub's gitignore repository, then customize for your project.
- Ignore all dependencies and build outputs without exception—they're reproducible from source.
- Never ignore lock files; they guarantee consistent environments across developers and CI/CD.
- Use a global
.gitignorefor editor and OS files, keeping your project ignore file focused and maintainable. - Review
.gitignorechanges in code review like any other file; mistakes here can silently break reproducibility. - If a file was committed before being ignored, use
git rm --cachedto remove it from history.






Comments
Loading comments…