Detecting and Removing Secrets Accidentally Committed to a Public Repo

May 30, 2026 7 min read 55 views

You pushed a commit, then noticed the .env file was tracked. Or a teammate spotted a hard-coded AWS key in a PR review β€” after it had already been merged to main. Either way, your first instinct is right: treat this as a live incident, not a cleanup task.

The key point most tutorials miss is that deleting the file and pushing a new commit does not remove the secret. The old commit still exists, is still publicly readable, and is already being crawled by automated scanners that harvest exposed credentials within minutes of a push.

What you'll learn

  • How to assess the blast radius immediately after discovering an exposed secret
  • How to scan a repository for secrets using open-source tools
  • How to rewrite Git history to permanently remove a file or string
  • How to force-push safely and handle forks and caches
  • How to prevent this from happening again with pre-commit hooks and CI checks

Step 1: Revoke First, Ask Questions Later

Before you touch the repository, rotate or revoke the exposed credential. This is non-negotiable. Automated bots scan GitHub continuously; by the time you read this article, the key may already have been picked up.

  • AWS keys: Go to IAM β†’ Security credentials β†’ deactivate or delete the access key, then create a new one.
  • GitHub tokens: Settings β†’ Developer settings β†’ Personal access tokens β†’ delete the token.
  • Database passwords: Log in to your DB host and change the user's password immediately.
  • API keys (Stripe, Twilio, etc.): Use that provider's dashboard to roll the key β€” most have a one-click rotate button.

Once the credential is dead, an attacker who has it gets nothing. Now you can work on history cleanup without racing against active exploitation.

Step 2: Check Whether GitHub Already Flagged It

GitHub's secret scanning runs on all public repositories automatically. If your repo is public and you pushed a credential from a supported provider (AWS, GCP, GitHub, Stripe, and many others), GitHub may have already sent you an email alert and, in some cases, may have notified the provider directly.

Check Security β†’ Secret scanning alerts in your repo's tab. If an alert is there, acknowledge it after you rotate the credential β€” this closes the alert and tells GitHub the issue is addressed. Do not close the alert before rotating; the provider integration depends on the alert state in some cases.

Step 3: Scan the Entire Repo for Other Secrets

One exposed secret is a reason to assume there might be others. Run a full scan before you start rewriting history, so you know exactly what you're dealing with.

Using Gitleaks

Gitleaks is the most widely used open-source secret scanner for Git repositories. It inspects every commit in the history, not just the current working tree.

# Install via Homebrew (macOS/Linux)
brew install gitleaks

# Or pull the binary from GitHub releases and add it to PATH

# Scan the entire commit history of the current repo
gitleaks detect --source . --log-opts="HEAD" -v

The --log-opts flag passes arguments to git log under the hood. Using HEAD walks every reachable commit. The output lists each finding with the file, commit hash, line number, and the matched secret pattern.

Using TruffleHog

TruffleHog is a complementary tool that uses both regex patterns and entropy analysis to catch secrets that don't match known patterns.

# Install
pip install truffleHog

# Scan a local repo
trufflehog git file://. --only-verified

The --only-verified flag tells TruffleHog to try calling the provider's API to confirm whether the credential is still active. This reduces noise significantly.

Run both tools and combine their output into a list of affected files and commit SHAs. You'll need this list for the next step.

Step 4: Rewrite History With git-filter-repo

The old recommended tool for this was git filter-branch. It works, but it's slow, error-prone, and the Git project itself now recommends against using it. Use git-filter-repo instead β€” it's faster, safer, and has a much cleaner API.

Install git-filter-repo

# macOS/Linux via pip
pip install git-filter-repo

# Or via Homebrew
brew install git-filter-repo

Remove a specific file from all history

If the secret lived in a dedicated file (e.g., secrets.env, config/credentials.json), remove that file entirely from every commit:

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

The --invert-paths flag means "keep everything except this path". After this command, the file will not exist in any commit in the rewritten history.

Remove a specific string from all history

Sometimes the secret is embedded in a source file that you still need. In that case, replace the literal secret value with a placeholder across all commits:

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

The replacements.txt file contains one replacement rule per line in the format literal:OLDVALUE==>NEWVALUE:

literal:AKIAIOSFODNN7EXAMPLE==>REDACTED_AWS_KEY
literal:wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY==>REDACTED_AWS_SECRET

