I committed an API key to GitHub — now what?

Last verified

Revoke the key. Right now, before you touch git.

Rewriting history is the part that feels productive, which is exactly why people do it first. It doesn't help: a committed key stays valid until it is revoked at the provider, and by the time you've fixed the history, a scanner has usually already read it.

Why deleting the commit does nothing

A new commit that removes the file leaves the old commit — and the key — in history. git log -p still shows it. So does the GitHub UI, if you know the SHA.

Even a full history rewrite doesn't undo the exposure, because a push is a publication event:

  • Forks keep their own copy. You cannot rewrite someone else's fork.
  • Clones already exist. Anyone who pulled has it on disk.
  • GitHub caches commits. Detached commits can remain reachable by SHA after a force-push until they're garbage-collected, which is not immediate and not under your control.
  • Scanners archive what they find. Automated harvesters index the public events firehose in near real time. If it was public for a minute, assume it was captured.

The only action that actually invalidates the credential is revocation at the provider.

Private repositories are not an exception

Private repos get cloned by collaborators, mirrored into CI, indexed by internal tooling, and occasionally flipped public by mistake. Rotate anyway. The cost of rotating unnecessarily is five minutes.

The correct order

  1. Revoke the key at the provider. Everything else is secondary.
  2. Reissue a replacement and put it in a secret manager — not a file in the repo.
  3. Deploy the new key and confirm the service works.
  4. Check usage for the exposure window — how to tell if someone used your key.
  5. Turn off auto-recharge and set a hard spend limit, so the next leak is capped.
  6. Purge the key from historyhow to remove an API key from git history.
  7. Stop it happening again — see below.

Steps 1–3 are the incident. Steps 6–7 are the cleanup. Don't invert them.

Check what else is in there

A repository that leaked one secret has usually leaked more. Before you move on, scan the full history:

# gitleaks — fast, catches most provider key formats
gitleaks detect --source . --log-opts="--all"

# or trufflehog, which verifies whether found keys are still live
trufflehog git file://. --only-verified

Check the obvious places by hand too:

  • .env files, and .env.example filled in with real values instead of placeholders
  • Jupyter notebooks — output cells keep secrets even after you edit the source cell
  • Config files, docker-compose.yml, Terraform state, CI workflow files
  • Test fixtures and integration tests with real credentials
  • README setup instructions with a real key pasted in as an example

Prevention that actually works

Ordered by how much they help relative to effort:

1. A pre-commit hook. Blocks the mistake at the only point where it's free to fix.

pip install detect-secrets
detect-secrets scan > .secrets.baseline
# then wire it into .pre-commit-config.yaml

2. .gitignore before the first commit. .env, .env.*, *.pem, secrets/. Add it when you create the repo, not after you need it.

3. GitHub push protection. Repository → Settings → Code security. Blocks pushes containing recognised secret patterns. Free on public repos.

4. Environment variables, always. No key literal in source, ever — not "temporarily", not "just for testing". Temporary code is what gets committed.

5. A spend limit and a daily number you actually see. Prevention fails eventually. What determines whether a leak costs $5 or $5,000 is how long it runs before someone notices — and nobody checks a billing console on a normal Tuesday.

That last one is what TKN is for: today's spend across every provider on one screen, checked against a rule you set, with a kill switch that revokes at the provider from your phone.

Sources