How to remove an API key from git history

Last verified

Do this after revoking, not instead of it

Rewriting history does not disable the credential. If you have not revoked the key yet, stop and do that first — how to revoke an OpenAI API key. This page is cleanup.

The tool git's own maintainers point to. Fast, and it fails loudly rather than silently doing the wrong thing.

pip install git-filter-repo

# work on a fresh mirror clone, not your working copy
git clone --mirror https://github.com/you/repo.git repo-mirror
cd repo-mirror

To remove a file that contained the key everywhere it ever existed:

git filter-repo --invert-paths --path .env --path config/secrets.json

To replace the key string itself, wherever it appears — better when the secret was inline in a source file you want to keep:

cat > /tmp/expressions.txt <<'EOF'
sk-proj-YOUR-ACTUAL-LEAKED-KEY==>REDACTED
sk-ant-YOUR-OTHER-KEY==>REDACTED
EOF

git filter-repo --replace-text /tmp/expressions.txt

Then push the rewritten history:

git push --force

Delete /tmp/expressions.txt afterwards. It contains your leaked keys in plaintext, which is a mildly absurd way to leak them a second time.

Option 2 — BFG Repo-Cleaner

Simpler for the common case, and considerably faster on large repositories.

git clone --mirror https://github.com/you/repo.git
bfg --replace-text passwords.txt repo.git   # one secret per line
# or
bfg --delete-files .env repo.git

cd repo.git
git reflog expire --expire=now --all && git gc --prune=now --aggressive
git push --force

The parts that don't get mentioned

Rewriting history changes every commit SHA after the rewrite point. That has consequences.

Everyone must re-clone. Do not let collaborators merge or rebase their old copies — that reintroduces the removed commits. Tell them explicitly to delete and re-clone.

Open pull requests break. They reference commits that no longer exist. Expect to close and reopen them.

Forks keep the secret. You have no ability to rewrite someone else's fork. If the repository has forks, the key is still out there in them.

GitHub caches detached commits. After a force-push, orphaned commits can remain reachable by direct SHA until garbage collection. That is not immediate and not under your control. For a public repository, GitHub Support can be asked to run GC and purge cached views — but treat that as tidying, not remediation.

Tags and other refs need attention. --mirror covers them; a plain clone does not.

Verify

git log --all --full-history -p -S 'sk-proj-' | head
git log --all --full-history -- .env

Both should come back empty. And run a scanner over the full history to catch what you didn't know was there:

gitleaks detect --source . --log-opts="--all"
trufflehog git file://. --only-verified

--only-verified on trufflehog is the useful flag: it tells you which discovered credentials are _still live_, which is the list that actually matters.

Then stop it recurring

  • .gitignore for .env, .env.*, *.pem, secrets/ — added when you create the repo
  • detect-secrets or gitleaks as a pre-commit hook
  • GitHub push protection: Settings → Code security
  • Secrets from environment variables only, with no literal keys in source ever

And accept that prevention eventually fails. What decides whether the next leak costs $5 or $5,000 is how quickly someone notices — which is a monitoring problem, not a git problem. TKN exists for that half.

Sources