AlgoInsight

Free lesson · Git Mastery

Removing Files (git rm)

Easy8 min readgit

Removing Files (git rm)

A note from Arun Selvam, Software Engineer at Apple iOS Frameworks.

What This Command Actually Does

git rm removes a file from your project. It does two things in one step: deletes the file from your working directory, and stages that deletion in the index so the next commit records it.

That two-in-one behavior matters because deleting a tracked file with the regular rm command (or by dragging it to the trash) only does the first part. Git still tracks the file; the working directory just doesn't have it anymore. You have to separately stage the deletion before it can be committed. git rm collapses the two steps into one.

In my second year at Anna University, before I really understood Git, I would routinely delete files in my editor and then run git add . and wonder why the deletion seemed to be working. It was - git add picks up deletions too, despite the name. But I didn't understand at the time that I had two paths: delete + add, or use git rm. Knowing the difference is the first half of this topic.

The Basic Form

The simplest use removes one tracked file:

git rm old_module.py
This removes old underscore module dot p y from the working directory and stages the deletion. After running it, the file no longer exists on disk, and git status will show it as a deletion ready to be committed. The next commit will record the file as removed from the project.

The output is brief - just one line confirming the removal:

rm 'old_module.py'

After git rm, the deletion is staged. You still need git commit to record it permanently. Until then, you can undo the removal with git restore (Topic 5.3).

Removing Multiple Files

You can list several files in one command:

git rm file1.py file2.py docs/old_readme.md
This removes all three files at once. The behavior is identical to running git rm three times. After this single command, all three files are gone from the working directory and their deletions are staged for the next commit.

Or use a glob pattern:

git rm "*.tmp"
git rm "logs/*.log"
The quoted glob lets Git expand the pattern instead of the shell. Without quotes, the shell would expand the wildcard against the current directory only; Git's expansion is smarter about path matching. The first command removes all dot t m p files anywhere; the second removes log files inside the logs directory.

I usually prefer explicit file names. Globs are convenient but they're also the easiest way to delete more than you meant to.

Removing Directories Recursively

By default git rm refuses to delete directories - you'd have to remove every file inside individually. The -r flag enables recursive removal:

git rm -r old-feature/
The dash r flag enables recursive removal. This deletes the entire old dash feature directory and stages all the file removals inside it. Without dash r, Git refuses to delete the directory and prints an error. Use dash r with care - it removes everything inside the directory, no confirmation.

A common case at Apple: removing an entire deprecated feature folder when a framework gets restructured. The directory has 30+ files inside; git rm -r handles them in one step.

The --cached Flag

This is the form that does not delete the file from your working directory. It only removes the file from Git's tracking. The file stays on disk as an untracked file:

git rm --cached config.local.json
The dash dash cached flag tells Git to untrack the file but leave it in place on the working directory. After running this and committing, Git no longer tracks config dot local dot json - future changes to it won't appear in git status - but the file itself is still there. This is the right form to use when you accidentally committed something that should have been gitignored.

This is the recovery pattern Topic 2.8 walks through for accidentally-tracked files. You committed node_modules or .env; you don't want to delete them from disk (they're still needed); you just want Git to stop tracking them. git rm --cached is the verb.

A short version of the full recovery:

git rm --cached -r node_modules
git rm --cached .env
echo "node_modules/" >> .gitignore
echo ".env" >> .gitignore
git add .gitignore
git commit -m "Stop tracking node_modules and .env"
This sequence untracks both items, updates gitignore so they stay ignored going forward, and commits the change. The node modules folder and the dot env file are still on disk locally - only Git's tracking is updated. Anyone who pulls this commit will see the folders deleted from their working directory unless they also have them locally; if they don't, the deletion takes effect for them too. Topic 2.8 covers this recovery in full.

The asymmetry is worth understanding: people who already have the files locally keep them as untracked files after the commit; people who didn't have them - fresh clones, for instance - never see them at all.

What Happens to History

A common misconception: when you git rm a file, the file's history is lost. It isn't. The file is removed from the current state of the project, but every previous commit that contained the file still has it.

This means:

That last point matters for sensitive content. If you accidentally committed credentials, deleting them in a later commit does not remove them from history. The credentials are still visible to anyone looking at the previous commit. The only way to truly remove something from history is to rewrite history (Module 8), and even then it requires force-pushing and is disruptive for collaborators. The safer answer when you commit a secret: assume it's leaked and rotate the credential.

