Guide

Accidentally Pushed an API Key or .env File to GitHub? What to Do Next

Leaked an API key, password or .env file to GitHub? A calm, step-by-step guide to revoking it, checking for misuse, cleaning history and preventing a repeat.

By Joe Shenton7 min readSecuritySecrets

It happens to good developers all the time. You run git add ., push, and a minute later notice that .env, a cloud access key or a live Stripe key went up with it.

The instinct is to delete the file, force-push and hope nobody saw. That's the wrong order. This guide covers what to actually do, in the order that matters, plus how to stop it happening again.

Why minutes matter

Leaked secrets aren't found by chance. Automated scanners watch public GitHub commits around the clock.

So assume that if a secret was pushed to a public repo, someone else already has a copy. Private repos are lower risk but not safe: anyone with access, any forks, every developer laptop and every CI log that cloned it has a copy too.

Step 1: Revoke or rotate the secret, first

Deleting the file doesn't help. Even rewriting history doesn't help if the key still works. The only fix that actually closes the hole is making the leaked value useless. GitHub's own guide to removing sensitive data puts this first, and notes that for credentials, rotating may be all you need.

Do this for every secret in the leaked file, not just the one that worried you. A .env usually holds several: database URL, mail provider, payment keys, OAuth client secrets, signing keys.

Secret type What to do
AWS access key Create a new key, update your app, then deactivate and delete the old one. If the key is still in use, deactivate it immediately and accept the brief outage.
Stripe secret or restricted key Roll it in the Dashboard and choose to expire the old key now. Stripe's key best practices say to rotate immediately "even if you aren't sure anyone saw it".
GitHub personal access token Revoke it in Settings → Developer settings, then issue a new fine-grained token with the least scope that works.
Database password or connection string Change the password, and restrict network access to the database if it's reachable from the internet.
Session or JWT signing secret Replace it. Every existing session or token becomes invalid, which is exactly what you want.
Third-party SaaS / AI API key Regenerate it in the provider's dashboard, and check usage and billing pages.

For AWS, the rotation looks like this with the CLI:

# 1. Create a replacement key and put it in your app's secret store
aws iam create-access-key --user-name my-app-user

# 2. Disable the leaked key (it stops working immediately)
aws iam update-access-key --user-name my-app-user \
  --access-key-id AKIAOLDKEYEXAMPLE --status Inactive

# 3. Once you're sure nothing needs it, delete it
aws iam delete-access-key --user-name my-app-user \
  --access-key-id AKIAOLDKEYEXAMPLE

A note on AWS: when GitHub spots an AWS key in a public repo, AWS attaches a quarantine policy called AWSCompromisedKeyQuarantine to the IAM user. Unit 42 tested this and saw it applied about 10 seconds after the push. It's useful, but it only denies a list of risky actions. It doesn't disable the key, and anything the policy doesn't cover still works. Don't treat the quarantine email as "handled".

Step 2: Check whether it was used

