Quick Answer

Ignore dependencies, build output, environment files, editor settings and operating system junk. Add .gitignore before your first commit, because it has no effect on files git is already tracking.

What to exclude, and why

Four categories, each for a different reason.

Dependenciesnode_modules/, venv/, vendor/. Thousands of files, platform-specific, and fully regenerable from your lockfile. A committed node_modules makes every clone enormous and every diff unreadable, and it breaks on a different operating system anyway.

Build outputdist/, build/, __pycache__/, *.class. Derived from source, so committing it means every build produces spurious changes and merge conflicts in files nobody edited.

Secrets.env, key files, service account JSON. The one category where the consequence is a security incident rather than untidiness. See secrets management.

Local and machine-specific files.vscode/, .idea/, .DS_Store, Thumbs.db, log files, local database files. Your editor preferences are not part of the project, and they create noise for everyone else.

The pattern syntax

# comment
node_modules/          # directory, anywhere in the tree
*.log                  # any file with this extension
/config.local.js       # only at repository root
build/                 # directory
!important.log         # negation: keep this one
docs/**/*.pdf          # any depth under docs

Details that matter:

  • A trailing slash means directory only. Without it, a file of the same name is also ignored.
  • A leading slash anchors to the repository root. /build ignores only the top-level one; build/ ignores every build directory anywhere.
  • ! negates, but it cannot re-include a file if its parent directory is excluded. Git never descends into an ignored directory, so the negation is never evaluated. This surprises people regularly.

To check why something is ignored:

git check-ignore -v path/to/file
# .gitignore:2:.env    .env

That names the file and line responsible, which beats guessing.

The rule that catches everyone

.gitignore only affects untracked files. Adding a pattern for something git is already tracking does nothing at all — it continues to be tracked and every change continues to appear.

To stop tracking it while keeping your local copy:

git rm --cached .env
git rm -r --cached node_modules
git commit -m "Stop tracking files that should be ignored"

--cached removes it from git's index but leaves it on disk. Without that flag you delete the file itself.

And the essential caveat: this removes it from future commits only. The file remains in history. If it was a secret, it is still readable in earlier commits, so the credential must be rotated regardless of any cleanup.

Starting from a good template

Do not write one from scratch. GitHub maintains templates per language, and most tools generate an appropriate one — npm create vite, django-admin startproject and similar all include a sensible default.

A reasonable starting point for a typical project:

# dependencies
node_modules/
venv/

# build output
dist/
build/
__pycache__/
*.pyc

# secrets
.env
.env.*
!.env.example
*.pem

# editors and OS
.vscode/
.idea/
.DS_Store

# logs and local data
*.log
*.sqlite3

Note !.env.example. The example file should be committed — it documents which variables are required without containing real values — so it is explicitly re-included after excluding the pattern.

Create it before your first commit. That single habit prevents the whole class of problem, including the security one.

Related files worth knowing

  • A global gitignore for machine-specific things. Your editor directory and .DS_Store are personal, not project concerns, so put them in a global file rather than every repository: git config --global core.excludesfile ~/.gitignore_global.
  • .gitattributes controls line endings, which prevents the entire-file-changed diffs that occur when Windows and Unix contributors share a repository.
  • .dockerignore is separate and frequently forgotten. Without it, node_modules and .git are copied into your build context, making images far larger and slower — see Docker for beginners.
  • git status --ignored lists what is being excluded, which is useful when you suspect something is missing from a deployment.

One judgement call: commit your lockfile. package-lock.json and equivalents are not build output — they pin exact versions so builds are reproducible and audits are meaningful.

Frequently Asked Questions

Why is my file still tracked after adding it to .gitignore? Because .gitignore only affects untracked files. Run git rm --cached on it to stop tracking while keeping your local copy, then commit.
Does .gitignore remove a secret from history? No. It stops future commits including it, but earlier commits still contain it. Rotate any exposed credential — that is the only reliable fix.
Should I commit package-lock.json? Yes. It pins exact dependency versions so installs are reproducible and audits meaningful. It is not build output despite being generated.
Why does my negation pattern not work? Git does not descend into ignored directories, so a negation inside one is never evaluated. Un-ignore the parent directory and exclude its contents selectively instead.
Where do editor settings belong? In a global gitignore rather than each project's file, since they are specific to you rather than to the project. Some teams do commit shared editor config deliberately.