<- Back to BlogGit and DevOps

.gitignore Best Practices: Keep Secrets and Build Files Out of Git (2026)

Need a starter .gitignore?

Use the .gitignore Generator to combine language, OS, IDE, and project rules, then review the result before committing it.

A .gitignore file is a version-control rule set, not a security boundary. It tells Git which untracked paths to hide from ordinary status and add operations. A good file keeps generated noise out of reviews while making sure source code, lockfiles, and deployment configuration remain visible to the team.

1. What .gitignore does

Git evaluates ignore patterns when a path is not already tracked. Ignoring a file does not delete it, encrypt it, or stop an application from reading it. If a credential was committed in an earlier revision, adding it to .gitignore does not remove it from history; revoke and rotate the credential, then clean history only with a deliberate migration.

Two consequences follow from that rule. Ignore rules apply only to paths Git does not track yet, so a file that reached a commit earlier keeps appearing in status output and diffs no matter how many patterns match it. And ignore rules are shared but not enforced: every clone receives the same patterns, yet nothing stops a teammate from committing a file that no pattern covers. Pair the file with review habits and automated checks rather than treating it as policy.

2. Pattern rules that matter

PatternMeaning
node_modules/Ignore directories with this name
*.logIgnore matching files at any depth
/dist/Ignore dist at repository root
build/Ignore build directories in matching scopes
!important.logRe-include a path after a broader rule
config/*.localIgnore matching files in a specific directory

A trailing slash communicates that a pattern is intended for directories. A leading slash anchors a pattern relative to the current .gitignore file. Negation is processed in order, so a parent directory ignored earlier may prevent Git from reaching an exception beneath it.

The syntax itself is small enough to memorize in one sitting. A leading ! re-includes a path, a trailing / restricts a pattern to directories, a leading slash anchors the pattern to the directory holding the file, and ** crosses directory boundaries. A line starting with # is a comment unless the hash is escaped, and trailing spaces are stripped unless escaped too, which occasionally surprises anyone who tries to ignore a file whose name ends with a space.

# node_modules at any depth
**/node_modules/

# build/ only when it sits directly under docs
/docs/build/

# re-include one file after a broad rule
!important.log

# a file literally named "#draft.txt"
\#draft.txt

# single-character range
debug[0-9].txt

3. Building a useful file

Start with generated dependencies, build output, local environment files, editor metadata, operating-system files, and test artifacts. Add rules only when the project has a reason for them. Overly broad patterns can hide a legitimate source file and make a new developer wonder why Git does not show it.

# Dependencies and build output
node_modules/
dist/
.astro/

# Local configuration
.env
.env.*
!.env.example

# Logs and editor files
*.log
.vscode/
.idea/
.DS_Store

Keep an example configuration such as .env.example tracked with placeholder values. It documents required settings without distributing real secrets. Language templates are a starting point, not a substitute for reviewing the repository's actual build system.

Match the rules to what your toolchain actually writes to disk. The usual offenders:

StackGenerated paths worth ignoring
Node (Vite, Next, webpack)node_modules/, dist/, build/, .next/, .turbo/
Python__pycache__/, *.pyc, .venv/, *.egg-info/
Rust and Gotarget/, bin/, coverage.out
JVM and .NETbuild/, target/, obj/, .gradle/
Terraform.terraform/, *.tfstate*, *.tfvars with a tracked *.tfvars.example
Test and coverage outputcoverage/, .nyc_output/, *.lcov, junit.xml

GitHub's official github/gitignore repository ships per-language templates plus global fragments for macOS, Windows, Vim, and JetBrains editors, and the template chooser offers them when a repository is created. Treat those files as checklists rather than scripture: delete rules for tools the project never uses, because a pattern that does not match your build is just one more way for a legitimate file to disappear from status output.

4. Secrets and already-tracked files

The most important rule is operational: do not rely on ignore rules to protect a secret that has already been committed. If a key appears in a commit, treat it as exposed. Revoke it with the provider, replace it, and inspect logs for use. Removing the current file does not erase old commits from every clone or fork.

Use the Secret Scanner before creating a commit and add pre-commit or CI scanning for defense in depth. Review generated files too; build output can contain embedded URLs, tokens, source maps, or configuration that was safe only on the server.

