Nov 12, 2025·6 min read

Remove secrets from Git history after an AI prototype leak

Learn how to remove secrets from Git history using git filter-repo or BFG, rotate leaked keys, and confirm your repo is clean before shipping.

Remove secrets from Git history after an AI prototype leak

What a leaked secret in Git really means

A "secret" is anything that grants access if someone else gets it: API keys, access tokens, passwords, database connection strings, signing certificates, and private keys. If it can log in, charge a card, read data, or deploy code, treat it as a secret.

When a secret is committed to Git, it becomes part of the repository history. Deleting the file later (or adding it to .gitignore) does not remove the earlier commit that still contains the value. Anyone with access to the repo, a fork, an old clone, or a cached copy can still find it. That is why removing secrets from history is a separate job from deleting the file.

AI-built prototypes leak secrets in predictable ways. A common pattern is committing an .env file during early setup and forgetting it. Another is pasting a working snippet from a provider dashboard into source code "just to test." Debug logs can be just as bad if they print tokens and then get committed with everything else.

What’s at risk is more than embarrassment: unauthorized access to services (databases, storage, auth providers), unexpected billing from abused APIs, exposure or modification of customer data, and lost trust if users or partners hear about it.

A concrete example: a prototype uses a real Stripe key, commits it once, then switches to a new key in a later commit. Even if the current code looks fine, the first key is still sitting in history, waiting to be copied.

Stop the bleeding first

The moment a secret lands in Git history, treat it as exposed. Even if the repo was private, it may already be copied by a teammate’s clone, a CI runner, a build cache, or an AI tool that pulled the code. Don’t wait to confirm. Act like an attacker has it.

First goal: make the leaked key useless. Revoke, disable, or delete it as quickly as you can. If the provider supports scoping, you can reduce permissions immediately as a temporary step, but plan to fully rotate the credential after cleanup.

Next, stop anything that might keep using or spreading the secret. Pause CI jobs, scheduled workflows, preview environments, and auto-deploys. These often fetch environment variables and produce logs that can leak values again.

Before you touch Git history, get the team aligned:

  • Revoke or disable the exposed credential.
  • Pause CI/deploys and tell everyone not to push, merge, or tag until the plan is clear.
  • Capture the exact value and where it appeared (file name and commit hash) so you can remove the right thing.
  • Keep a private incident note: what was rotated, when, and which services might be affected.

Example: if your prototype committed a Stripe key and CI runs tests on every push, the pipeline can keep calling Stripe with the leaked key and leave traces in logs. Pause the pipeline, revoke the key, then move to the history rewrite.

Inventory what needs to be purged

Before you rewrite history, get specific about what leaked and where it shows up. This is how you avoid a "cleanup" that misses the real problem.

Write down every leaked value you know about: API keys, database URLs, JWT secrets, OAuth client secrets, webhook tokens. If you only have a screenshot or an error log, extract the exact text if you can.

Then map each secret to its locations in the repo. Don’t just look at the current branch. Secrets often live in old commits, test files, debug output, exported data, and even screenshots.

Common places to check include .env variants, config files (like settings.json or docker-compose.yml), logs and tmp folders, seed/sample data, and docs/assets that might contain screenshots.

A quick way to search for a known string in your working copy:

git grep -n "PASTE_PART_OF_KEY_HERE" -- .

Decide what you need to remove:

  • Exact strings (best when a token appears in multiple places)
  • Whole files (best for .env or exported data that never belonged in Git)
  • Whole folders (best for logs/, tmp/, accidental backups)

Example: an AI-generated prototype might have committed .env and also pasted a production database URL into config.ts. That’s two targets: remove .env from all history, and remove the exact URL string everywhere it appears.

Finally, make a safe backup of the repo before rewriting history. Copy the entire folder or create a mirror clone so you can recover if the rewrite goes wrong.

Step by step: purge with git filter-repo

