TechEarl

How to Remove a File from Git Without Deleting It

Stop Git tracking a file while keeping it on disk with git rm --cached. Fix a file you committed by accident, then add it to .gitignore so it never comes back.

Ishan Karunaratne⏱️ 7 min readUpdated
Share thisCopied
How to remove a file from Git without deleting it using git rm --cached to stop tracking the file while keeping it on disk, then ignoring it.

To stop Git tracking a file but keep it sitting on your disk, run git rm --cached <file>. The --cached flag removes the file from the index (Git's list of tracked files) without touching the copy in your working directory. Commit that change, add the file to .gitignore, and Git forgets about it while the file stays exactly where it is.

That single flag is the whole trick. Plain git rm deletes the file from both Git and your disk; git rm --cached deletes it from Git only. Almost everyone who lands here did the same thing: committed a file they did not mean to (a .env, a node_modules folder, a build artifact, an IDE config), noticed afterwards, and wants it gone from the repo without losing the actual file.

The one command

Say you accidentally committed a config.local.json that has a password in it. The file is on disk and it is tracked by Git. You want Git to forget it, but you still need the file to run your app locally.

bash
git rm --cached config.local.json

Git responds:

text
rm 'config.local.json'

The word "rm" looks alarming, but the file is still right there in your folder. All that happened is Git staged a deletion of the tracked copy. Confirm it:

bash
git status
text
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	deleted:    config.local.json

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	config.local.json

Notice the file shows up twice: as a staged deletion (Git is about to stop tracking it) and as an untracked file (the copy on disk that Git no longer knows about). That is exactly what you want. Commit it:

bash
git commit -m "Stop tracking config.local.json"

From this commit forward, Git ignores changes to that file. But it is not done yet, because the file is now untracked, which means the next person who runs git add . will commit it straight back in. You need .gitignore to make the removal stick.

Add it to .gitignore so it never comes back

Open (or create) the .gitignore file at the root of your repository and add the path:

bash
echo "config.local.json" >> .gitignore

Then commit the .gitignore change:

bash
git add .gitignore
git commit -m "Ignore config.local.json"

Now the file is doubly handled: removed from history going forward, and listed in .gitignore so neither you nor anyone else can stage it again by accident. The whole mess usually starts when a repo gets going without an ignore file, so it is worth knowing how to set Git up correctly on a new project from the start and avoid the cleanup entirely. For the full set of patterns (wildcards, directories, negation), I wrote a separate walkthrough of how .gitignore patterns actually match. The short version is that .gitignore only affects untracked files, which is why you have to git rm --cached first. Adding a tracked file to .gitignore does nothing on its own; Git keeps tracking what it already tracks.

Removing a whole folder

The classic offender is node_modules or a vendor directory committed before anyone added a .gitignore, which is exactly what happens when you start adding Git to a project that already has files in it and skip the ignore file. Same idea, but you need -r for recursive:

bash
git rm -r --cached node_modules
text
rm 'node_modules/.bin/acorn'
rm 'node_modules/acorn/package.json'
...

It prints a line per file, which can be thousands of lines for node_modules. That is fine. The folder stays on disk untouched. Add it to .gitignore and commit:

bash
echo "node_modules/" >> .gitignore
git add .gitignore
git commit -m "Stop tracking node_modules"

rm vs rm --cached

The difference is one flag, and getting it wrong deletes the file off your disk. Here is the full picture:

CommandRemoves from Git indexDeletes file on diskWhen to use it
git rm --cached fileYesNoStop tracking, keep the file locally
git rm fileYesYesDelete the file from the repo and your disk
rm file (plain shell)No (until you commit)YesDelete on disk; Git records a deletion you still commit

If you only ever remember one thing: --cached means "keep my copy." Everything without it touches your disk.

I already deleted the file by mistake

If you ran git rm without --cached and the file vanished from your disk, do not panic as long as you have not committed yet. The file still exists in the last commit, so you can bring it back:

bash
git restore --staged config.local.json
git restore config.local.json

The first line unstages the deletion; the second pulls the file back out of the last commit onto your disk. On Git before 2.23, the equivalent is git checkout HEAD -- config.local.json. If you had already committed the deletion, recover it from the previous commit:

bash
git checkout HEAD~1 -- config.local.json

This is one of those moments where the reflog quietly saves your week, because nothing in Git is gone until it is garbage-collected, which takes weeks. If you want the broader picture of unstaging and rolling back, see discarding local changes and undoing the last commit.

A critical warning: the file is still in your history

git rm --cached stops tracking a file from this commit forward. It does not scrub the file out of past commits. If you committed a secret (an API key, a database password, a private key) and then ran git rm --cached, the secret is still sitting in every earlier commit, and anyone who clones the repo or reads the history can pull it out. Pushing the "removal" commit does not help; the old blob is still there.

If what you removed was sensitive, treat it as compromised: rotate the secret first, then rewrite history to purge it. I cover the actual rewrite (with git filter-repo) in how to remove a secret from Git history. And if the repo is already public, assume the secret was scraped; exposed .git directories are a real attack vector that bots scan for constantly.

For ordinary, non-sensitive files (a build artifact, an editor config, a log), the forward-only behaviour is completely fine and you can ignore this section.

Why this trips people up

The confusion comes from how Git thinks about a file. There are three places a file can live: your working directory (the actual file on disk), the index or staging area, and the committed history. git rm --cached operates only on the index. It tells Git "remove this from the list of things you track," and because it leaves the working directory alone, your file survives.

People expect git rm to behave like a Git-aware version of the shell's rm, and it mostly does: by default it removes the file everywhere. The --cached flag is the escape hatch that says "only the Git side." Once you internalise that the index is a separate layer from your disk, the whole thing stops being mysterious. The same mental model explains git stash, git reset versus git revert, and most of the rest of the Git toolbox.

New to all of this? Start with the Git for beginners guide, which walks through the working directory, the index, and commits from scratch before you hit edge cases like this one.

Sources

Authoritative references this article was fact-checked against.

Tagsgit remove file but keep itGitVersion Controlgit rm --cachedgitignoreDevOps

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Remove a Secret from Git History

Committed an API key or password? Deleting it in a new commit does not remove it. Rotate the credential first, then scrub it from all history with git filter-repo or BFG.