After running this, every commit that contained that exact string will be rewritten with the replacement text instead.

Verify the rewrite worked

# Search all blobs in history for the old value
git log --all --full-history -- secrets.env

# Or use git grep across all commits
git grep "AKIAIOSFODNN7EXAMPLE" $(git rev-list --all)

If these return nothing, the secret is gone from your local history. Now you need to push this rewritten history to the remote.

Step 5: Force-Push and Clean Up the Remote

Rewriting history creates new commit SHAs. Your local branch and the remote branch are now diverged, so a normal push will be rejected. You need a force push.

# Push the rewritten main branch
git push origin main --force

# If you have other branches that contained the secret, push those too
git push origin --force --all

# Push rewritten tags if any
git push origin --force --tags

Force-pushing to a shared branch will disrupt any collaborators who have the old history checked out. Coordinate with your team: they should run git fetch --all followed by git reset --hard origin/main to align with the rewritten history. Warn them not to push any local branches that were based on the old history.

Ask GitHub to purge cached views

Even after a force push, GitHub caches commit and blob views. A direct URL to the old blob may still be accessible for a short time. Contact GitHub Support and request a cache purge for the specific repository. GitHub's documentation covers this process, and they handle it promptly for security incidents.

Notify fork owners

If your repository has forks, those forks still contain the old history. You cannot rewrite someone else's fork. You can, however, ask fork owners to re-fork from the clean history. For a widely-forked repo this is a bigger problem β€” which is another reason why rotating the credential immediately (Step 1) is so critical.

Step 6: Audit Your Current Working Tree

Once history is clean, make sure the secret isn't still present in the current file tree. Run Gitleaks in directory mode (not history mode) against the working directory:

gitleaks detect --source . --no-git -v

Also check your .gitignore to confirm the file that held the secret is now properly ignored:

echo ".env" >> .gitignore
echo "config/credentials.json" >> .gitignore
git status  # the file should now show as ignored, not tracked

Common Pitfalls

  • Thinking a delete commit is enough. It never is. The secret exists in every prior commit. You must rewrite history.
  • Forgetting branches and tags. git filter-repo rewrites all reachable commits from HEAD, but you need to explicitly push --all and --tags to update the remote references.
  • Skipping the rotation step. History cleanup is secondary. An unrotated credential is still a live vulnerability even after you clean the repo.
  • Rewriting a repo with active PRs open. Open pull requests hold references to the old commits. Close or update them after the force push, otherwise the old blobs remain reachable through the PR's diff view.
  • Using git filter-branch instead of git filter-repo. Filter-branch is significantly slower on large repos and has known edge cases with symbolic refs. Stick with filter-repo.

Preventing This From Happening Again

Pre-commit hooks with Gitleaks

Block secrets at commit time, before they ever reach the remote:

# Install pre-commit framework
pip install pre-commit

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

pre-commit install

After pre-commit install, every git commit will run Gitleaks on the staged diff. If a secret pattern is matched, the commit is blocked and the finding is printed to the terminal.

Use environment variables, not config files

Store secrets in environment variables and read them at runtime. For local development, use a .env file loaded by a library like python-dotenv or dotenv for Node β€” and make sure .env is in .gitignore from day one.

import os
from dotenv import load_dotenv

load_dotenv()  # reads .env file into environment

db_password = os.environ["DB_PASSWORD"]  # raises KeyError if missing, which is intentional

CI secret scanning

Add a secret scanning step to your CI pipeline so that even if a pre-commit hook is bypassed, the CI build fails before merging:

# GitHub Actions example
- name: Scan for secrets
  uses: gitleaks/gitleaks-action@v2
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Wrapping Up

If you're in the middle of an incident right now, here's the condensed action list:

  1. Rotate or revoke the exposed credential immediately β€” don't wait until history is clean.
  2. Check GitHub's Secret scanning alerts under your repo's Security tab.
  3. Run Gitleaks or TruffleHog over the full history to find all affected commits and files.
  4. Rewrite history with git filter-repo to remove the file or replace the secret string, then force-push all branches and tags.
  5. Contact GitHub Support to request a cache purge, and notify fork owners to re-fork from the clean history.

Once the incident is resolved, set up pre-commit hooks and a CI scanning step so future secrets never make it past your local machine. The tools are free and the setup takes less than twenty minutes.

πŸ“€ Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

πŸ“¬ Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.