Join our Newsletter — 33% off our NHI Course
Home› Guides› How to Remove Secrets From Git History: Rotate…
Guide Secrets Management

How to Remove Secrets From Git History: Rotate First, Then Rewrite and Purge Every Copy

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 11 November 2026 12 min read
On this page

Deleting a committed secret in a new commit leaves it in every earlier commit, every clone and every fork, so the credential stays readable to anyone with history access. This guide covers the one task of getting a secret out of Git history properly: revoking it first, rewriting history with git-filter-repo, force pushing, getting the hosting platform to purge what a push cannot reach, cleaning colleagues' clones, and proving the data is gone. It is written for platform engineers, repository owners and incident responders who have already confirmed a leak. The steps follow the git-filter-repo manual and the GitHub and GitLab documentation as of November 2026. Platforms are listed alphabetically; the order is not a ranking.

Key takeaways

  • Rotation comes before rewriting. The git-filter-repo manual and GitHub's documentation both say to revoke or rotate a leaked credential first; once it is dead, a rewrite may not be needed at all.
  • Rewrite only when rotation cannot fix it. Keys that protect archived data, personal data and secrets you cannot rotate are the real cases for a history rewrite.
  • A force push is not the end. Pull request and merge request refs, cached views, forks and LFS objects keep old data until the platform purges them.
  • Every old clone can recontaminate. One git pull and git push from a pre-cleanup clone pushes the secret back, so freeze work and get clones deleted or cleaned.
  • Prove it with the first changed commit. git cat-file -t on each first changed commit must return a fatal error in every copy you clean.

When you need this

Most leaked credentials do not need a history rewrite. A revoked token is useless however many copies of it exist. The decision is about what rotation cannot fix.

SituationRotate or revokeRewrite history
API key, token or password that can be revokedYes, immediatelyOptional; do it if the repository is public or widely cloned and you want to stop repeat scanner alerts
Private key for signing or TLS that can be reissuedYes, reissue and revoke the old certificate or keyOptional, as above
Decryption key for data that stays encrypted with it (backups, archives)Re-encrypt where possibleYes: the old key still opens old data
Personal data, customer data or regulated contentNot applicableYes, and assess notification duties
Credential owned by a third party you cannot reach quicklyRequest revocation in parallelYes, to shorten exposure while you wait

GitHub states that its Support team helps purge sensitive data only when the risk cannot be mitigated by rotating credentials, which is another reason to rotate first. For triage of the finding itself, see the Developer Secret Scanning Triage. For a token on GitHub specifically, see How to revoke a leaked GitHub personal access token and check what it touched.

Before you start

  • Rotate and record: revoke or rotate the credential, record the time, and check the provider's logs for use between commit time and revocation. See the Secrets Rotation Runbook Template.
  • Freeze the repository: tell everyone to stop pushing. The git-filter-repo manual warns that work pushed during cleanup will either be discarded or force you to start again. On GitLab, pushes during blob removal can fail the job with a "source repository checksum altered" error, and GitLab suggests archiving the project to make it read-only (archiving also removes the fork relationship).
  • Close or merge open pull and merge requests: GitHub recommends this because new commit hashes change their diffs and can invalidate review comments; GitLab notes open merge requests may need a manual rebase.
  • Install git-filter-repo 2.47 or later: the --sensitive-data-removal flag (short form --sdr) needs it.
  • Work in a fresh clone: with --sensitive-data-removal the tool fetches all refs, which can discard un-pushed local work. A fresh clone avoids that. --no-fetch keeps local work but risks leaving the secret in refs you did not fetch.
  • List every path and string: find every file path the secret ever lived at, including renames, and every literal value. A secret in three files and two branches needs all of them in the rewrite.
  • Check the signing impact: rewriting changes commit and tag hashes, so existing signatures will not verify against the rewritten history; check how your git-filter-repo version handles signatures. If signed history matters to you, plan to re-sign tags and tell auditors. See the Git Repository Security Guide.

Step-by-step

