Antoine Duno
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
- Hardcoded API keys in source code are one of the most common and costly security mistakes in software development. This guide shows you how to find them with grep and automated tools, remove them from git history, and prevent them from appearing again.
- Includes copy-paste code examples and step-by-step instructions.
- Free automated scan available to verify your implementation.
How to Find and Remove Hardcoded API Keys from Your Codebase
Finding hardcoded API keys in your codebase is not optional — it is a pre-release checklist item. GitHub''s own research found that hundreds of thousands of secrets are pushed to public repositories every year. The majority are API keys, database credentials, and OAuth tokens.
In many cases, automated bots scrape GitHub for secrets within minutes of a public push. By the time you notice the leak and rotate the key, it may already have been used.
This guide covers how to find secrets that are already in your code, remove them from git history permanently, and prevent them from entering your codebase again.
Why Hardcoded Secrets Are So Common
Developers hardcode secrets for understandable reasons: - It works immediately without any setup - Environment variables require more configuration - It is easy to forget a development key is still in the code - Copy-paste from documentation or Stack Overflow includes placeholder keys that get replaced with real ones
The result is that secrets end up in:
- .env files committed to git (the most common)
- Configuration files (config.json, settings.py)
- Source code directly (strings in function calls)
- Test files (especially integration tests against real APIs)
- Comments (base64-encoded or "for reference only")
- CI/CD configuration files
Method 1: grep with Regex Patterns
grep is available everywhere and requires no installation. These patterns catch the most common secret formats:
# Generic high-entropy strings (catches many key formats)
grep -rE ''[A-Za-z0-9+/]{40,}'' --include="*.js" --include="*.ts" --include="*.py" \\
--exclude-dir=node_modules --exclude-dir=.git .
# API key patterns
grep -rE ''(api[_-]?key|apikey|api_secret|access[_-]?token)["\\s:=]+["\\x27][A-Za-z0-9_\\-]{20,}'' \\
--include="*.{js,ts,py,rb,go,env,json,yaml,yml}" \\
--exclude-dir=node_modules --exclude-dir=.git .
# AWS keys
grep -rE ''AKIA[0-9A-Z]{16}'' --exclude-dir=node_modules --exclude-dir=.git .
# Stripe keys
grep -rE ''sk_(live|test)_[A-Za-z0-9]{24,}'' --exclude-dir=node_modules --exclude-dir=.git .
# SendGrid API keys
grep -rE ''SG\\.[A-Za-z0-9_-]{22}\\.[A-Za-z0-9_-]{43}'' --exclude-dir=node_modules .
# GitHub tokens (personal access tokens)
grep -rE ''ghp_[A-Za-z0-9]{36}'' --exclude-dir=node_modules --exclude-dir=.git .
grep -rE ''github[_-]?token["\\s:=]+["\\x27][A-Za-z0-9_]{40}'' \\
--exclude-dir=node_modules --exclude-dir=.git .
# Google API keys
grep -rE ''AIza[0-9A-Za-z\\-_]{35}'' --exclude-dir=node_modules --exclude-dir=.git .
# Slack tokens
grep -rE ''xox[baprs]-[0-9A-Za-z]{10,}'' --exclude-dir=node_modules --exclude-dir=.git .
# Twilio
grep -rE ''AC[a-z0-9]{32}'' --exclude-dir=node_modules --exclude-dir=.git .
# Database connection strings with credentials
grep -rE ''(postgres|mysql|mongodb)://[^:]+:[^@]+@'' \\
--exclude-dir=node_modules --exclude-dir=.git .
# Private key headers (SSH, TLS)
grep -rl ''BEGIN RSA PRIVATE KEY\\|BEGIN EC PRIVATE KEY\\|BEGIN OPENSSH PRIVATE KEY'' \\
--exclude-dir=node_modules --exclude-dir=.git .Save these as a shell script (check-secrets.sh) and run it before every commit.
Method 2: Search git History
The most important place to check is not the current working tree — it is your entire commit history. A secret removed from the current codebase might still be in a commit from six months ago.
# Search all commits for a specific pattern
git log -p | grep -E ''AKIA[0-9A-Z]{16}''
# Search all commits for common secret keywords
git log -p --all | grep -iE ''(password|secret|api_key|token|private_key)\\s*=\\s*["\\x27][^"\\x27]{8,}''
# Show all commits that touched a specific file
git log --all --full-history -- "**/.env*"
# Show the content of a specific commit
git show <commit-hash>
# List all .env files ever committed
git log --all --name-only --format="" | sort -u | grep -E ''\\.env''If you find a secret in git history, it must be treated as compromised and rotated immediately — even if you are about to remove it from history.
Method 3: truffleHog
truffleHog is purpose-built for secret detection and uses both regex and entropy analysis to find secrets with fewer false positives than raw grep.
# Install
pip install trufflehog3
# Or use Docker (no installation)
docker run --rm -v "$PWD:/pwd" trufflesecurity/trufflehog:latest \\
filesystem --directory=/pwd
# Scan a git repository including history
docker run --rm trufflesecurity/trufflehog:latest \\
git file:///path/to/your/repo
# Scan a GitHub repository directly
docker run --rm trufflesecurity/trufflehog:latest \\
github --repo https://github.com/yourorg/yourrepo
# Scan a specific git ref
trufflehog git file://. --since-commit HEAD~10 --only-verifiedThe --only-verified flag attempts to verify that detected secrets are actually valid by calling the relevant API. This dramatically reduces false positives.
truffleHog output
Found verified result 🐷🔑
Detector Type: Stripe
Decoder Type: PLAIN
Raw result: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Commit: a3b4c5d6e7f8...
File: src/payment/stripe.js
Line: 42Method 4: ZeriFlow Advanced Scan
ZeriFlow''s Advanced Scan accepts a GitHub repository URL or a ZIP upload of your codebase and checks for:
- Hardcoded secrets using pattern matching and entropy analysis
- CVEs in your dependencies
- Insecure API patterns (open debug endpoints, missing authentication middleware)
This is the fastest way to get a comprehensive secret scan alongside a dependency and API pattern audit in a single report, without setting up CLI tools locally.
How to Remove Secrets from git History
Once you have found a secret in your git history, removing it from the working tree is not enough. The secret is still in every past commit. Anyone who clones your repository can find it.
The only correct response is: 1. Rotate the secret immediately (before cleaning history) 2. Clean the git history 3. Force-push the rewritten history
Using BFG Repo Cleaner (recommended)
BFG is faster and simpler than git-filter-repo for the common case of removing specific strings or files.
# Install BFG
brew install bfg # macOS
# Or download from rtyley.github.io/bfg-repo-cleaner/
# Clone a fresh mirror of your repo
git clone --mirror git@github.com:yourorg/yourrepo.git yourrepo.git
# Create a file listing strings to delete
echo ''sk_live_actual_secret_value_here'' > secrets.txt
echo ''AKIA_actual_aws_key_here'' >> secrets.txt
# Run BFG — replaces all instances of the strings in all commits
bfg --replace-text secrets.txt yourrepo.git
# Navigate into the repo and run gc
cd yourrepo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# Force push the rewritten history
git pushWarning: Rewriting git history is destructive. Everyone who has cloned or forked the repository must re-clone after you force-push. Coordinate with your team before doing this on a shared repository.
Removing an entire file from history
# Remove a specific file (.env, config/secrets.json) from all commits
bfg --delete-files .env yourrepo.git
bfg --delete-files "*.pem" yourrepo.gitUsing git-filter-repo (alternative)
pip install git-filter-repo
# Remove a file from all history
git filter-repo --path .env --invert-paths
# Replace a specific string across all history
git filter-repo --replace-text secrets.txtRotating Compromised Credentials
After any secret exposure, assume it has been used by an attacker. Rotation order:
Stripe
- 1Dashboard → Developers → API Keys → Roll key
- 2Update all production and staging environments
- 3Verify webhooks are using the new key
AWS
- 1IAM → Users → Security credentials → Deactivate old key → Create new key
- 2Check CloudTrail for any suspicious activity using the old key
- 3Update all services, update ~/.aws/credentials, rotate any infrastructure automation
GitHub Personal Access Token
- 1Settings → Developer Settings → Personal Access Tokens → Delete old token
- 2Generate new token with minimum required scopes
- 3Check for any unauthorized repository access or workflow triggers
Database credentials
- 1Update the database user password
- 2Update all connection strings in all environments
- 3Invalidate any existing connection pools (restart services)
- 4Check database audit logs for unauthorized queries
General checklist after key rotation
- [ ] Old key is revoked or deleted (not just rotated)
- [ ] New key is stored only in environment variables or a secrets manager
- [ ] All environments (production, staging, CI/CD) are updated
- [ ] Relevant audit logs have been reviewed for unauthorized use
- [ ] The team has been notified
Preventing Future Secret Leaks
Pre-commit hooks with detect-secrets
pip install detect-secrets
# Scan your current codebase and create a baseline
detect-secrets scan > .secrets.baseline
# Set up the pre-commit hook
cat > .git/hooks/pre-commit << ''EOF''
#!/bin/sh
detect-secrets-hook --baseline .secrets.baseline
EOF
chmod +x .git/hooks/pre-commitGitHub secret scanning
Enable GitHub''s built-in secret scanning in your repository settings. It automatically scans pushes for over 200 secret patterns from major service providers and notifies you (and sometimes the service provider) immediately.
For public repositories, this is enabled automatically. For private repositories, it requires GitHub Advanced Security (included in GitHub Team and Enterprise plans).
Environment variable patterns
The correct pattern for every secret:
// .env file (never commit this)
STRIPE_SECRET_KEY=sk_live_...
DATABASE_URL=postgres://user:pass@host:5432/db
// In code
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Validate at startup — fail fast if env vars are missing
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error("STRIPE_SECRET_KEY environment variable is required");
}Add .env to .gitignore immediately when starting any project:
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo ".env.production" >> .gitignore
echo ".env.*.local" >> .gitignoreUse a .env.example file with placeholder values to communicate required environment variables without exposing real ones:
# .env.example — commit this
STRIPE_SECRET_KEY=sk_live_your_stripe_key_here
DATABASE_URL=postgres://user:password@localhost:5432/mydb
SENDGRID_API_KEY=SG.your_sendgrid_key_hereSummary
Finding and removing hardcoded API keys requires searching both your current working tree (grep, truffleHog) and your entire git history (git log -p). Any secret found in git history must be rotated immediately before rewriting history with BFG Repo Cleaner. Prevent future leaks with pre-commit hooks (detect-secrets), GitHub secret scanning, and always using environment variables for sensitive values. For a comprehensive automated scan of your repository including dependency CVEs and insecure API patterns, run an advanced scan at zeriflow.com.