How to Scan Your Repo for Exposed Secrets
Every codebase older than a few months has a decent chance of containing at least one hardcoded credential: an AWS key pasted in for a quick test, a database password in an old config file, a Stripe token in a commit from 2023 that someone “deleted” a week later. Git never forgets, so that deletion changed nothing.
In this tutorial you will scan your repository for exposed secrets, including the parts of history nobody looks at anymore, verify which findings are real, and clean them up properly. You will finish with a repo you can actually trust and a repeatable command you can run any time.
If you want the background on why this matters so much, read exposed secrets in your repo first. This tutorial is the hands-on part.
What you’ll need
- A local clone of the repository you want to scan
- Git installed
- Homebrew, Go, or Docker (any one of them) to install the tools
- About 30 minutes
1. Install a secret scanner
We will start with gitleaks. It is fast, open source, and understands both plain files and git history. Pick whichever install method fits your machine:
# macOS
brew install gitleaks
# Go
go install github.com/gitleaks/gitleaks/v8@latest
# Docker (no install)
docker pull ghcr.io/gitleaks/gitleaks:latestConfirm it works:
gitleaks versionGitleaks ships with rules for hundreds of secret formats: AWS access keys, GitHub tokens, Slack webhooks, private keys, generic high-entropy strings, and more. You do not need to configure anything for a first pass.
2. Scan your working tree
First, check what is sitting in your files right now. From the repo root:
gitleaks detect --no-git --source . --verboseThe --no-git flag tells gitleaks to treat the directory as plain files instead of a git repo, so this covers exactly what is on disk, including files that are gitignored but still present locally (a common hiding place for .env files that later get committed by accident).
Each finding shows the rule that matched, the file, the line, and a redacted preview. A clean run exits with code 0 and prints no leaks. Do not celebrate yet, because the working tree is the easy part.
3. Scan your full git history
This is the scan that surprises people. Run gitleaks in its default git mode:
gitleaks detect --source . --verbose --report-path gitleaks-report.jsonThis walks every commit on every branch it can see and reports secrets that appeared in any version of any file, ever. The JSON report gives you commit hashes, authors, and dates, which you will need for cleanup.
If a finding looks unfamiliar, use git itself to see the full context of when the secret entered and left the codebase:
git log --all -p -S "AKIA" -- .The -S flag (pickaxe) finds commits where the number of occurrences of a string changed, so it shows you both the commit that added a secret and the commit that “removed” it. Both are still in history, which is precisely the problem.
4. Verify findings with trufflehog
Scanners produce false positives: example keys in docs, test fixtures, high-entropy strings that are just UUIDs. Before you spend an afternoon rotating credentials, find out which ones are actually live. Trufflehog can verify many credential types against the real provider APIs:
# macOS
brew install trufflehog
# Scan the local repo and only report verified, live credentials
trufflehog git file://. --only-verifiedA verified finding means trufflehog called the provider (AWS, GitHub, Stripe, and so on) and the credential authenticated successfully. Treat every verified finding as an active incident, because anyone who has ever cloned or forked your repo has that credential too.
Unverified findings still deserve a manual look. A database password cannot be verified from the outside, but it is no less dangerous.
5. Rotate every confirmed secret
Here is the rule that matters most in this entire tutorial: removing a secret from the code does not revoke it. The only real fix is rotation at the provider.
For each confirmed secret:
- Generate a replacement credential at the provider (AWS IAM, GitHub settings, Stripe dashboard, your database).
- Move the new value into a proper secret store or environment variable, never into a tracked file.
- Deploy the change.
- Revoke the old credential.
- Check the provider’s access logs for use you do not recognize.
Do rotation before history cleanup. A purged-but-unrotated key is still a working key.
6. Purge secrets from history and prevent repeats
Once everything is rotated, decide whether to rewrite history. If the repo is public or widely forked, rotation already did the important work, but purging still reduces noise in future scans. The maintained tool for this is git filter-repo:
pip install git-filter-repo
# Remove a file from all of history
git filter-repo --invert-paths --path config/secrets.ymlRewriting history changes every commit hash after the earliest affected commit, so coordinate with your team: everyone re-clones, and you force-push once.
Then make sure this is the last time you do this exercise. Add gitleaks to CI so every push gets scanned, and set up a local pre-commit hook so secrets get caught before they ever reach a commit. We cover the hook setup end to end in how to set up pre-commit secret scanning, and the broader checklist lives in our security review checklist template.
Get a second pair of eyes (free)
A secret scan is one slice of a real audit. Scanners cannot tell you that your token never expires, that your API leaks internal IDs, or that a dependency three levels deep is doing something it should not. After you have run your own scan, get a free code audit: a Webisoft engineer will manually review your codebase, no tooling shortcuts, and send you a prioritized report of what actually needs fixing. It costs nothing and takes one short call to set up.