Tutorial7 min read

How to Write a .gitignore File (and Avoid the Common Traps)

A good .gitignore protects your repo from bloat, secrets, and OS noise — but the matching rules have subtleties that surprise even experienced developers. Learn the syntax, common patterns, and the 'already tracked' problem.

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:

  1. Pick your tech stack (Node, Python, Go, Rust, Java, etc.).
  2. Optionally add OS and editor templates (macOS, Windows, VS Code, JetBrains).
  3. Copy the merged .gitignore into 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.

Try the tools