The Plain rm Alternative

You can delete files with the regular Unix rm command (or your operating system's file manager) and then stage the deletion separately:

rm old_module.py
git add old_module.py
This achieves the same end result as git rm. The first command deletes the file from disk. The second stages the deletion. Despite the command name git add, it picks up deletions as well as additions - the verb stages whatever change is in the working directory for that path. Two commands instead of one, but sometimes more natural if you've already deleted the file before remembering Git is involved.

Or even shorter, using git add -A after the deletion:

rm old_module.py
git add -A
This stages all changes including the deletion. Useful when you've deleted several files in a batch - one git add dash A call picks them all up. Reserved for cases where you intend to stage everything; check git status first to be sure.

Both approaches work. I use git rm when I'm planning the deletion deliberately. I use rm + git add when the deletion happened naturally - through my editor, through cleanup scripts, through reorganization - before I thought about Git.

What Happens to a Modified File You Try to Remove

If the file has uncommitted modifications, git rm refuses by default:

$ git rm modified_file.py
error: the following file has local modifications:
    modified_file.py
(use --cached to keep the file, or -f to force removal)
This safety prevents accidental loss of uncommitted work. If you really want to delete a file that has local changes you don't care about, the dash f flag forces the removal. If you want to keep the local file as untracked while removing it from Git's tracking, dash dash cached is the right flag. Read the error carefully - Git is telling you which option fits each intent.

This safety net has saved me at least three times that I remember. I'd been editing a file, the changes were uncommitted, and I decided the file should go entirely. Without the safety, the changes would have been lost silently. With it, Git asks for explicit confirmation.

A Pattern at Apple

When I'm restructuring code on the iOS Frameworks team - moving functionality between modules, deleting deprecated patterns - git rm is one half of the operation. The other half is git mv (Topic 3.2 covers renaming).

The pattern I follow: stage all the deletions in one commit, separate from the additions or modifications. The reasoning is that "I deleted this code" is a different mental change from "I added this new approach." When the eventual reviewer looks at the pull request, separating the deletions makes the diff easier to read. Topic 18.4 covers atomic commits - this is one of the patterns it enables.

A concrete example from last quarter: I was migrating a UIKit animation helper from old class-based syntax to a Swift protocol-oriented approach. The change involved:

  1. Adding the new protocol-based implementation
  2. Updating call sites to use the new pattern
  3. Deleting the old class-based implementation

Three logical changes, three commits. The deletion was its own commit with a message explaining what was being removed and pointing at the commit that added the replacement. Anyone reading the history sees the deletion as a clean, intentional step rather than an undifferentiated part of a larger churn.

What Comes Next

Topic 3.2 covers git mv - renaming files. Topic 3.3 covers file permissions. Topic 3.4 covers tracking binary files, which has its own set of considerations distinct from regular source files.

What Goes Wrong

Warning: Common git rm mistakes:

Believing the file is gone from history. It isn't. Every previous commit still has the file. Deletion removes it from the current state only.

Using git rm for secrets and assuming they're cleaned up. Credentials in previous commits are still recoverable. Rotate the credential; consider history rewriting (Module 8) only as a secondary measure.

Confusing git rm with git rm --cached. The first deletes the file from disk; the second only untracks it. Pick the one that matches your intent.

Skipping the -r flag for directories and getting confused by the error. Read the error message; it tells you what's needed.

Force-removing with -f and losing uncommitted changes. The default safety is there for a reason. Override it only when you're certain.

Removing a file that you haven't pushed yet. Removal stages a deletion locally; it doesn't affect anyone else until you push.

Try It

Exercise: Practice removal:

  1. In a practice repository, commit a file. Then git rm it. Run git status - note the deletion is staged. Commit the deletion.
  2. Make a new file, commit it, then use plain rm followed by git add. Verify the result is identical to git rm.
  3. Stage a file with git rm --cached. Confirm the file is still on disk but git status no longer shows it as tracked.
  4. Try to git rm a file that has uncommitted modifications. Note the safety error. Choose between -f (discard changes), --cached (keep as untracked), or committing the changes first.
  5. Run git rm -r on a directory with several files. Notice all the deletions get staged at once.
  6. After removing a file and committing, recover it from history using git checkout HEAD~1 -- <filename> (Topic 5.2 covers this). Confirm the file's content is intact.

Back to Git Mastery