Compliance & SecOps

How to Remove Files from Git History Safely

An advanced GitHub guide to deleting files normally or rewriting Git history, with safety checks for secrets, collaborators, tags, and force-pushes.

·9 min read·
#Git#GitHub#Secrets Management#DevOps#DevSecOps#Security

Read this before running anything

This is an advanced Git and GitHub operation. A normal deletion is routine; a history rewrite is not. Rewriting history creates new commits, changes commit IDs, force-updates remote references, and can disrupt every collaborator, open pull request, release tag, integration, and downstream clone.

Use the commands in this guide with the utmost caution. Understand every placeholder and every command before you execute it. If you are unsure what a command will change, do not run it. Stop, inspect the repository, and ask an experienced repository administrator for help. For sensitive-data incidents, follow your organisation's incident-response process as well.

TL;DR

  • Use git rm when a file should disappear from the current project but may remain in past commits. This is the normal, low-risk choice.
  • Use a history rewrite only when old copies must be removed from reachable branches and tags—for example, after a secret, regulated data, or oversized artifact was committed.
  • Rotate or revoke exposed credentials before any rewrite. Rewriting Git history does not make a leaked secret safe and cannot erase copies in forks, clones, releases, logs, or caches.
  • Prefer Method A: git-filter-repo. Use Method B: git filter-branch only as a legacy fallback.
  • Never run a force-push, ref deletion, garbage collection, or reset command unless you understand its exact scope and have coordinated with repository owners and collaborators.

There are two distinct jobs people often call “deleting a file from Git”:

  1. Remove a file from the current version of the project while keeping its historical commits.
  2. Remove a file or its contents from reachable Git history by rewriting branches and tags.

The first is right for almost all ordinary cleanup. The second is for exceptional cases such as exposed credentials, regulated data, copyrighted material, or repository bloat. GitHub's guidance is explicit: rotate or revoke an exposed secret first; a history rewrite cannot make a compromised credential safe again. Read GitHub's sensitive-data removal documentation before attempting the second path.

Normal deletion: remove the current file and retain history

Use a normal Git deletion when the file no longer belongs in the project but retaining the audit trail is acceptable. Typical examples include obsolete documentation, retired source code, or an accidentally committed non-sensitive file.

git rm path/to/file.md
git commit -m "docs: remove outdated guide"
git push origin main

For a directory:

git rm -r path/to/directory
git commit -m "chore: remove obsolete directory"
git push origin main

The file remains visible in old commits, which is often desirable. You can inspect or recover it later:

git log --all -- path/to/file.md
git show <old-commit>:path/to/file.md

Do not choose a history rewrite merely because a file was deleted from the working tree. Normal deletion is simpler, reviewable, reversible through a new commit, and does not break other clones.

When a history rewrite is justified

Consider a rewrite only when the old content must no longer be reachable through the repository's normal branches and tags. Examples include:

  • A password, API token, private key, or connection string was committed.
  • Personal, customer, employee, regulated, or proprietary data was committed.
  • Large generated files, database dumps, archives, or binaries permanently bloat the repository.
  • The repository is being made public and historical internal material must be removed.

History rewriting has limits. It does not remove material from existing clones, forks, screenshots, package registries, CI logs, GitHub Releases, attached assets, external caches, or a person's local backups. GitHub may need to help purge cached sensitive data after the repository rewrite; it does not offer that service for ordinary non-sensitive cleanup.

Pre-flight checklist for advanced GitHub users

Do all of this before changing repository history:

  • Revoke or rotate exposed credentials immediately. Treat them as compromised even if the repository is private.
  • Freeze merges and pushes, and close or coordinate open pull requests. New commits made during the rewrite can be lost or can reintroduce the removed history.
  • Confirm the exact repository-relative paths, including every historical filename and location. Git does not automatically follow renames during path filtering.
  • Identify protected branches, protected tags, signed commits or tags, releases, Git LFS objects, forks, mirrors, deployment systems, and automations that rely on commit SHA values.
  • Tell collaborators that they will need a fresh clone or carefully rebased branches after the rewrite. They must never merge old history back into the cleaned repository.
  • Work in a separate fresh mirror clone. Do not experiment in your everyday working clone.