If you need flexible, reliable rewrites (multiple branches, tags, odd history, or more than one secret), git filter-repo is usually the best choice.

Start from a fresh clone and make a safety copy. Close any open PRs that might reintroduce the old commits.

1) Remove a file everywhere (example: an env file)

If a prototype accidentally committed .env (or a credentials JSON), purge it by path:

git filter-repo --force --invert-paths --path .env

That rewrites every commit and drops the file wherever it appeared.

2) Remove or replace secret strings by content

When secrets are embedded inside code, use a replace file. Create replacements.txt like this (left side is what to match, right side is what to write):

AKIAIOSFODNN7EXAMPLE==>REMOVED
"api_key": "sk-live-123"==>"api_key": "REMOVED"

Then run:

git filter-repo --force --replace-text replacements.txt

Before you rewrite, decide which branches and tags you will include. By default, filter-repo rewrites what is in your local clone, so fetch everything you plan to keep (including tags) and delete any branches you do not want to publish.

After the rewrite, confirm nothing broke locally. Run your build and tests, start the server, and do a quick end-to-end check (for example, a basic login flow). Then search again for old key patterns.

Step by step: purge with BFG Repo-Cleaner

BFG Repo-Cleaner is a good fit when you want a fast cleanup, especially for deleting whole files like .env or creds.json, or swapping out known key strings.

Before you start, make a mirror clone so you rewrite all refs (branches and tags) safely:

# 1) Mirror clone (works best for history rewrites)
git clone --mirror <your-repo-url> repo.git
cd repo.git

# 2) Remove whole files everywhere in history
bfg --delete-files .env
bfg --delete-files creds.json

# 3) Or replace leaked text (use a rules file)
# lines like: OLD_SECRET==>REMOVED
bfg --replace-text replacements.txt

# 4) Prune old objects BFG made unreachable
git reflog expire --expire=now --all
git gc --prune=now --aggressive

BFG rewrites commits that contained the secret, creating a new history where those blobs or strings are gone. It does not rotate credentials, and it can’t protect you from copies of the repo that someone already cloned.

Validate the result before pushing anything. Search for exact tokens you expect to be gone, check that sensitive files no longer exist in any commit, and inspect tags too.

Push the rewritten history without causing chaos

Get Authentication Working Again
We fix broken login flows, token handling, and permission bugs that often follow a leak.

After you rewrite history, the push is where teams often get surprised. The goal is simple: publish the cleaned history, then stop anyone from accidentally reintroducing the old commits.

Coordinate a short freeze (15 to 60 minutes) where nobody pushes, merges, or opens new branches. Decide what you truly need to rewrite: usually main plus any long-lived branches people actually use. Old feature branches can often be deleted instead of rewritten.

A safe sequence:

  • Announce the freeze and confirm everyone is paused.
  • Temporarily relax protected branch rules if they block force pushes.
  • Force-push the rewritten branches (and tags if needed).
  • Re-enable branch protections right away.
  • Tell everyone exactly how to sync their local clones.

Because commit IDs changed, you will usually need a force push. Give collaborators one of two options: re-clone (simplest) or hard reset (faster, but easy to get wrong). For example:

git fetch --all --prune
git checkout main
git reset --hard origin/main

Also clean up where secrets like to linger: old local clones, CI caches, build artifacts, and mirrored repos.

Rotate credentials the safe way

Once a secret has been committed, treat it as compromised. Rotate every leaked credential you can find, not just the one that triggered the alarm.

List anything that could grant access: API keys, database passwords, service account files, OAuth client secrets, JWT signing keys, SMTP creds, webhook signing secrets, and any "test" keys that point to real systems. If the prototype touched production at any point, include production in the rotation plan.

Create new credentials with least privilege. Prefer scoped tokens and short lifetimes when the provider supports it.

A safe order:

  • Create new secrets first, then deploy them to apps and CI.
  • Update production, staging, and local dev configs.
  • Confirm the app works end to end.
  • Revoke old secrets last, then monitor for errors.

