example-petstore.com

Example domain · Not a live service · Browser visits show this page · API requests get 410 Gone

Guide · Security

Removing a secret from Git history

A key, token or password ended up in a commit. Deleting the file in a new commit does not help: the old commit still contains it, in every clone and fork. This guide shows the right order: revoke first, then rewrite the history, clean up the hosting platform, and make sure it does not happen again.

First: revoke the secret

Treat a committed secret as exposed, even in a private repository: clones, forks, CI logs and backups may already hold a copy, and automated scanners pick up new commits in public repositories within minutes.

  1. Revoke or rotate the key, token or password at the service that issued it.
  2. Put the new one in configuration or a secrets manager, not in the code.
  3. Check the service’s logs for use you do not recognise.

Where to revoke keys at common providers: Leaked credentials: what to do now. Rewriting history comes after this, never instead of it.

Rewrite the history or not

Once a secret is revoked it no longer grants access, and GitHub notes that this may be enough. A rewrite is still worth it when:

  • the secret cannot be revoked, or the file contains other sensitive data such as personal data or customer records;
  • the repository is public or will be made public;
  • you want scanners to stop reporting the old secret.

A rewrite changes the ID of every later commit. Collaborators have to rebase their work, open pull requests may lose their review comments, and commit signatures are removed. Agree on a moment with everyone involved, and merge or close open pull requests first.

Find every occurrence

# Which commits added or removed the string?
git log --all --oneline -S 'the-secret-value'

# Which files did it appear in?
git grep 'the-secret-value' $(git rev-list --all)

# Scan the whole history for known secret formats
gitleaks git -v

Note every file path the secret appeared under, including earlier names if the file was moved or renamed.

Rewrite with git-filter-repo

git-filter-repo is the tool GitHub recommends. Use version 2.47 or later, which has the --sensitive-data-removal option, and work in a fresh clone.

# Install (or use your package manager)
brew install git-filter-repo        # macOS
pip install git-filter-repo         # anywhere with Python

git clone https://github.com/YOUR-ORG/YOUR-REPO
cd YOUR-REPO

# Option 1: remove a whole file from all history
git-filter-repo --sensitive-data-removal --invert-paths --path config/secrets.yml

# Option 2: replace the secret everywhere it occurs
git-filter-repo --sensitive-data-removal --replace-text ../replacements.txt

The replacements file lists one value per line. By default each match becomes ***REMOVED***; with ==> you choose the replacement, and regex: matches a pattern:

# ../replacements.txt (outside the repository)
sk_live_51Hx0000000000000000000000
AKIA0000000000000000==>AWS_ACCESS_KEY_ID_REMOVED
regex:password\s*=\s*"[^"]+"==>password = "REMOVED"

Check the result with git log --all -S 'the-secret-value': it should print nothing. Then overwrite the remote:

git push --force --mirror origin

Branch protection that blocks force pushes has to be switched off for this moment. After the push, the rewrite cannot be undone.

Clean up GitHub

After the force push, old commits can still be reached through pull requests, cached views and forks.

  • Pull requests and cached views: contact GitHub Support with the number of affected pull requests (grep -c '^refs/pull/.*/head$' .git/filter-repo/changed-refs) and the “First Changed Commit(s)” that git-filter-repo printed. GitHub only helps where rotating the credential cannot remove the risk.
  • Forks: commits in forks remain there. Ask the owners to delete the fork or clean it up; GitHub does not share their contact details.
  • Colleagues’ clones: everyone must rebase, not merge, their branches onto the new history. A single merge brings the old commits back.

On GitLab, Bitbucket or a self-hosted server the steps are similar; check that platform’s documentation for how it removes old objects and cached views.

Alternative: BFG

BFG Repo-Cleaner is an older, Java-based tool that is still widely used. It works on a mirror clone and, by default, leaves the latest commit untouched, so remove the secret from the current version in a normal commit first.

git clone --mirror https://github.com/YOUR-ORG/YOUR-REPO.git
java -jar bfg.jar --replace-text replacements.txt YOUR-REPO.git
cd YOUR-REPO.git
git reflog expire --expire=now --all && git gc --prune=now --aggressive
git push

Stop the next leak

  • Push protection: on GitHub, secret scanning with push protection blocks a push that contains a known secret format before it reaches the repository.
  • A pre-commit check: run gitleaks or git-secrets as a pre-commit hook, and in CI for everyone who skips it.
  • Configuration, not code: read secrets from environment variables or a secrets manager, and add .env and similar files to .gitignore before the first commit.
  • Look before you commit: stage files one by one and check git diff --cached, instead of git add . or git commit -a.

Also sent that key to a placeholder address such as api.example-petstore.com? Then it reached a server outside your control as well: Configuring API clients and SDKs.

Sources