The recovery order matters. Add the ignore rule, remove the file from the index with git rm --cached as described in section 7, and rotate the secret with the provider before anything else. Rotate even after the path disappears from the branch tip: old revisions survive in clones, forks, mirrors, CI caches, artifacts, and backup archives, and git filter-repo cannot reach any of those. History rewriting is optional cleanup; rotation is the control. The wider lesson about pasting configuration into online tools is covered in why you should never upload .env files online.

5. Team maintenance

  1. Keep the root file focused on rules shared by every contributor.
  2. Use a global Git excludes file for personal editor or OS files that do not belong in the repository.
  3. Document exceptions in comments with the reason, not just the pattern.
  4. Review ignore changes like code because they change what enters version control.
  5. Test patterns with git check-ignore -v path.
  6. Make sure required fixtures, lockfiles, migrations, and deployment manifests are not accidentally ignored.

For a new project, the .gitignore Generator can combine common templates. After generation, compare the result with the repository's package manager, framework, deployment target, and secret-handling policy.

Two review habits catch most regressions. Ask reviewers to justify every new negation, because exceptions are where a broad rule starts hiding files it was never meant to touch. And run a continuous integration check that executes git check-ignore against a short list of critical paths, failing the build when an environment file stops being ignored or a required fixture stops being tracked. The GitHub Actions CI/CD Guide shows where such a step belongs in the pipeline.

6. Common mistakes

  • Ignoring the entire config directory: useful templates and documentation disappear with local secrets.
  • Adding a rule after the leak: existing tracked files are unaffected.
  • Ignoring lockfiles: reproducible builds become harder.
  • Using a global ignore for project files: teammates cannot see why a required file is missing.
  • Copying a giant template: irrelevant rules hide mistakes.
  • Assuming Git ignores empty directories: Git tracks files, not empty directory entries.
  • Confusing build with build/: without the trailing slash the pattern matches files too, so an unrelated file named build vanishes from status output.
  • Adding the rule too late: a pattern in your local file protects one clone only; the file itself must be committed before teammates inherit the rule.

7. Tracked files are never re-ignored

The number one misunderstanding is the belief that adding an existing file to .gitignore stops Git from tracking it. It does not. Ignore rules are consulted only for untracked paths, so a file that reached a commit earlier keeps being staged, diffed, and pulled no matter how many patterns now match it. The visible symptom is a status list that never goes quiet after the ignore file was "fixed".

The correction removes the path from the index while keeping it on disk:

# stop tracking, keep the local copy
git rm --cached .env.production

# remove a directory from the index only
git rm -r --cached dist/

# unstage a file added by accident
git restore --staged scratch.yaml

git commit -m "Stop tracking local build output"

After that commit the rule finally takes effect, and because the working tree copy never moved, builds, editors, and scripts keep working. Two cautions remain. The removal commit still leaves older revisions in history, so a committed secret needs rotation regardless. And every clone receives the deletion on its next pull, which can surprise teammates whose build scripts still expect the file — announce the change when it affects tooling configuration.

Occasionally a file that matches an ignore rule really does belong in the repository, such as a test certificate fixture or a seed database. Force it past the pattern with git add -f path, then leave a comment beside the rule explaining the exception, or narrow the pattern so the exception is visible to everyone. Force adds are legitimate; silent ones become mysteries during the next cleanup.

8. The three ignore layers: repository, global, and local

Git combines several rule sources, in fixed precedence: patterns passed with --exclude on the command line, then .git/info/exclude, then the file named by core.excludesFile, then the .gitignore files in the working tree and its parent directories. Within one file the last matching pattern wins; across sources the higher-precedence source decides, and a lower layer cannot re-include a path that a higher layer already excluded.

LayerVisible to teammatesRight place for
.gitignore, committedYes, with every cloneBuild output, environment files, agreed fixtures
.git/info/excludeNo, it is never committedOne-off experiments and machine-specific paths
Global core.excludesFileNo, it applies to every repository on this machineEditor and operating-system noise
git config --global core.excludesFile ~/.config/git/ignore
git config --get core.excludesFile
cat .git/info/exclude

IDE and operating-system files belong in the global layer because they describe your machine rather than the project: .DS_Store, Thumbs.db, editor swap files, and workspace folders follow you across every repository you open. Committing .vscode/ or .idea/ is reasonable when a team deliberately shares launch configurations or extension recommendations — document that exception in the repository file instead of inheriting it accidentally from a copied template.

