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.
Anay Pandya
1,600 words
Anay Pandya
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
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.
Is your site actually secure?
Run a free check — 60 seconds
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.
Keep reading
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
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.
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.
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.
Found verified result 🐷🔑
Detector Type: Stripe
Decoder Type: PLAIN
Raw result: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Commit: a3b4c5d6e7f8...
File: src/payment/stripe.js
Line: 42ZeriFlow''s Advanced Scan accepts a GitHub repository URL or a ZIP upload of your codebase and checks for:
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.
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
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.
# Remove a specific file (.env, config/secrets.json) from all commits
bfg --delete-files .env yourrepo.git
bfg --delete-files "*.pem" yourrepo.gitpip 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.txtAfter any secret exposure, assume it has been used by an attacker. Rotation order:
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-commitEnable 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).
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_hereFinding 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.
Related resources
Website Vulnerability Scanner
Run a broader website security audit across headers, TLS, DNS, cookies, SEO, and disclosure checks.
Security Headers Checker
Check CSP, HSTS, X-Frame-Options, and other response headers.
SSL Checker
Review TLS certificate, HTTPS, and transport security signals.
DMARC Checker
Validate email authentication records for domain spoofing protection.
CSP Checker
Review Content-Security-Policy coverage and common gaps.
April 30, 2026
Best SAST Tools for Small Development Teams in 2026 (Free Included)
Monitor your security score
Schedule recurring scans and catch regressions after deployments.