Writing a good .gitignore: presets, negation, and what not to ignore

Published: 2026-09-05

How .gitignore patterns work, when to use stack presets, how !negation re-includes files, and what you should still track so clones stay useful.

A .gitignore file tells Git which untracked paths to skip. Good ignore rules keep git status quiet, prevent accidental commits of build junk and secrets, and still leave enough tracked files that a fresh clone builds and documents itself. Bad rules do the opposite: they hide things you meant to share, or they grow into an unread paste of every template on the internet.

This guide covers pattern basics, stack presets, ! negation, and what not to ignore—then points you at a browser-local generator so you can draft a combined file without uploading your repo layout.

What Git actually ignores

Ignore rules apply to untracked paths. If a file is already in the index, adding its name to .gitignore will not untrack it; you still need something like git rm --cached for that path. Patterns are matched relative to the directory that contains the ignore file (repo root for the usual top-level .gitignore).

Common building blocks:

Pattern Typical meaning
node_modules/ Ignore that directory (and its contents)
*.log Ignore matching files in any matched directory
dist / dist/ Directory-style ignores (trailing / is clearer for folders)
**/*.tmp Recursive globs where your Git version supports them
# comment Comments; blank lines are ignored

Order matters when you mix ignores with negation (next section). Later matching rules can override earlier ones for the same path.

Stack presets: start from common junk, then edit

Almost every language and OS produces the same clutter: dependency trees (node_modules/, .venv/), build outputs (dist/, target/), editor metadata (.vscode/ vs personal settings—team norms vary), and OS noise (.DS_Store, Thumbs.db).

Presets (Node, Python, Next.js, macOS, VS Code, Docker, and so on) are condensed lists of those patterns—inspired by community templates such as GitHub’s gitignore collection, not a byte-for-byte mirror. A sensible workflow:

  1. Pick presets that match your stack (language + framework + OS + IDE is usually enough).
  2. Merge them into one file; dedupe identical lines if several presets repeat dist/ or .env.
  3. Skim before committing. Delete patterns you do not use; add team-specific paths.

Presets are a starting point. A monorepo, a Unity project, or a repo that intentionally commits build artifacts will need hand edits.

Negation: ! to re-include exceptions

A leading ! negates a previous ignore so a path can be tracked again. The classic Node example is:

.env
.env.*
!.env.example

That ignores real env files and machine-local variants, but keeps a template (dummy values only) in the repository so new contributors know which keys exist. The same idea applies to !.gitkeep under an otherwise-ignored empty directory, or a checked-in sample config next to ignored local overrides.

Rules of thumb for negation:

  • Negation only works for paths that were ignored by an earlier pattern; parent directories still need to be reachable (you cannot re-include a file inside a fully ignored folder unless you also un-ignore the parents carefully).
  • Prefer narrow ignores plus explicit ! exceptions over ignoring everything and whitelisting the world.
  • Document non-obvious exceptions in a short comment above the ! line so the next reader does not “clean them up.”

What not to ignore (and what to keep out)

Usually keep out of Git (do ignore):

  • Dependencies and virtualenvs (node_modules/, .venv/, vendor/ when regenerated).
  • Build and coverage output (dist/, build/, coverage/, *.tsbuildinfo).
  • Secrets and local env (.env, .env.local, credential dumps)—see .env files: syntax, merging, and secrets hygiene.
  • Personal IDE noise when the team agrees it is personal (some teams do commit shared .vscode/launch.json; decide once and stick to it).

Usually do not ignore (do track):

  • Source, lockfiles your package manager expects (package-lock.json, pnpm-lock.yaml, Cargo.lock for apps—follow ecosystem norms).
  • Templates and examples (.env.example, sample configs) that use fake credentials only.
  • Project docs, CI configs, and the .gitignore itself.
  • Small committed assets the app needs at runtime (unless you generate them in CI).

Ignoring lockfiles “to avoid merge conflicts,” or ignoring dist/ in a package that publishes from git without a build step, are common foot-guns. Match the ignore file to how the project is actually built and installed.

Nested ignores and global excludes

You can add .gitignore files in subdirectories for package-specific rules. Developers can also set a global excludes file (core.excludesFile) for personal OS/editor clutter without polluting every repo. Prefer repo rules for anything every clone needs; keep global rules for your machine only.

A safe local drafting workflow

  1. List the stacks you use (for example Node + Next.js + macOS + VS Code).
  2. Generate a combined draft from those presets; append custom lines for secrets/, *.local, or internal paths.
  3. Confirm .env / secret patterns exist, and that !.env.example (or your template name) is present if you ship an example.
  4. Diff against any existing ignore file so you do not drop intentional exceptions.
  5. Commit only after a quick git status on a dirty tree to verify expected noise is gone and expected files still show up.

Merging presets and custom lines can run entirely in your browser so draft patterns never need to leave your machine. For the broader privacy model, see Why “local only” matters for developer tools.

Try it locally in your browser

Use the Gitignore generator to:

  • Select language, framework, OS, IDE, and tooling presets.
  • Merge them into one .gitignore with optional dedupe and custom lines.
  • Copy or download the result, then review negation rules (such as !.env.example) before committing.

Related reading

All learn articles