After rotation, keep secrets out of the repo for good. Use hosting platform variables or a proper secret store, and keep local secrets in an uncommitted env file. Add guardrails like a pre-commit secret scan and CI checks.

Keep a simple record of what changed, when, where the new value is stored, and who owns it.

Verify the repo is actually clean

Free Code Audit First
Get a clear remediation plan first with a free code audit before any commitment.

Assume you are not done until you prove the leak is gone everywhere a developer could fetch it.

Search for the exact leaked value, not just the filename. Run the search across all refs (branches and tags), not only your current branch.

git fetch --all --tags --prune

git grep -n "PASTE_LEAKED_VALUE_HERE" $(git rev-list --all)

Then do a broader scan for common secret shapes. Look for private keys (BEGIN PRIVATE KEY), long base64-like strings, JWTs (three dot-separated chunks), and any provider-specific prefixes you recognize.

Don’t forget CI logs and build artifacts. Even if the repository is clean, a leaked secret may still be stored in CI logs, cached Docker layers, or uploaded artifacts. Re-run the last pipeline after rotating credentials to make sure logs no longer print sensitive values.

Finally, do a fresh clone test from the remote in a new folder or clean machine. A repo can look clean locally while an old remote ref still exposes the secret.

mkdir /tmp/clean-test && cd /tmp/clean-test
git clone <your-remote>
cd <repo>
git log --all -S "PASTE_LEAKED_VALUE_HERE" --oneline

If you keep finding traces after a rewrite, it usually means a tag, remote branch, or CI artifact is still holding the old content.

Common mistakes that bring secrets back

Most leaks return because the cleanup was only half done. Deleting a file in the latest commit is not the same as removing it from history. If someone can still check out an old commit, the secret is still exposed.

Two common misses:

  • Forgetting tags and old branches. A secret can be gone on main but still live in a release tag or a stale branch.
  • Removing a file but not the value. AI prototypes often copy the same key into multiple places: config files, test fixtures, build scripts, and logs.

Another frequent cause is a teammate pushing from an old clone after the rewrite. Coordinate one moment for everyone to re-clone or reset, and block merges until that’s done.

Example: an AI prototype leak and a realistic cleanup plan

A common story: a founder builds a quick prototype in Lovable or Replit, then commits everything to Git without noticing a .env file slipped in. The demo works, and then something feels off.

What shows up first is usually real-world fallout: a cloud bill spike overnight, strange login alerts from an email provider, or an API dashboard showing traffic from countries they do not serve.

If the repo is public, assume the keys are already copied. Rotate credentials immediately, then rewrite Git history to remove the file and any pasted tokens. If the repo is private, do the same work, and also review who had access (old contractors, shared machines, CI logs).

If you don’t know which key leaked, act like all of them did. List every system the prototype touched (database, auth provider, payments, email, storage) and rotate in an order that won’t lock you out.

A realistic plan:

  • Freeze deploys and pause CI so nothing keeps spreading the value.
  • Rotate the most dangerous credentials first (cloud admin, database, payment keys).
  • Rewrite history to remove .env and any committed tokens, then force-push.
  • Update the app to read secrets only from safe config (env vars or a secret manager).
  • Verify the result with a fresh clone and full-history searches.

Communication matters. Tell stakeholders what happened in plain terms, what was exposed, what you rotated, and what you verified. Give a short timeline and a clear note on what changes for them.

Quick checklist before you resume development

Purge Git History Safely
We safely remove secrets from Git history and help you rotate credentials without breaking deploys.

After a leak, the goal is straightforward: make the stolen secret useless, erase it from history, and prove it’s gone before you write new code.

  • Revoke first, then pause work. Disable the leaked key/token right away and stop pushes during cleanup.
  • Inventory what leaked. List every filename and pattern that might contain credentials.
  • Rewrite history once, with one tool. Pick either git filter-repo or BFG and run it on a fresh clone.
  • Push the new history and reset collaborators. Force-push rewritten branches/tags, then have everyone re-clone (or hard reset).
  • Rotate and relocate. Create new credentials, move secrets out of the repo, and add checks to prevent repeats.

