You've just realized that an API key, database password, or private token was committed to your Git repository. Your heart sinks. In this guide to removing secrets from Git history: a step-by-step guide, we'll walk you through the exact process we use in production to contain the damage, fix the repository, and prevent it from happening again.
The window to act is small. If the repository is public or has been pushed to a shared platform, assume the secret is already compromised. The three-step approach that has saved us countless times: rotate credentials immediately, rewrite the Git history to remove the exposed secret entirely, then put guardrails in place so it never happens again.
Rotate first — Assume the secret is compromised
Before you touch Git history, revoke the exposed credential now. This is non-negotiable in a production environment. Whether it's an AWS access key, Stripe API token, or database password, treat it as if an attacker already has it.
Here's the reality: rewriting Git history takes time. Your CI/CD systems may have cached the secret. GitHub's API may have indexed it. A security scanner somewhere may have already logged it. The only safe assumption is that the credential is no longer safe to use.
The steps we follow:
- Identify exactly which credential was exposed and in which file(s).
- Log into the service that issued the credential (AWS IAM, Stripe dashboard, your database admin panel) and revoke it immediately.
- Generate a fresh replacement credential.
- Deploy the new credential to your running services and staging environments.
- Document what happened and when for your audit trail.
Only after the credential is rotated should you begin history rewriting. Removing the secret from Git is the cleanup step, not the containment step.
Rewrite history — git filter-repo removes files or strings from all commits
Once credentials are rotated, you need to remove the secret from Git history so it's not available to anyone who clones the repository. The tool we recommend is git filter-repo, which is more powerful and safer than the older git filter-branch.
Installing git filter-repo
git filter-repo is a separate tool, not built into Git. Install it first:
pip install git-filter-repoOn macOS with Homebrew:
brew install git-filter-repoScenario: A .env file with secrets was committed
Imagine this happened: a developer committed .env to the repository six commits ago with production database passwords. The file is in .gitignore now, but those commits still contain it.
Here's how we fix it. First, clone a fresh copy of the repository to work in (don't do this on your main checkout—filter-repo rewrites the repository):
git clone --mirror https://github.com/yourorg/yourrepo.git repo-mirror.git
cd repo-mirror.gitNow remove the .env file from all commits:
git filter-repo --invert-paths --path .envThis command rewrites every commit that touched .env, removing it completely from history. The --invert-paths flag means "keep everything except this path."
Scenario: A secret string is embedded in code
Sometimes the secret isn't in a separate file—it's hardcoded in a Python script or baked into a configuration. For example, an AWS key might be in config.py:
AWS_KEY = "AKIAIOSFODNN7EXAMPLE"Use git filter-repo with a replace expression:
git filter-repo --message-filter 'cat' --replace-text replacements.txtWhere replacements.txt contains:
AKIAIOSFODNN7EXAMPLE==>REDACTED
b'AKIAIOSFODNN7EXAMPLE'==>b'REDACTED'Create one line per occurrence of the secret, matching exact strings (including quotes and prefixes). This approach is more surgical—it preserves the file but removes the credential.
What filter-repo actually does
git filter-repo rewrites the entire commit graph. Every commit hash changes, even those that don't touch the secret file. This is because Git commits are immutable—change the contents, and the hash changes, which cascades through the tree.
When it's done, you'll see output like:
Rewrite complete!
* Processed 124 commits
* Changed 3 commits
* Deleted 0 commitsForce-push and notify — Everyone must re-clone or reset
After rewriting history, you need to push the cleaned repository back to the remote. This is a destructive operation and requires careful coordination with your team.
Push the rewritten history
Still in your mirror clone:
git push --mirror https://github.com/yourorg/yourrepo.gitThe --mirror flag pushes all branches and tags, overwriting the remote repository completely.
Notify your team
Before or immediately after pushing, send a message to all developers on the team. The notification should include:
- What secret was exposed and when it was discovered.
- That the credential has been rotated and is no longer active.
- That Git history has been rewritten and they must re-clone.
- Instructions for what to do with their local checkouts.
A message we've sent:
Action required: Repository history rewritten. A database password was accidentally committed to
mainin a .env file. We have rotated the credential immediately. The Git history has been cleaned. Please: (1) Back up any uncommitted work, (2) Delete your local clone, (3) Re-clone from the remote. Any branches with uncommitted work should be pushed to a new temporary branch before deletion, then rebased on the new main. Questions? Ask in #engineering.
What developers must do locally
If a developer has uncommitted changes, they should stash or push them to a temporary branch before re-cloning:
cd /path/to/repo
git stash # or git push origin my-work:my-work-temp
rm -rf /path/to/repo
git clone https://github.com/yourorg/yourrepo.git
If they've made commits locally that are now orphaned (because the history was rewritten), they'll need to rebase or replay those commits on top of the new main branch.
Prevent it — Secret scanning and pre-commit hooks
Now that the immediate crisis is handled, prevent this from happening again. The best defense is layered: scanning at commit time, scanning in CI/CD, and good practices.
Git pre-commit hook
A pre-commit hook runs before a commit is created. We use detect-secrets, a Python tool that scans for patterns that look like secrets:
pip install detect-secretsCreate a file at .git/hooks/pre-commit:
#!/bin/bash
detect-secrets scan --baseline .secrets.baseline
if [ $? -ne 0 ]; then
echo "Potential secrets detected. Add --force to override."
exit 1
fiMake it executable:
chmod +x .git/hooks/pre-commitThis runs on every commit. If secrets are detected, the commit fails unless the developer explicitly overrides (which they shouldn't, but we log it).
GitHub secret scanning
If you use GitHub, enable native secret scanning in repository settings under "Security & analysis." GitHub will scan for patterns from AWS, Stripe, and 200+ other providers, and alert you if they appear in commits or pull requests.
For private repositories, secret scanning is available with GitHub Advanced Security. For public repositories, it's always free.
CI/CD scanning
Add a scanning step to your CI pipeline. For example, in a GitHub Actions workflow:
- name: Scan for secrets
run: |
pip install detect-secrets
detect-secrets scan --baseline .secrets.baseline
This catches secrets that developers committed despite the local hook, and it's a checkpoint before code lands in main.
.env and .gitignore practices
Ensure every developer knows the pattern:
- Never commit
.env,.env.local,secrets.json, or any file with credentials. - Add these to
.gitignoreat the root of the repository. - Provide a template:
.env.exampleshows the structure without real values.
Document this in your README.md or onboarding guide so new team members understand the practice.
Common mistakes
- Delaying credential rotation: We've seen teams rewrite history first, then rotate credentials days later. By then, the secret may have been used by an attacker. Always rotate first.
- Using git filter-branch instead of git filter-repo: The older tool is slower and more error-prone. Unless you have a very old codebase, use filter-repo.
- Forgetting to notify the team: If developers keep working on the old history and push before pulling, you'll have conflicts and merge issues. Communication is essential.
- Rewriting public history without coordination: If the repository is public and cloned by external parties, rewriting history disrupts their workflows. For public open-source projects, consider whether history rewriting is proportional to the secret exposure.
- Not verifying the rewrite: After filter-repo, inspect a few commits to confirm the secret is gone:
git log -p | grep -i "api_key"should find nothing.
Key takeaways
- Assume any committed secret is compromised. Rotate credentials immediately before rewriting history.
git filter-repois the tool for safely removing files or strings from all commits in a repository's history.- A force-push after rewriting history requires team coordination—everyone must re-clone or rebase their local work.
- Layer prevention: use pre-commit hooks, GitHub secret scanning, and CI/CD scanning to catch secrets before they're committed.
- Document the process and make it part of your team's runbook so you can move quickly under pressure.






Comments
Loading comments…