.gitignore — what shouldn't be tracked at all

Not everything in a project folder belongs in version control — some files are secrets, some are regenerable, and committing them by accident is a genuinely common, sometimes serious mistake .gitignore exists to prevent.

Beginner

4 min read

The problem: some files genuinely shouldn't be committed

my-project/
  app.py
  .env                    <- contains real API keys and database passwords
  __pycache__/             <- regenerated automatically every time Python runs
  node_modules/             <- hundreds of megabytes, fully reproducible from package.json
  venv/                      <- a full Python virtual environment, machine-specific

Some files are secrets — API keys, database passwords, tokens — that should never end up in Git's history at all, since the earlier lessons in this domain established that history is permanent and shared the moment it's pushed to a remote. Others are regenerable build artifacts — compiled files, dependency folders, cache directories — that add no real value to version control and just bloat the repository with files that get recreated automatically anyway. Both categories are what .gitignore exists to keep out.

.gitignore: a file that tells Git what to ignore

# .gitignore
.env
__pycache__/
node_modules/
venv/
*.pyc
.DS_Store

.gitignore is a plain text file, one pattern per line, placed at the root of the repository — any file or folder matching a pattern in it is excluded from git status, git add ., and everything else that would normally pick up new files. *.pyc uses a wildcard to match any file with that extension, anywhere; a trailing / (node_modules/) specifically matches a directory. Once a pattern is in .gitignore, Git stops surfacing matching files as "untracked" entirely — they become invisible to the normal workflow from the daily-workflow lesson.

The trap: .gitignore doesn't touch files Git is already tracking

$ git add .env          # committed once, before .gitignore existed
$ git commit -m "oops"
$ echo ".env" >> .gitignore   # too late — .env is already tracked
$ git status                    # .env still shows as tracked, .gitignore has no effect on it now

This is a genuinely common, real mistake: adding a pattern to .gitignore only prevents Git from picking up files it doesn't already know about — it has no effect on a file that was already git added and committed before the ignore rule existed. If a secret was accidentally committed once, adding it to .gitignore afterward stops future changes to that file from being tracked, but the secret is still sitting in the commit history from before, fully readable by anyone who can see that history.

The actual fix once something's already tracked: git rm --cached

git rm --cached .env        # stop tracking the file, but keep it on disk locally
git commit -m "Stop tracking .env"

git rm --cached removes a file from Git's tracking without deleting it from the working directory — the file stays on disk, exactly as it was, but a new commit records that it's no longer tracked going forward. This handles "stop tracking this file from now on" — but it does not remove the file from earlier commits already in history; a secret that was committed once is still recoverable from that earlier commit, permanently, unless the history itself is rewritten (a genuinely more advanced, disruptive operation, and the real reason "just add it to .gitignore after the fact" is not a real fix for a leaked secret).

Why .gitignore matters even for files that aren't secrets

# Committing node_modules/ would mean:
# - hundreds of megabytes added to every clone of the repository, forever
# - constant, noisy diffs every time a dependency updates
# - merge conflicts in machine-generated files nobody actually reads

Regenerable build artifacts (compiled output, installed dependencies, cache files) cause real, ongoing problems if committed — they bloat every future clone, generate noisy diffs unrelated to actual code changes, and can even conflict with a teammate's independently-regenerated version of the same files. The rule of thumb: if a file can be regenerated automatically from something else that is tracked (a package.json, a requirements.txt), it usually belongs in .gitignore, not in the repository itself.

Standard .gitignore templates exist, and are usually the right starting point

# github.com/github/gitignore has ready-made files for:
# Python.gitignore, Node.gitignore, Django.gitignore, and hundreds more

Rather than writing a .gitignore from scratch for every new project, GitHub maintains a large collection of standard, language/framework-specific templates (a Python one covers __pycache__/, .pyc files, virtual environments; a Node one covers node_modules/, build output) — starting from one of these and adding project-specific entries on top is the normal, practical way most .gitignore files actually get created.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What two categories of files does .gitignore exist to keep out of version control?

2. If a file is already tracked and committed, does adding it to .gitignore afterward remove it from history?

3. What does git rm --cached actually accomplish, and what does it NOT accomplish?

4. What's the general rule of thumb for whether a file belongs in .gitignore?