1. Rewrite the repository locally

  1. Clone fresh and change into the clone.
  2. Remove whole files when no version of the file should survive: git-filter-repo --sensitive-data-removal --invert-paths --path path/to/file. Use the full path from the repository root, and add a --path for every earlier location if the file was moved or renamed.
  3. Replace strings when the file should stay: write the secret values to an expressions file outside the repository, one per line, and run git-filter-repo --sensitive-data-removal --replace-text ../expressions.txt. Values are literal by default, regex: and glob: prefixes are supported, and each match becomes ***REMOVED*** unless a line ends with ==> and your own replacement. Replacement applies to non-binary files only. Delete the expressions file afterwards: it is now a list of secrets.
  4. Note the output: keep the lines under NOTE: First Changed Commit(s) and any NOTE: There were LFS Objects Orphaned by this rewrite message with its file. You need both later.
  5. Inspect before pushing: run git log --all --name-status -- path/to/file and git log -S"secret-fragment" --all -p --. Search with a distinctive fragment so the full value does not land in shell history. If either finds anything, rewrite again.
  6. Count affected pull requests on GitHub with grep -c '^refs/pull/.*/head$' .git/filter-repo/changed-refs. If the number is far higher than expected, throw the clone away and rethink: GitHub notes that once you push, the rewrite cannot be undone.