Do one final proof check: clone into a brand-new folder and scan that fresh clone. If the scan is clean and the app runs with the new credentials, you’re back to a safe baseline.

Next steps if the repo is messy or the app is already broken

Sometimes the problem is bigger than a history rewrite. If the repo is tangled, the build is failing, or you don’t even know what leaked, it’s easy to burn days on Git surgery while the app stays unstable.

Signs it’s time to get help: multiple repos and forks you can’t track, secrets duplicated across generated files, deploys failing after the rewrite, suspected multiple leaks, or a codebase that’s hard to change safely (spaghetti structure, no tests, random AI edits).

If you inherited an AI-generated prototype, this is exactly what FixMyMess (fixmymess.ai) is built for: diagnosing the codebase and history, repairing logic and security issues (like broken auth or exposed secrets), and getting the app ready to deploy cleanly. They offer a free code audit to map the exposures before you commit to changes, and most remediation work is completed within 48-72 hours.

FAQ

If I deleted the `.env` file, why is the secret still “leaked”?

It means the secret is now in the repository’s history, not just in the current files. Even if you delete the file or add it to .gitignore, anyone who can access an old commit, a fork, or an earlier clone can still retrieve the value.

What should I do first when I discover a secret in Git history?

Revoke or disable the exposed credential immediately so the leaked value stops working. Then pause CI/deploys that might keep using or logging it, and only after that rewrite Git history to purge the secret from past commits.

Is it still dangerous if the repo was private?

Yes. Private repos still get copied through teammate clones, CI runners, build caches, mirrored repos, and shared machines. Treat any secret committed to Git as compromised and rotate it, even if you believe access was limited.

Should I use `git filter-repo` or BFG Repo-Cleaner?

For most cases, git filter-repo is the safer default because it’s flexible for multiple branches, tags, and complicated histories. BFG is often faster and simpler for deleting specific files or doing straightforward text replacements, but you still must validate tags and run cleanup steps afterward.

Why do I need to force-push after cleaning the repo?

Force-pushing is necessary because history rewrites change commit IDs, so the cleaned commits are “different” from the old ones. Plan a short freeze, push the rewritten branches/tags once, then have everyone re-clone or hard reset so nobody accidentally pushes the old commits back.

How can I confirm the secret is truly gone after the rewrite?

Search across all refs, not just the current branch, and look for the exact leaked value as well as common secret patterns. Then do a fresh clone into a new folder and search again; a fresh clone is the easiest way to catch leftover remote branches or tags still carrying the secret.

If I rotate the key, do I still need to remove it from Git history?

Because you can’t reliably prove who saw it, and clones or logs may already contain it. Rotation makes the stolen value useless; the Git cleanup is about preventing future discovery and accidental reuse, but it doesn’t undo prior exposure.

What usually causes secrets to come back after a cleanup?

The most common mistake is cleaning only main and forgetting tags or stale branches that still reference the old commit. Another frequent issue is a teammate pushing from an old clone after the rewrite, which reintroduces the secret unless everyone resets or re-clones.

How do I prevent AI-generated prototypes from leaking secrets again?

Don’t commit .env files, and load secrets from environment variables or a secret store provided by your hosting platform. Add a pre-commit secret scan and CI checks so a pasted token or debug log doesn’t slip into a commit again.

Can FixMyMess help if the repo is tangled or I’m worried I’ll break things?

Yes. If the codebase is messy, builds are failing, or secrets are duplicated across generated files, a history rewrite can become risky and time-consuming. FixMyMess can audit the repo, identify where secrets were exposed, clean the history safely, rotate credentials without breaking deploys, and get the app back to a deployable state quickly.