Once the secret is dead, look back at the window it was live.

  • Cloud accounts: check CloudTrail (or your provider's audit log) for API calls you don't recognise, new IAM users or keys, and instances in regions you never use. Check billing too: cryptomining shows up as cost before anything else.
  • Stripe: review the request logs for calls from IPs or integrations that aren't yours.
  • GitHub: check the organisation or account security log for new tokens, deploy keys, SSH keys or OAuth app authorisations.
  • Databases: look at connection logs and recently created users, and check whether anything was exported or dropped.
  • Email and SaaS providers: check sent logs and usage. A leaked mail API key is often used for spam, which can wreck your domain's sending reputation.

If you find signs of real misuse rather than just exposure, treat it as an incident. Write down what you've found with timestamps, contain it (disable the accounts, block the IPs), and then deal with clean-up. If customer personal information may have been accessed, get advice on your obligations, because that can trigger notification requirements.

Step 3: Stop tracking the file

A very common mistake is adding .env to .gitignore after it's already been committed. Git keeps tracking files it already knows about, so the next commit happily includes it again. Untrack it explicitly:

echo ".env" >> .gitignore
git rm --cached .env
git commit -m "Stop tracking .env"

--cached removes the file from Git but leaves your local copy alone.

Then commit a .env.example with the variable names and dummy values, so new developers know what to set without anyone sharing real values.

Step 4: Decide whether to rewrite history

The secret is still in your Git history. Whether you need to remove it depends on what it was.

If it was a rotated credential (API key, token, password), rewriting history is mostly tidiness. The old value no longer works. Many teams leave history alone in this case, because a rewrite is disruptive.

If it was data that can't be rotated, such as customer records, a private key you can't replace, or personal information, then rewrite history properly. GitHub recommends git-filter-repo (version 2.47 or later), run against a fresh clone:

# Remove a file from every commit in history
git-filter-repo --sensitive-data-removal --invert-paths --path .env

# Or replace specific strings everywhere they appear
# (passwords.txt lists one secret per line)
git-filter-repo --sensitive-data-removal --replace-text ../passwords.txt

# Push the rewritten history
git push --force --mirror origin

Be aware of the side effects GitHub documents:

  • Every commit hash after the change is rewritten, which can break tooling and links.
  • Anyone with an old clone can push the secret straight back. Everyone needs to re-clone or rebase, not merge.
  • Forks keep the original commits, and cached views and pull request references can still expose them. For full removal you need to contact GitHub Support with the details filter-repo gives you.
  • Commit signatures are removed.

Making the repository private or deleting it is not a substitute. Clones, forks and scanners have already taken their copy.

Step 5: Check the places secrets hide besides your code

The .env in the repo is the obvious one. While you're cleaning up, check the less obvious ones:

  • Frontend bundles. Variables with a NEXT_PUBLIC_ (Next.js) or VITE_ (Vite) prefix are compiled into the JavaScript sent to every visitor. A secret key in one of those is public even if your repo is private.
  • CI logs. A debug echo or env dump in a build step can print secrets into logs that many people can read.
  • Issues, pull request comments and gists. People paste error output containing tokens. GitHub's partner secret scanning covers issues, PRs, discussions, wikis and public npm packages for this reason.
  • Docker images. A COPY . . before .dockerignore excludes .env bakes it into an image layer.
  • Chat and email. If a key was shared in Slack or a ticket, it's leaked to everyone with access to that history.

Step 6: Make it hard to happen again

Prevention is cheap compared with clean-up. In rough order of value:

  1. Turn on GitHub secret scanning and push protection. Push protection blocks a push containing a recognised secret before it lands. It's free on public repositories. We cover the set-up in our GitHub organisation security checklist.

  2. Add a pre-commit scanner. gitleaks catches secrets on your machine before they're even committed, and works with private repos without a paid add-on:

    # .pre-commit-config.yaml
    repos:
      - repo: https://github.com/gitleaks/gitleaks
        rev: v8.30.1
        hooks:
          - id: gitleaks
    

    Run gitleaks git -v once against the whole repository to find anything already sitting in history.

  3. Keep secrets out of files altogether. Use your platform's secret store (Cloudflare, Vercel, AWS Secrets Manager, GitHub Actions secrets) and inject values at runtime.

  4. Use least-privilege, short-lived credentials. Restricted Stripe keys, fine-grained GitHub tokens, and OIDC instead of long-lived cloud keys in CI. A leaked key that can only read one thing is a much smaller problem.

  5. Practise rotation. If rotating a key is a scary, unknown process, it won't happen quickly when it matters. Write down where each secret lives and how to replace it.

The short version

  1. Revoke or rotate every secret in the leaked file. Now.
  2. Check logs and billing for use during the window it was live.
  3. git rm --cached the file and fix .gitignore.
  4. Rewrite history only if the data can't be rotated, and follow GitHub's process if you do.
  5. Look for secrets in bundles, logs, images and chat.
  6. Turn on push protection and a pre-commit scanner.

If you'd like a second pair of eyes, our Tech Audit ($1,800 flat) includes a security and dependency review of your codebase, history and deployment set-up, with a written report ranked by impact.

Found a leaked key and not sure what it could reach? Tell us what happened and we'll help you work through it.