At some point during a build, almost every developer does the same thing: pastes an API key directly into the source code to test something quickly, fully intending to move it to an environment variable before committing. Then they get distracted, or it works and they move on, and the key ends up in the commit history. From that point on, even if you delete it from the file tomorrow, it is permanently accessible to anyone who can clone or view the repository.
The consequences are not hypothetical. Exposed AWS credentials have generated six-figure bills within hours of being pushed to a public repo. Leaked database connection strings have resulted in full data dumps of user tables. Compromised Stripe keys have led to fraudulent charges that the developer was liable for. These are not rare edge cases. They happen regularly and the repos involved are usually owned by developers who were building something legitimate and simply moved too fast.
The good news is that secrets in repos are findable. The bad news is that attackers are looking for them faster than most developers are. This article covers how to scan your repo, what to look for, and what to do if you find something that should not be there.
Why This Happens More Than You Think
Secrets end up in repositories through a handful of predictable patterns. The most common is the testing shortcut: a developer is working locally, needs to make a real API call, and pastes the key directly into the code to unblock themselves. The plan is always to clean it up before committing. The cleanup gets forgotten.
A second pattern is the accidental .env commit. A developer creates a .env file, populates it with real credentials, and at some point runs git add . without checking what is being staged. The .gitignore entry for .env was never added, or it was added to the wrong location, and the file gets committed. This one is particularly dangerous because.env files tend to contain every secret the project uses, all in one place.
A third pattern is the public fork problem. A developer works on a private repo, forks it to contribute to something publicly, and does not realize that the fork carries the full commit history, including the commit three months ago where the Stripe key was briefly hardcoded. The repo is now public and the history is searchable.
GitHub indexes public repositories in real time. Automated bots scan for secrets continuously and will find an exposed key within minutes of a push. By the time you notice and rotate the key, it may already have been used.
What Counts as a Secret
The category is broader than most developers realize. The obvious ones are API keys, but the definition extends to anything that grants access to a system or service you control.
- API keys -- strings like
sk-proj-abc123...(OpenAI),sk_live_4eC39...(Stripe),AKIA...(AWS access key ID) - Database connection strings -- full URIs like
postgresql://user:password@host:5432/dbnameormongodb+srv://user:pass@cluster.mongodb.net - Private keys and certificates -- PEM blocks starting with
-----BEGIN RSA PRIVATE KEY-----or-----BEGIN EC PRIVATE KEY----- - OAuth tokens -- GitHub personal access tokens (
ghp_...), Google OAuth client secrets, Facebook app secrets - Webhook secrets -- shared secrets used to verify incoming webhook payloads from Stripe, GitHub, Twilio, and similar services
- Firebase credentials -- the full config object including
apiKey,authDomain, andstorageBucketvalues from the Firebase console - Service account JSON files -- Google Cloud and Firebase service account files that contain a private key inline
If a string or file would let someone authenticate to a service as you, or read and write data that belongs to your users, it is a secret. Treat it accordingly.
How to Scan Your Repo for Exposed Secrets
GitHub's Native Secret Scanning
GitHub has built-in secret scanning that automatically detects secrets from over 200 service providers in public repositories. For private repositories, it is available on GitHub Advanced Security, which requires a paid plan. When a secret is detected, GitHub notifies the repository owner and, in some cases, notifies the affected service provider directly so they can revoke the credential automatically.
To check your settings, go to your repository on GitHub, open Settings, scroll to Security, and look for Secret scanning under the Code security section. Enable it if it is not already on. You can also enable push protection, which blocks commits containing known secret patterns before they are accepted by GitHub.
The limitation of GitHub's scanner is that it only covers patterns it has been trained to recognize. Custom secrets, internal tokens, and secrets with non-standard formats will not be flagged. It also does not scan the full git history by default on repositories where scanning was enabled after the secrets were committed.
git-secrets and TruffleHog
For deeper scanning, two CLI tools are widely used: git-secrets from AWS Labs and trufflehog from Truffle Security. TruffleHog is the more capable of the two: it scans the entire git history, not just the current working tree, and uses both regex patterns and entropy analysis to catch secrets that do not match a known format.
# install trufflehog
brew install trufflehog
# scan a local repo
trufflehog git file://. --only-verified
# scan a GitHub repo directly
trufflehog github --repo=https://github.com/your-org/your-repoThe --only-verified flag tells TruffleHog to only report secrets it can confirm are active by making a test API call. This cuts down on false positives significantly. git-secrets is lighter weight and integrates well as a pre-commit hook to prevent new secrets from being committed in the first place.
# install git-secrets (macOS)
brew install git-secrets
# set it up in a repo
cd your-repo
git secrets --install
git secrets --register-aws # adds AWS key patterns
# scan the current repo
git secrets --scan-historySearching Your Git History Manually
If you want to check for specific patterns without installing additional tools, you can search the full git history directly from the command line. This is useful for targeted lookups when you suspect a particular secret may have been committed.
# search all commit diffs for common secret patterns
git log -p | grep -iE "api_key|api_secret|secret_key|access_token|password"
# search for specific key prefixes
git log -p | grep -E "sk_live_|sk_test_|AKIA[0-9A-Z]{16}"
# find all commits that touched .env files
git log --all --full-history -- "**/.env"
# show the full content of those commits
git log --all --full-history -p -- "**/.env"
# search for anything that looks like a private key
git log -p | grep "BEGIN.*PRIVATE KEY"Pipe the output to a file if you are scanning a large history so you can review it without losing context. For a thorough scan across all branches and all commits, add the --all flag to git log.
Automated Repo Scanning
For a full-repo scan that covers secrets alongside other security issues, tools like GitDoctor scan your GitHub repository and surface exposed credentials, hardcoded tokens, and insecure patterns with a severity rating and a ready-to-use fix for each finding.
What to Do If You Find One
The order of operations matters here. Do not start by editing the file or rewriting history. Start by revoking the secret.
- Revoke and rotate the key immediately. Go to the service dashboard (AWS IAM, Stripe, OpenAI, etc.) and invalidate the exposed credential before doing anything else. Assume it has already been seen. Generate a new key and store it securely.
- Remove it from the code. Delete the hardcoded value from the source file and replace it with an environment variable reference. This is the easy part.
- Scrub the git history. Deleting the value from the current file does not remove it from past commits. Use BFG Repo Cleaner (faster and simpler than
git filter-branch) to remove the secret from the entire history.
# using BFG Repo Cleaner
# first, download bfg.jar from rtyley.github.io/bfg-repo-cleaner
# clone a fresh mirror of your repo
git clone --mirror git@github.com:your-org/your-repo.git
# create a file listing the secrets to remove
echo 'sk_live_4eC39HqLyjWDarjtT7en' > secrets.txt
# run BFG
java -jar bfg.jar --replace-text secrets.txt your-repo.git
# clean up and force push
cd your-repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force- Force push to update all remotes. After rewriting history, you need to force push to replace the remote history. Coordinate with any collaborators who have cloned the repo, as their local histories will diverge and they will need to re-clone.
- Audit for unauthorized usage.Check the service's usage logs for the period the key was exposed. Look for API calls you did not make, unusual geographic locations, or unexpected resource creation. Most services retain access logs and can tell you exactly what was done with a credential.
How to Prevent It Going Forward
Prevention is simpler than remediation. These habits eliminate most of the risk before it materializes.
- Add
.env*to your.gitignorebefore writing your first line of code. Do this in every project, every time. Keep a.env.examplefile with placeholder values committed so collaborators know what variables are needed. - Install a pre-commit hook. Tools like
git-secretsordetect-secretsfrom Yelp can block commits containing secret patterns before they are written to history. Set them up once per machine or enforce them project-wide with a.pre-commit-config.yaml. - Never hardcode a secret, even temporarily. If you need to test with a real credential, put it in a local
.envfile that is already gitignored. The habit of using environment variables from the start costs thirty seconds and eliminates the entire class of accidental commits. - Use a secret manager for production credentials. AWS Secrets Manager, Doppler, HashiCorp Vault, and similar tools centralize credential management and give you rotation, access logging, and fine-grained permissions. They also mean that no developer on your team ever needs to see the raw production secret.
- Rotate credentials on a schedule. Even credentials that have not been exposed should be rotated periodically. A key that is ninety days old and has never touched a commit is still lower risk than one that is two years old with an unknown history.
Make Scanning Part of Your Regular Workflow
Scanning your repo once is useful. The more durable practice is running a scan before every significant release, every time a new contributor joins the project, and every time you make a repository public that was previously private. Secrets accumulate in repos over time, often introduced by contributors who did not know the conventions, and a scan that comes back clean today does not guarantee a clean scan six months from now. Build the check into your workflow and it stops being something you have to remember and starts being something that happens automatically.