If you cannot confidently complete this checklist, do not continue.

Use Method A whenever possible. git-filter-repo is the maintained tool for repository-wide history rewriting and is safer and faster than the legacy git filter-branch workflow. For a sensitive-data cleanup, use a current version that supports --sensitive-data-removal.

Step A1: Create a disposable mirror clone

Replace the example URL with the correct repository. Do not paste a production command blindly, and do not run this inside the repository you normally use for development.

git clone --mirror git@github.com:YOUR-ORGANISATION/YOUR-REPOSITORY.git repository-cleanup.git
cd repository-cleanup.git
git remote -v

A mirror includes remote branches and tags, so it is the right place to make a repository-wide rewrite. Confirm that the displayed remote is exactly the intended repository before proceeding. If the remote is wrong, delete this disposable clone and start again; do not continue.

Step A2: Install and verify the tool

On macOS with Homebrew:

brew install git-filter-repo
git filter-repo --version

Use your platform's approved package-management process elsewhere. Do not substitute an unreviewed script or similarly named package.

Step A3: Remove one file from reachable history

For a sensitive-data path, replace the placeholder with its complete repository-relative path. If it ever had another path or name, add every old path as another --path argument.

git filter-repo --sensitive-data-removal \
  --invert-paths \
  --path path/to/unwanted-file.md

--invert-paths means “keep all paths except the selected path.” For multiple known paths:

git filter-repo --sensitive-data-removal \
  --invert-paths \
  --path path/to/old-name.env \
  --path path/to/current-name.env

For non-sensitive repository-size cleanup, omit --sensitive-data-removal only after you have confirmed that the paths do not contain secrets or regulated data. For sensitive strings that occur inside files rather than in a single file path, do not invent a replacement command: follow the official git-filter-repo and GitHub documentation for the incident.

Step A4: Verify before publishing

This is the last safe point to abandon the mirror and start over. Inspect the target path and the rewrite report before pushing anything:

git log --all -- path/to/unwanted-file.md
git remote -v

The first command should return no reachable commits for the specified path. If git-filter-repo reported affected pull-request references, record them for the GitHub cleanup process. Also verify that expected branches and tags still exist, that releases and deployment references have an owner, and that the mirror's origin is the repository you intend to overwrite.

Step A5: Force-push the approved mirror

Danger: the next command force-updates every mirrored reference. It can overwrite remote branches and tags. Run it only after the team freeze, review, and explicit repository-owner approval.

git push --force --mirror origin

