Security Basics

8 min

How to Safely Remove Secrets From Git History

How to Safely Remove Secrets From Git History

A deleted .env file isn't gone if it was ever committed. What to rotate first, how to rewrite history safely with git filter-repo, and why the cleanup matters less than the credential rotation.

A deleted .env file isn't gone if it was ever committed. What to rotate first, how to rewrite history safely with git filter-repo, and why the cleanup matters less than the credential rotation.

A .env file deleted in the next commit is not gone. If it was committed even once, the key may still be available in Git history, a pull request diff, a fork, a developer’s local clone or a CI log. To remove secrets from git history safely, treat the event as a credential exposure first and a repository-clean-up task second.

That order matters. Rewriting history can reduce future discovery, but it cannot make a copied credential private again. A Supabase service-role key, Stripe secret key, cloud access token or production database password should be considered compromised from the moment it reaches a shared repository.

Start by containing the credential

Before changing Git history, identify what the secret can access and revoke or rotate it. Do not wait until the repository is tidy. If the credential has production privileges, the safest assumption is that it has been seen.

For example, rotate a payment provider secret in its dashboard, create a replacement database password, revoke a GitHub personal access token, or issue a new API key with the minimum permissions required. Update the live application, deployment environment and CI/CD variables with the replacement. Then test the affected flow. A key rotation that breaks checkout, login or background jobs can create its own incident.

The scope depends on the secret. A public client-side analytics identifier may not need the same response as an AWS key with administrator access. But many startup incidents come from misclassifying powerful credentials as harmless configuration. Supabase service-role keys bypass row-level security. An exposed signing secret may allow forged JWTs. A database URL can be enough to turn a repository mistake into direct customer-data access.

Check provider audit logs where available. Look for unexpected API calls, new users, changes to webhooks, database queries, failed authentication bursts or usage from unfamiliar IP addresses. This is not always conclusive, but it tells you whether the problem is limited to exposure or has become active misuse.

Map everywhere the secret may exist

A repository rewrite only addresses Git objects. The same value may appear elsewhere, particularly in a rapidly built application.

Search the repository, including configuration examples, test fixtures, shell scripts, infrastructure files and generated frontend bundles. A secret placed in a NEXT_PUBLIC_ variable, a Vite VITE_ variable or a browser bundle is public by design. Removing it from the repository will not remove it from a deployed JavaScript asset or a CDN cache.

Also review open and closed pull requests, issue comments, release artefacts, build logs, deployment logs and copied snippets in team chat. If the repository is public, check forks. If it is private, remember that former collaborators, contractors and connected integrations may still have access to historical objects.

This is the point at which a quick automated scan helps, but it should not be the only check. Secret scanners can find known token formats and high-entropy strings. Manual review is needed to understand whether a value is live, what it grants access to and whether an AI-generated configuration file has duplicated it in a less obvious place.

Remove secrets from Git history without breaking the repo

Once the credential is rotated, rewrite history from a clean clone. Tell the team not to push while this is happening. A force-pushed rewrite can be undone accidentally when someone pushes an old branch or stale clone back to the remote.

For most teams, git filter-repo is the practical option. It is built for rewriting history and is generally safer and faster than older approaches such as filter-branch. First make a protected backup of the repository. Keep it access-controlled, because it still contains the exposed secret.

If a file should never have been committed, remove that path from all history. For example:

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

If the file has appeared in several locations, specify each path. Be precise: deleting a broad directory may remove application files you need. If the secret is embedded in a file that must remain, use a replacement file with git filter-repo to replace the exact value throughout history. Test the replacement against a copy first, especially when the value contains special characters or has appeared in encoded form.

After rewriting, search all reachable history again. Inspect old tags and branches, not only the default branch. Then force-push the rewritten refs to the remote. Repository hosts may retain inaccessible objects for a limited period, and support intervention may be needed for an urgent public exposure. Repository settings, forks and pull request references can affect what remains reachable, so follow your host’s current remediation process as well.

Do not confuse a green force-push with proof of removal. Clone the remote into a new directory and run the search there. Check for both the full value and distinctive fragments. If a scanner flagged the secret initially, run it again after the rewrite.

Help the team recover cleanly

Every contributor needs to stop using their old clone. The least error-prone approach is usually to delete it and clone the rewritten repository afresh. If engineers must preserve local work, they need to carefully rebase or patch it on to the new history without reintroducing the affected file.

Communicate the change clearly: which branches were rewritten, when the new remote history became authoritative, and which files must never be restored. This is operational hygiene, not ceremony. One well-meaning git push --force from an old clone can put the secret back into the repository within minutes.

Fix the condition that allowed the commit

History rewriting is a response, not the control. The lasting fix is to make it difficult for secrets to enter source control and easy to detect when they do.

Keep production credentials in your deployment platform’s encrypted environment variables or a dedicated secrets manager. Commit an .env.example file containing variable names and safe placeholders, never working values. Add real environment files to .gitignore, but do not rely on .gitignore alone - it cannot protect a file already tracked by Git.

Add pre-commit scanning to catch secrets before a developer creates a commit, then run the same checks in CI to catch bypasses. Configure repository-level secret scanning and alerts where your source host provides them. These controls should cover feature branches and pull requests, not merely the main branch.

Use separate keys for local development, staging and production. Limit each key to the smallest permission set and rotate high-impact credentials on a schedule. For Supabase, keep service-role keys out of browser code and verify that row-level security is enabled and policies restrict each user to the data they should access. Rotation does not repair an API that was already authorised to bypass controls.

It is also worth reviewing deployment behaviour. A secret can leave Git but remain exposed through verbose build output, source maps, server error messages or a misconfigured client environment variable. The repository and the live application need to agree on where sensitive values belong.

When a clean-up needs independent review

If the secret granted production access, the repository was public, or you cannot confidently identify every copied location, get an independent review. The question is no longer only whether Git history is clean. It is whether the exposed credential enabled access to customer data, payment systems, authentication flows or internal infrastructure.

A focused assessment can trace the key’s effective permissions, inspect logs, validate RLS and API controls, and confirm that the replacement configuration does not create a new outage. HollowByte reviews both repository evidence and the live application, which matters when a historical secret has been copied into deployed code.

The useful end state is not a cleaner commit graph. It is a rotated credential, a verified application, a team working from clean history and controls that stop the same mistake reaching production again.

Back to blog