Audit guide
How to find hardcoded API keys in your codebase
Hardcoded secrets are the most common class of credential exposure in developer projects — and AI-assisted coding has made it worse, not better. LLMs frequently suggest inline API calls with keys embedded directly in function arguments. This guide covers how to audit your source code for hardcoded credentials: a quick browser scan for individual files, grep and ripgrep patterns you can run anywhere, a gitleaks working-tree scan, and Semgrep for CI integration.
- Where secrets hide (beyond .env)
- Quick scan: LeakCheck
- grep / ripgrep patterns
- gitleaks --no-git
- Semgrep
- Found one — now what?
- Stop the next one
- FAQ
Where secrets hide beyond .env files
Most developers know to check their .env file before committing. What they miss is that secrets regularly appear in five other locations — and three of them are specific to AI-assisted development.
Quick scan: LeakCheck in the browser
LeakCheck runs 40+ credential patterns client-side — nothing is uploaded. It is the fastest way to check a specific file or config snippet without installing anything.
What to paste for a pre-ship audit:
- The main application file (
app.js,main.py,index.ts) - Any config file in the repository (
config.json,firebase.json,database.yml) - The README and any files in
docs/that contain code examples - Build output — the compiled
dist/main.jsor a chunk from.next/static/
LeakCheck does not crawl a directory — it scans whatever you paste. For a full codebase audit across hundreds of files, use the CLI tools below and reach for LeakCheck for the specific files those flag, or for quick ad-hoc checks during development.
Paste and scan with LeakCheck →
grep / ripgrep — zero install, works on any machine
grep is preinstalled on macOS and Linux. ripgrep (rg) is faster, respects .gitignore automatically, and is worth installing for any regular audit. Both work without any additional scanner tooling.
Strategy 1: search by variable name
Most hardcoded secrets appear in assignment context — a variable or config key named api_key, secret, token, or password assigned a string value. This catches them regardless of the credential’s format:
# grep (preinstalled on macOS / Linux)
grep -rn \
-e 'api[_-]\?key\s*[=:]' \
-e 'api[_-]\?secret\s*[=:]' \
-e 'auth[_-]\?token\s*[=:]' \
-e 'access[_-]\?token\s*[=:]' \
-e 'private[_-]\?key\s*[=:]' \
-e 'client[_-]\?secret\s*[=:]' \
-e 'password\s*[=:]' \
--include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" \
--include="*.py" --include="*.rb" --include="*.go" --include="*.php" \
--include="*.json" --include="*.yaml" --include="*.yml" --include="*.toml" \
. | grep -v "/node_modules/" | grep -v "/.git/"
# ripgrep (brew install ripgrep) — respects .gitignore automatically
rg -i '(api[_-]?key|api[_-]?secret|auth[_-]?token|access[_-]?token|private[_-]?key|client[_-]?secret|password)\s*[:=]' \
-t js -t ts -t py -t rb -t go -t php \
--glob '*.json' --glob '*.yaml' --glob '*.yml' --glob '*.toml' \
--glob '!node_modules/**' --glob '!.git/**'
Strategy 2: search by credential value format
These patterns match the actual shape of credentials from major providers, regardless of what variable they are assigned to. Useful for catching keys embedded as function arguments rather than named assignments:
# ripgrep — specific credential formats
rg 'AKIA[0-9A-Z]{16}' . # AWS access key ID
rg 'sk-[a-zA-Z0-9]{20,}' . # OpenAI API key
rg 'sk_(live|test)_[a-zA-Z0-9]{24,}' . # Stripe secret key
rg 'gh[pors]_[a-zA-Z0-9]{36,}' . # GitHub PATs (classic + fine-grained)
rg 'xoxb-[0-9]+-[a-zA-Z0-9]+' . # Slack bot token
rg 'AC[a-f0-9]{32}' . # Twilio Account SID
rg '-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----' . # PEM private keys
# Exclude build artifacts and dependencies
rg 'AKIA[0-9A-Z]{16}' . \
--glob '!node_modules/**' \
--glob '!.git/**' \
--glob '!dist/**' \
--glob '!.next/**' \
--glob '!build/**' \
--glob '!vendor/**'
rg 'api_?key' . | grep -vi 'your_key\|example\|placeholder\|xxx\|123456'. In ripgrep, use --glob '!docs/examples/**' to skip directories that contain only documentation examples.
Don’t skip build output
Build artifacts are where secrets from .env files most commonly surface in a form developers don’t expect. Run the same grep patterns against dist/, .next/, and build/ explicitly — ripgrep skips them if they are in .gitignore, but they are worth a direct check before any deploy:
# Scan build output directly (override .gitignore exclusion with --no-ignore)
rg 'AKIA[0-9A-Z]{16}' dist/ --no-ignore 2>/dev/null
rg 'sk-[a-zA-Z0-9]{20,}' .next/ --no-ignore 2>/dev/null
gitleaks working-tree scan (--no-git)
Most people know gitleaks for scanning git history (gitleaks detect). It also has a working-tree mode that scans the current files on disk without walking git object history — the right mode when you want to audit what is in the code right now.
Use cases for --no-git:
- Auditing a project that was never initialized as a git repo (a downloaded archive, a vendor package directory)
- Scanning only the current file state without the noise of old historical findings you already know about
- Running a scan before
git initon a new project
# Install gitleaks
brew install gitleaks # macOS
# Linux: download binary from github.com/gitleaks/gitleaks/releases
# Scan the working tree as a plain directory (no git history)
gitleaks detect --source . --no-git
# With a JSON report for further processing
gitleaks detect --source . --no-git --report-path working-tree-report.json
# Verbose: show context around each finding
gitleaks detect --source . --no-git --verbose
This is distinct from the two other gitleaks modes: gitleaks detect (walks all git commits) and gitleaks protect --staged (scans only staged files). The --no-git flag treats the directory as a plain filesystem and scans the content of every readable file.
--no-git scans every file it can read, including node_modules/ and build artifacts. Exclude large directories explicitly: gitleaks detect --source . --no-git --ignore-path node_modules --ignore-path dist. Alternatively, add a .gitleaks.toml allowlist with path exclusions.
Semgrep — SAST that understands code structure
grep and gitleaks match text patterns. Semgrep matches patterns in the abstract syntax tree — it understands that new Stripe({ apiKey: "sk_live_..." }) is a constructor call with a named argument, not just a line of text. This lets it catch credentials passed as function arguments, in multi-line assignments, and in contexts where the variable name does not suggest a secret.
Semgrep’s community p/secrets ruleset covers OpenAI, AWS, Stripe, GitHub, GitLab, Slack, Twilio, SendGrid, and dozens more:
# Install
pip install semgrep # or: brew install semgrep
# Run the community secrets ruleset
semgrep --config p/secrets .
# With JSON output for CI integration
semgrep --config p/secrets --json . > semgrep-report.json
# Exclude test fixtures and dependency directories
semgrep --config p/secrets \
--exclude 'node_modules' \
--exclude 'tests/fixtures' \
--exclude 'docs/examples' \
.
Semgrep works on any code directory regardless of whether it uses git, making it a good fit for auditing a directory before version control is initialized or integrating into a CI pipeline that needs a SAST check alongside the git-history scanner.
| Tool | Scans | Install needed | Best for |
|---|---|---|---|
| LeakCheck (browser) | Pasted text | No | Quick check of a single file or snippet |
| grep / ripgrep | Working tree | grep: no; rg: yes | Fast full-directory scan, scriptable, no external deps |
| gitleaks --no-git | Working tree | Yes | 40+ patterns, JSON report, non-git directories |
| Semgrep p/secrets | Working tree | Yes | Code-structure-aware matching; CI SAST integration |
| gitleaks detect | Full git history | Yes | Complete history audit — see companion guide |
Found a hardcoded key — what now?
Rotate it at the provider first, before touching the code. Deleting the line or replacing it with an environment-variable reference in the working tree does nothing for a credential that has already been committed to history or deployed to a server. Rotation at the provider is what actually stops the exposure.
After rotating:
- Replace the hardcoded value with a reference to an environment variable (
process.env.MY_KEY,os.getenv("MY_KEY"), etc.) - Store the new credential value in your hosting platform’s secrets UI or a local
.envfile that is in.gitignore - If the key was ever in a committed file, scan git history to confirm it is not in older commits — the working-tree audit only tells you what is in the files now
If the project has been deployed, also check whether any credentials are now publicly accessible through the live server. A secret committed to source often makes it through automated deploy pipelines into files that web servers can serve publicly — a .env in the project root, a bundled source map with inlined values, or a committed .git directory that exposes the full repository. ExposureCheck scans your live URL for exposed secrets — paste your site’s URL for a free scan.
The complete remediation sequence — provider-by-provider rotation, git history cleanup with git filter-repo and BFG, and prevention — is in the companion guide:
Full remediation: accidentally committed API key →
Stop the next one from getting in
An audit finds what is already there. Two additional layers stop new secrets from entering the codebase in the first place:
- Pre-commit hook: runs
gitleaks protect --stagedbefore each commit and rejects it if a credential pattern is found in the staged diff. See the pre-commit secret scanning guide for the three setup approaches (pre-commit framework, Husky, raw hook). - Git history scan: confirms nothing leaked in commits that predate the hook. Run
gitleaks detect --source . --log-opts="--all"against the full history. See the git repository scanning guide for the full workflow, including CI enforcement with GitHub Actions.
If you want a second opinion on any staged diff before committing, paste the output of git diff --cached directly into LeakCheck:
Scan a staged diff with LeakCheck →
Frequently asked questions
What is the difference between scanning the working tree and scanning git history?
Scanning the working tree examines the files as they exist right now on disk. Scanning git history examines every commit that ever entered the repository, including files that were deleted or overwritten in later commits. A secret you removed from a file last week is gone from the working tree but is still readable in history by anyone who runs git log -p. This guide covers working-tree audits; the git history guide covers the full history audit. If you are preparing to open-source a project, you need both.
Will grep catch secrets in minified JavaScript or bundled build output?
Yes. Bundlers like webpack and esbuild inline the value of environment variables referenced with process.env at build time. A key that was stored correctly as an env var during development can end up as a literal string in dist/main.js or .next/static/chunks/. Both grep and ripgrep scan file contents regardless of formatting — minified files are just very long lines. Run the same credential-format patterns against your dist/, .next/, and build/ directories explicitly, since ripgrep may skip them if they are in .gitignore.
Do I need to scan node_modules?
Not for an audit of your own code. node_modules contains third-party packages you do not control; any secret in there belongs to the package author, not to your project. Exclude it from every scan with --glob '!node_modules/**' in ripgrep or | grep -v node_modules in grep. If you are concerned about malicious packages embedding backdoors in dependency code, that is a supply-chain problem best addressed by dependency scanning, not a secret scan.
I’m about to open-source a project — what order should I check?
In order of importance: (1) paste your main application files and any config files into LeakCheck; (2) run a ripgrep value-pattern scan across the whole repo excluding node_modules; (3) scan the full git history with gitleaks detect — this is the step most people skip and the most common place secrets survive after being “deleted” from the working tree; (4) check your README and docs/ folder for real credentials in curl examples; (5) if you committed build output, check dist/ and .next/ for inlined env vars.
My grep search returns hundreds of results from documentation examples — how do I filter them?
Pipe through a second grep that excludes placeholder strings: rg 'api_?key' . | grep -vi 'your_key\|example\|placeholder\|xxx\|123456'. In ripgrep, use --glob '!docs/examples/**' and --glob '!tests/fixtures/**' to skip directories with documentation examples. For gitleaks and Semgrep, add a .gitleaks.toml or .semgrepignore allowlist — see the allowlist section of the pre-commit guide for the exact syntax.
How does LeakCheck compare to running ripgrep manually?
LeakCheck runs 40+ credential-specific patterns with severity ratings and masked output — zero setup, nothing installed. ripgrep gives raw file-and-line output and requires writing the correct regex for each credential type. LeakCheck is faster for checking a single file or config snippet; ripgrep scales better for a full directory tree. The two are complementary: use LeakCheck for ad-hoc checks during development and ripgrep (or gitleaks) for systematic full-codebase coverage before open-sourcing or deploying.
I found a Stripe test key (sk_test_...) — do I still need to rotate it?
Yes. Stripe test keys are valid credentials against Stripe’s test environment. A leaked test key gives an attacker access to your test data, the ability to enumerate your test customers and subscriptions, and the ability to probe your payment integration for vulnerabilities. They do not give access to live payment data, but they are not zero-risk. Rotate any hardcoded test key and store the replacement in an environment variable. If the key was ever in a public repository, assume it has been collected.
Also in the Copper Bay Labs ship-safety suite
Finding hardcoded secrets in source is one layer of a broader pre-deploy security posture. These free tools cover the rest:
Scan your live URL for exposed .env files, .git directories, source maps, and bundled secrets. Free, no signup.
Check your security headers — CSP, HSTS, X-Frame-Options, and more — so your site does not become an XSS vector.
Dependencies DepCheckScan your package.json for vulnerable, outdated, and abandoned npm packages before they reach production.