9. Testing patterns with git check-ignore

Debug rules with Git's own diagnostic instead of creating and deleting files until status output looks right:

git check-ignore -v node_modules/react/index.js
# .gitignore:1:node_modules/   node_modules/react/index.js

git check-ignore -v src/app.ts
# no output, exit code 1: this path is not ignored
  • The -v flag prints the source file, line number, and matching pattern, which settles arguments about which rule made the decision.
  • Exit codes are script-friendly: 0 means the path is ignored, 1 means it is not, and 128 signals an error such as running outside a repository.
  • --no-index evaluates patterns even for paths that are already tracked, which is the quickest way to prove a rule matches while section 7 explains why tracking continues anyway.

A small continuous integration job that fails when .env or a private key path stops being ignored catches template edits, botched rebases, and merges that quietly drop rules long before anyone commits a secret.

The command also documents intent: its output names the file and line that matched, which you can paste into a pull request when proposing a rule change. When two rules could both match, the diagnostic shows which one actually wins, so nobody has to reason about pattern ordering by hand.

10. Lockfiles: commit or ignore them

A lockfile pins every resolved dependency, so the real question is whether installs have to be reproducible for other people and machines.

Project typeLockfileCommit it?
Deployed applicationpackage-lock.json, yarn.lock, pnpm-lock.yaml, poetry.lockYes, continuous integration installs from it
Published librarysame filesTeam choice: it never overrides a consumer's resolution, but committing it documents the versions you tested against
Monorepo workspaceone root lockfileYes, per-package lockfiles fight the workspace installer
Go modulego.sumAlways, it is a checksum ledger rather than a resolver snapshot
Rust binary or workspaceCargo.lockYes; libraries may omit it because dependents never read it

Ignoring a lockfile in an application is how environment drift reaches production: two installs on the same day can resolve different transitive versions and pass different tests. Commit it and enforce it with frozen installs such as npm ci or pnpm install --frozen-lockfile, which fail loudly instead of silently updating the lockfile.

11. Monorepos and nested .gitignore files

Git reads the .gitignore in each directory together with every parent file up to the root, and a path is ignored as soon as any applicable file matches it. Nested files let each package carry its own rules without bloating the root:

# repository root
node_modules/
*.local

# services/web/.gitignore
.next/
out/

# services/api/.gitignore
target/

Patterns in a deeper file outrank the root file for paths in that subtree, but exceptions still cannot cross an excluded parent directory: if the root ignores build/, no nested file can re-include build/report.html. Keep shared rules at the root, package-specific rules beside the package, and machine-only rules in .git/info/exclude. Commands run from a subdirectory always load the whole chain, so there is no such thing as a package-local ignore context.

Conclusion

A good .gitignore keeps generated and local-only material out of collaboration without hiding source or pretending to secure secrets. Generate a sensible baseline, review each pattern, test with Git's diagnostic command, and pair ignore rules with rotation and secret scanning.

When a repository starts from a template, review it line by line once, delete what does not apply, and confirm that .env.example, lockfiles, migrations, and deployment manifests stay on the tracked side of the line. Ten minutes of that review is cheaper than a year of wondering why a file will not appear in status.

Frequently Asked Questions

Does .gitignore protect secrets?
No. It prevents ordinary tracking of untracked paths but is not encryption or a history cleanup mechanism.
What if a secret is already committed?
Revoke and rotate it immediately, then remove it from history through a planned repository migration if needed.
Should .env.example be committed?
Yes, when it contains placeholders and documents required variables without real credentials.
How do I test an ignore rule?
Use git check-ignore -v followed by the path to see the matching rule.
Should lockfiles be ignored?
Usually no; lockfiles are commonly required for reproducible dependency installation.
Can I re-include a file?
Yes, with a later negation pattern, provided parent directories are accessible to Git.
Does git check-ignore tell me whether Git ignores a file?
Not on its own. It reports which pattern matches the path; you still have to know that an already-tracked file is never ignored no matter what the pattern says.
Where should personal ignores like .DS_Store go?
In a global excludes file configured with core.excludesFile, or in .git/info/exclude when the rule should stay local to one clone.
Can a subdirectory have its own .gitignore?
Yes. Git reads the .gitignore in the current directory and in every parent, so nested files add rules scoped to their subtree.