2. Push the rewritten history

  1. Force push every ref: git push --force --mirror origin. This overwrites all branches and tags on the server, discarding anything pushed since you started.
  2. Expect some refs to be rejected: forges keep review refs such as refs/pull/* and refs/merge-requests/* read-only. Those are handled by the platform in the next stage.
  3. Handle branch protection: if other refs fail, protection rules are usually the cause. Lift them for the push window only, push again, and restore them straight away. Log who changed them and when.

3. Purge what the push cannot reach

GitHub. Open a request through the GitHub Support portal with the owner and repository name, the number of affected pull requests, the first changed commits, and the orphaned LFS file if there was one. GitHub describes Support then dereferencing or deleting affected pull requests, running server-side garbage collection, removing cached views and purging orphaned LFS objects. Its documentation says this happens only when all non-pull-request refs are clean and no fork still holds the data. Forks are separate: contact fork owners yourself, as GitHub cannot share their details. See the GitHub and GitHub Actions Security Guide.

GitLab. GitLab provides two built-in options under Settings > Repository > Repository maintenance, both needing the Owner role and both irreversible. Remove blobs takes blob IDs (find them with git ls-tree -r -t --long --full-name <branch>, third column). Redact text takes exact strings, with no regex or glob, and replaces them with ***REMOVED***. After either, wait for the job (GitLab says at least five minutes without an email), then under Settings > General > Advanced select Run housekeeping, wait at least 30 minutes, and select Prune unreachable objects. GitLab's documentation says its own git filter-repo cleanup path is for large files, not passwords or keys, so use the built-in options for secrets. GitLab documents that the built-in options cannot be used on forked projects, and that blob removal also excludes public upstream projects with downstream forks. See the GitLab Security Guide.

ItemGitHubGitLab
Where the rewrite runsLocally with git-filter-repo, then force pushIn the project UI (Remove blobs, Redact text)
Who can do itAnyone who can force push all refs, plus a Support requestProject Owner role
Review refs and cachesPurged by GitHub Support on requestCleared by housekeeping and Prune unreachable objects
ForksContact fork owners directlyFork relationships can stop the process; archiving removes the relationship
What to keep for the recordTicket number, first changed commits, PR countBlob IDs or strings submitted, housekeeping and prune times

4. Clean every other clone

  • Preferred: everyone deletes their clone and clones again. CI caches, mirrors, build agents and backup copies count as clones too.
  • If someone cannot re-clone: following the git-filter-repo manual, they delete all local tags and run git fetch --prune --tags, rebase local work onto the new history (rebase, never merge: one merge commit brings the old history back), then run git reflog expire --expire=now --all and git gc --prune=now. The reflog step also drops their stash.
  • Do not rerun the rewrite in other clones: the manual warns that repeating the same git-filter-repo command elsewhere can produce different hashes and duplicate every commit.
  • Orphaned LFS objects: delete them from .git/lfs/objects/ in local clones and from the LFS server.

Verify it worked

  1. Per clone: git cat-file -t <first-changed-commit> must return a fatal error for every first changed commit. If it returns commit, run git for-each-ref --contains <hash> to find the ref still holding it.
  2. On the server: a fresh clone searched with git log -S and git log --all --name-status must find nothing.
  3. Old links: on GitLab, GitLab documents that links to old commits return 404 after text redaction. On GitHub, check that old pull request diffs no longer show the data after Support confirms the purge.
  4. Scanner: rerun your secret scanner across full history and confirm the finding does not reappear. See the Git Secret Scanning Guide.
  5. Credential: confirm the old value fails authentication. This is the check that actually protects you.

Rollback and common errors

There is no rollback once the force push lands: GitHub and GitLab both describe the rewrite as permanent. Your safety net is before the push: keep the original clone (encrypted, access-limited) until verification passes, then destroy it, because it is also a copy of the secret.

SymptomLikely causeFix
Secret reappears days laterA pre-cleanup clone was pushed, or a branch was merged rather than rebasedFind the pusher in audit logs, repeat the rewrite, enforce re-cloning
Push rejected for some branchesBranch or tag protectionLift protection for the window, push, restore
Push rejected for refs/pull/* or refs/merge-requests/*Read-only review refsExpected: use the platform purge step
Commit still resolves in a cloneOld tags, stashes or reflog entriesDelete tags, expire reflogs, garbage collect, check again
Pipelines fail after rewriteJobs or deployments pinned to old commit SHAsUpdate pinned references; GitLab notes this explicitly
Signed tags no longer verifySignatures removed by the rewriteRe-sign release tags and record the change

Hardening afterwards

  • Turn on push protection: both GitHub and GitLab document secret push protection that blocks commits containing recognised secrets. See the Push Protection Guide.
  • Add a pre-commit scanner: the same hook for every contributor. See the Git Hooks and Pre-commit Security Guide.
  • Ignore the file type: add the file pattern to .gitignore and commit it so the protection travels with the repository. See Environment Variables and .env Files.
  • Stage deliberately: GitHub advises avoiding git add . and git commit -a, and reviewing git diff --cached before each commit.
  • Remove the need for the secret: inject it at runtime from a secrets manager, or replace it with workload identity federation. See the Secrets Management Guide.

Where this sits in common frameworks (our mapping):

FrameworkReferenceHow this task supports it
NIST CSF 2.0RS.MI-01, RS.MI-02Rotation contains the incident; the rewrite and purge eradicate the exposed data
NIST SP 800-53 Rev 5IA-5(7) No Embedded Unencrypted Static AuthenticatorsRemoving the embedded secret and preventing its return
NIST SP 800-53 Rev 5IR-4 Incident HandlingRecorded containment, eradication and verification steps
MITRE ATT&CKT1552.001 Credentials In Files; T1213.003 Code RepositoriesThe behaviours an attacker uses to harvest secrets from repositories and their history

Practitioner checklist

  • Revoke or rotate the credential first and record the time and any use found in provider logs.
  • Decide, using the table above, whether a rewrite is needed at all.
  • Freeze pushes and close or merge open pull and merge requests.
  • Rewrite in a fresh clone with git-filter-repo 2.47+ and --sensitive-data-removal; keep the first changed commits.
  • Inspect with git log -S and --name-status before pushing.
  • Force push with --mirror, restoring any branch protection you lifted.
  • Complete the platform purge: GitHub Support request, or GitLab Remove blobs or Redact text, housekeeping and prune.
  • Get every clone, mirror and CI cache deleted or cleaned; contact fork owners.
  • Verify with git cat-file -t in each copy and a full-history scan.
  • Turn on push protection and pre-commit scanning, and record the incident.

Standards and references

Related NHI Mgmt Group resources: Git Secret Scanning Guide · Push Protection Guide · Exposed .git Directory Response · OWASP NHI Top 10 NHI2 Secret Leakage Guide

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 11 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org