GitHub-managed refs/pull/* references are read-only, so failures for those references are expected. Failures for normal branches commonly mean protection rules are still active; do not work around policy without repository-owner approval. For sensitive-data removal, follow GitHub's documented support process afterward to address affected pull requests, cached views, and any orphaned LFS objects.

Method B — legacy fallback: remove history with git filter-branch

Use Method B only when git-filter-repo is unavailable and a repository owner has explicitly approved this fallback. git filter-branch is older, slower, and easier to misuse. Method A remains the preferred approach, especially for exposed secrets.

Step B1: Start in a dedicated, clean clone

Create a fresh non-mirror clone and verify that it has no uncommitted work. Do not clean, stash, reset, or discard work you do not fully understand.

git clone git@github.com:YOUR-ORGANISATION/YOUR-REPOSITORY.git repository-cleanup
cd repository-cleanup
git status --short

The last command must produce no output before proceeding. Confirm the repository path and origin once more:

git remote -v

Step B2: Rewrite all local branches and tags

Replace only the exact repository-relative path you verified in the pre-flight checklist. This command rewrites every local reference selected by -- --all; it is not a normal file deletion.

FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --force \
  --index-filter 'git rm --cached --ignore-unmatch path/to/unwanted-file.md' \
  --prune-empty \
  --tag-name-filter cat \
  -- --all

For several files, list each exact path after --ignore-unmatch:

FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --force \
  --index-filter 'git rm --cached --ignore-unmatch path/to/old-name.env path/to/current-name.env' \
  --prune-empty \
  --tag-name-filter cat \
  -- --all

--ignore-unmatch allows a path to be absent in individual commits. --prune-empty removes commits left empty by the rewrite, and --tag-name-filter cat rewrites tag targets without renaming the tags.

Step B3: Verify the path before deleting local backup refs

filter-branch creates backup references under refs/original/, so the old history is still locally reachable at this point. Verify both the rewritten history and the backup references:

git log --all -- path/to/unwanted-file.md
git for-each-ref --format='%(refname)' refs/original/

The first command may still show commits because refs/original/ is included by --all. That is expected before the next step. Inspect the output carefully; if the wrong path was filtered, abandon this clone and get help rather than trying to repair the rewrite in place.

Step B4: Remove backup refs and prune local objects

Danger: these commands permanently remove locally recoverable Git objects, including unrelated work that exists only in this clone. Execute them only in the dedicated cleanup clone after a successful review.

git for-each-ref --format='%(refname)' refs/original/ | xargs -n 1 git update-ref -d
git reflog expire --expire=now --all
git gc --prune=now

Step B5: Verify again, then force-push branches and tags

First, the target path should no longer have reachable history:

git log --all -- path/to/unwanted-file.md

Only after that result is empty and the repository owner approves, publish the rewritten primary branch and tags. Replace main if the repository uses another primary branch.

git push --force origin main --tags

List remote branches immediately afterward. Any old branch that still retains the file must be separately rewritten or deliberately removed with repository-owner approval.

git ls-remote --heads origin

Releases, tags, forks, and collaborators

A rewrite changes commit IDs. Tags will point to rewritten commits, and signatures tied to the original commits or tags no longer validate. Audit release notes, attached binaries, source archives, package releases, and immutable or protected tags. If a released artifact contained the unwanted material, revoke, rebuild, and republish it according to the relevant platform's process.

Every fork and old clone can still contain the original history. Coordinate with fork owners where needed. The safest instruction for collaborators is to create a new clone:

git clone git@github.com:YOUR-ORGANISATION/YOUR-REPOSITORY.git

Developers with unmerged work should get repository-specific help. They must rebase their work onto the cleaned history or transfer only reviewed changes; merging an old branch can restore the removed commits. Avoid giving a generic destructive reset command to a team without first confirming that no local work needs preserving.

Prevent the next incident

After the cleanup, fix the condition that allowed the file into Git. Add appropriate ignore rules for generated or local-only files, store credentials in a secret manager, enable GitHub secret scanning and push protection where available, and review staged changes deliberately with git diff --cached. Use specific git add and git rm commands instead of broad staging commands when a change is sensitive.

Final takeaways

  • Choose git rm for ordinary removals. It changes today's project without disrupting history or collaborators.
  • Treat a history rewrite as a coordinated security and operations event, not as a routine cleanup command.
  • Revoke or rotate secrets first. A rewrite reduces exposure in the central repository but cannot recall credentials or erase copies outside it.
  • Use the recommended git-filter-repo method in a dedicated clone, verify every target path and ref, and force-push only with explicit repository-owner approval.
  • Communicate the rewrite to every contributor, fork owner, and release owner, then prevent recurrence with ignore rules, secret management, and push protection.

The important distinction is simple: git rm removes a file from today's project; git-filter-repo rewrites the story of the repository. Know which operation you need before you type a command.

Public profile lookup

Ask AI About the Author

Open this query in ChatGPT, Claude, or Perplexity.

Comments

Comments are open to confirmed email subscribers. Use the email you subscribed with. To edit a comment, delete it and post a new one.

0/2000
Verify:

    Subscribe to get the new blogs.

    Field notes from someone who ships before they write about it. Sovereign AI, AI-SDLC, DevOps, and what 59 production deployments teach you. No spam. Unsubscribe anytime.

    Related field notes