Someone commits .env early in a project. Weeks later a teammate notices, adds .env to .gitignore, commits, and assumes the problem is solved. The next time anyone edits .env, git status shows it as modified, and it gets committed again with somebody’s local API key in it.
This confuses people because .gitignore sounds like it means “Git ignores this file.” It actually means something narrower: “don’t suggest this file when it’s untracked.” Interviewers use the scenario to check whether you know the difference between the working tree, the index and what’s tracked, and whether you know what happens to everyone else’s copy when you fix it.
What .gitignore actually controls
Git has two kinds of files in your working tree: tracked files, which are in the index (and so in the next commit), and untracked files, which aren’t. Ignore rules apply only to untracked files. They stop those files appearing as ?? in git status, and stop git add . from picking them up. Once a file is tracked, Git keeps tracking it no matter what the ignore rules say.
Reproduced in a test repository:
$ git commit -m "Initial" # includes .env
$ echo ".env" > .gitignore
$ git add .gitignore && git commit -m "Ignore .env"
$ echo "API_KEY=dev-456" > .env
$ git status --short
M .env.env matches an ignore rule and still shows as modified, because it’s tracked.
Debugging rules with check-ignore
git check-ignore -v <path> tells you which rule, in which file and on which line, matches a path. It has a quirk that fits this topic exactly: by default it reports nothing for tracked files, because ignore rules don’t apply to them.
$ git check-ignore -v .env
$ echo $?
1
$ git check-ignore -v --no-index .env
.gitignore:1:.env .envThe first command finds nothing and exits 1. With --no-index, Git evaluates the rules as if the file weren’t tracked and shows the matching rule. If the rule matches with --no-index but not without it, the file is tracked, and the rule isn’t the problem.
Rules come from several places, checked in order of precedence: patterns on the command line, .gitignore files in the directory and its parents (closer ones win), .git/info/exclude for rules that are personal to one clone, and the file named by core.excludesFile for global rules, such as editor swap files, that belong to you rather than the project.
Untracking a file: git rm --cached
To make Git stop tracking a file while keeping your copy, remove it from the index only:
$ git rm --cached .env
rm '.env'
$ git status --short
D .env
$ git commit -m "Stop tracking .env"
$ ls -a | grep env
.envThe commit records the file as deleted. Your working copy is untouched, and because .env now matches an ignore rule and isn’t tracked, it disappears from git status. For a whole directory that was committed by mistake, use git rm -r --cached build/.
A frequent mistake is running plain git rm .env, which also deletes your local copy. Another is running git rm -r --cached . to “reapply all ignore rules,” which stages a deletion of every file and relies on git add . to restore everything that isn’t ignored. That works, but the diff is enormous and easy to get wrong. Untrack the specific paths.
What happens on everyone else’s machine
This is the part most answers miss. To Git, “stop tracking” is a deletion. When a teammate pulls that commit, Git applies the deletion to their working tree too.
If Alice has no local changes to .env, her pull quietly removes it:
$ cat .env
API_KEY=dev-123
$ git pull
$ ls -a
.
..
.git
.gitignore
app.jsHer local configuration is gone, and the app won’t start. If she does have local edits, Git refuses to overwrite them and the pull stops:
$ git pull
error: Your local changes to the following files would be overwritten by merge:
.env
Please commit your changes or stash them before you merge.
AbortingNeither is a disaster, but both are surprising if nobody warned her. When you untrack a file that people rely on locally, tell the team in the pull request: back up the file before pulling, or run cp .env .env.backup && git pull && mv .env.backup .env. Better still, commit a template such as .env.example with placeholder values in the same change, and have the app’s setup instructions copy it. Then the tracked file documents what’s needed, and the real file is always local.
If the file contained a secret
Untracking stops future commits from including the file. Every past commit still contains it, and so does every clone and fork made before your change. git log --all --oneline -- .env lists the commits that touched it.
So the first step when a credential was committed isn’t Git at all: revoke and rotate the credential. After that, you can rewrite history with git filter-repo --invert-paths --path .env and force-push, but that requires every collaborator to re-clone or carefully rebase, and it doesn’t reach copies you don’t control. Hosting providers like GitHub may keep cached views of old commits until support removes them. Treat the secret as public from the moment it was pushed.
Two flags that look like solutions and aren’t
Search results often suggest git update-index --assume-unchanged .env or --skip-worktree. Both tell your local Git to stop noticing changes to a tracked file. Neither untracks it.
--assume-unchanged is a performance hint: it promises Git that the file won’t change so Git can skip checking it. If the file changes upstream, Git may overwrite your copy or error, and the flag can be dropped silently. --skip-worktree is closer to “I’m deliberately keeping a local version,” but it’s per-clone, invisible to teammates, and causes confusing errors on pulls and branch switches that touch the file. They’re occasionally useful for a config file you must keep tracked, but they’re not a way to make a tracked file ignored for everyone.
Negation patterns and directories
A related puzzle: keeping one file inside an ignored directory. This doesn’t work:
logs/
!logs/.gitkeep$ git status --short --untracked-files=all
?? .gitignorelogs/ excludes the directory itself, and Git doesn’t look inside excluded directories, so the negation is never evaluated. Ignore the directory’s contents instead:
logs/*
!logs/.gitkeep$ git status --short --untracked-files=all
?? .gitignore
?? logs/.gitkeepInterview practice
A repository has node_modules/ committed by accident, about 40,000 files. A junior developer added node_modules/ to .gitignore but git status still shows changes there after every npm install. What do you do, and what should the team expect?
Answer
The ignore rule is correct but has no effect because those files are already tracked. I’d confirm with git check-ignore -v --no-index node_modules/some-package/package.json, which should print the rule, while the same command without --no-index prints nothing.
The fix is git rm -r --cached node_modules, then commit “Stop tracking node_modules” with the .gitignore change if it isn’t committed yet. My local node_modules stays on disk. The commit will be large, since it records 40,000 deletions, but it’s a one-time cost. Before merging, I’d warn the team: when they pull, Git will delete their node_modules directory, and if any tracked file in it was modified locally, the pull will stop with “would be overwritten.” Running npm ci after the pull restores everything from the lockfile, which is the right way to get dependencies anyway. The history still contains the old files, which keeps clones large; rewriting history to remove them is possible with git filter-repo, but it disrupts everyone, so I’d only do it if repository size is a real problem.