Hardcoded API keys in source code are one of the most common security mistakes in Node.js projects. Not .env files committed by accident (though that happens too). I mean a Stripe key pasted into a test fixture, a GitHub token left in a debug script, or a PostgreSQL URL sitting in a migration comment.
Secret scanning in GitHub Actions catches these before merge. This guide covers a local scan, a full workflow file you can copy, how it compares to gitleaks and trufflehog, and what to do when you actually find a leaked key.
Takes about 10 minutes to set up.
TL;DR
| Goal | What to do |
|---|---|
| Scan locally before commit | npx secretguard . |
| Block merges with live keys | Add workflow below to GitHub Actions |
| Scan git history for old leaks | Use gitleaks or trufflehog |
| Stop pushes at GitHub boundary | Enable push protection (free on public repos) |
| Avoid noise from test fixtures | Use a scanner that skips fake emails in *.test.ts
|
Why hardcoded secrets in source code are still a problem
.gitignore protects .env. It does not protect src/config.ts.
Common leak locations in Node.js repos:
- Test files with copied production tokens
- Debug scripts committed to
scripts/ - Database URLs in ORM config or seed files
- Sample API responses checked into
fixtures/ - Commented-out credentials "just for reference"
GitHub secret scanning and push protection help at the remote. But you often discover the problem when git push fails at 6pm before a release. Scanning in CI on every pull request shifts that feedback to review time.
What to scan for
Credentials (rotate immediately if found)
- Cloud keys:
AKIA...(AWS),sk_live_...(Stripe),ghp_...(GitHub) - Database URLs with embedded passwords (
postgres://user:pass@...) - Private keys: RSA, OpenSSH, PGP blocks
- JWTs and high-entropy strings assigned to
api_key,API_KEY,secret
PII in source (compliance risk)
- Emails, phone numbers, SSNs, credit card numbers in non-test files
- Public IPs in config (private ranges like
192.168.xare usually fine to ignore)
.env must stay out of git. Add .env to .gitignore and commit only .env.example with placeholder values.
Scan your Node.js repo locally
No install. Works on any machine with Node 18+.
npx secretguard .
This scans the working tree (current files on disk), not git history. It skips node_modules, .git, dist, build, and binary files automatically. Findings are grouped by severity. Values are masked in output.
Useful variants:
# Scan only application source
npx secretguard ./src
# JSON for scripts or CI artifacts
npx secretguard . --json
# HTML report for security review
npx secretguard . --output report.html
# Extra ignore paths
npx secretguard . --ignore legacy --ignore sandbox
Exit codes: 0 = no CRITICAL findings, 1 = at least one CRITICAL. HIGH and MEDIUM are reported but do not fail by default. That is a practical default: a live Stripe key should block a merge; a JWT-shaped string in a test helper might be worth reviewing without stopping the pipeline.
Secret scanning in GitHub Actions
Below is a complete workflow file. Copy it to .github/workflows/secret-scan.yml.
name: Secret Scan
on:
pull_request:
branches: [main, master]
push:
branches: [main, master]
permissions:
contents: read
jobs:
secret-scan:
name: Scan for hardcoded secrets
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Scan source for secrets and PII
run: npx secretguard . --json > secretguard-report.json
continue-on-error: true
id: scan
- name: Upload scan report
if: always()
uses: actions/upload-artifact@v4
with:
name: secretguard-report
path: secretguard-report.json
- name: Fail on CRITICAL findings
if: steps.scan.outcome == 'failure'
run: |
echo "CRITICAL secrets or PII found. Review the scan report artifact."
exit 1
Why a separate workflow: Keeps security checks visible in the Actions tab. Teams can require this check in branch protection without coupling it to your test job.
Minimal one-liner if you already have a CI workflow:
- name: Scan for hardcoded secrets
run: npx secretguard .
No API key or config file required. npx pulls the latest published version from npm.
Avoiding false positives in test files
Scanners that flag every email and phone number fail on real codebases. Test files contain user@example.com and 555-123-4567. That is fixture data, not a production leak.
Three approaches teams use:
| Approach | Pros | Cons |
|---|---|---|
| Ignore all test directories | Zero noise | Misses real tokens copied into tests |
| Filter placeholder values | Works in prod files | Misses realistic fake data |
| Credentials everywhere, PII only in prod paths | Balanced | Slightly more complex |
secretguard uses the third approach. PII patterns are skipped in *.test.*, *.spec.*, __tests__/, fixtures/, and mocks/. Placeholders like example.com and US 555- numbers are filtered elsewhere. Credentials are always scanned, including in test files, because a real ghp_ token in a test is still a real leak.
gitleaks vs trufflehog vs a Node.js CI scanner
| Tool | Scans git history | Scans working tree | Node.js native | Best for |
|---|---|---|---|---|
| gitleaks | Yes | Yes (detect --no-git) |
No (Go binary) | Finding secrets in past commits |
| trufflehog | Yes | Yes | No | Deep entropy + verified secrets |
| GitHub secret scanning | On push | No | N/A | Platform-level detection |
| secretguard | No | Yes | Yes | Fast PR check, credentials + PII |
Practical combo many teams use:
- gitleaks or trufflehog in CI weekly (or on main) for git history
- secretguard on every PR for the current diff's files on disk
- GitHub push protection enabled as a last line of defense
Example gitleaks step for comparison:
- name: Gitleaks scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
None of these replace a secrets manager (Vault, AWS Secrets Manager, Doppler) or proper .gitignore hygiene.
Enable GitHub push protection (free for public repos)
GitHub can block pushes that contain known secret patterns before they enter the repo.
- Go to Repository → Settings → Code security and analysis
- Enable Secret scanning (public repos: free)
- Enable Push protection
This is reactive at push time. CI scanning on pull requests is proactive at review time. Use both.
Official docs: Working with push protection
What to do when you find a leaked API key
Finding a secret is not enough. You have to assume it is compromised.
Step 1: Remove from code immediately
# Find the file and line from scan output, edit, commit
git add -A && git commit -m "Remove hardcoded API key"
Step 2: Rotate the credential
- Stripe: roll the key in Dashboard → Developers → API keys
- GitHub: revoke the PAT under Settings → Developer settings
- AWS: deactivate the access key in IAM
- Database: change the password and update your secrets manager
Step 3: Check git history
If the key was ever committed, removing it from HEAD is not enough. It still exists in old commits. Use gitleaks or git log -S 'sk_live_' to find when it was introduced. You may need git filter-repo or GitHub's secret scanning alert workflow for serious incidents.
Step 4: Enable prevention
- Add secret scanning to CI (workflow above)
- Add a pre-commit hook locally
- Never use production keys in test fixtures; use env vars or mock values
Step 5: Document for your team
Log the incident (without the secret value). Note which key was rotated and when.
Pre-commit secret scan (optional)
Catch leaks before they leave your laptop:
#!/bin/sh
# .git/hooks/pre-commit
npx secretguard . || exit 1
chmod +x .git/hooks/pre-commit
Slower feedback than CI but prevents the push entirely. For teams, consider Husky to share hooks via the repo.
Example scan output
CRITICAL (2)
src/config.ts:14 Stripe Live Secret Key sk_live_****7890
src/db.ts:8 Database URL (PostgreSQL) postgres://****@host/db
HIGH (1)
src/auth.ts:22 JWT Token eyJhbG****In0
Add a screenshot of your own scan before publishing. Use alt text: secretguard terminal output showing masked API key findings.
Programmatic scan in Node.js
Embed scanning in custom tooling:
import { scan, credentialPatterns } from 'secretguard'
const result = await scan('./src', {
patterns: [...credentialPatterns],
})
const critical = result.findings.filter((f) => f.severity === 'CRITICAL')
if (critical.length > 0) {
console.error(`Found ${critical.length} critical issues`)
process.exit(1)
}
FAQ
How do I detect hardcoded API keys in a Node.js project?
Run a secret scanner against your source tree: npx secretguard . locally, or add a GitHub Actions step that runs the same command on every pull request. Complement with gitleaks for git history if keys may exist in old commits.
What is the best secret scanning tool for GitHub Actions?
Depends on scope. gitleaks and trufflehog are strong for git history. For a lightweight check of current files without installing Go/Python tooling, a Node-native CLI like secretguard fits standard JavaScript CI images. Enable GitHub push protection regardless of which scanner you pick.
Does secret scanning replace .gitignore?
No. .gitignore prevents committing .env. Secret scanning catches credentials written directly in .ts, .js, .json, and test files that are supposed to be in the repo.
Should I scan test files?
Yes for credentials. A real GitHub token in auth.test.ts is a real leak. PII scanners should skip or filter test fixtures to avoid false positives from user@example.com.
What exit code should fail CI?
Fail on CRITICAL (live API keys, private keys, database passwords). Report HIGH and MEDIUM without blocking until your team agrees on policy.
Checklist before you merge
- [ ] Run
npx secretguard .locally once - [ ] Add
.github/workflows/secret-scan.yml - [ ] Enable GitHub push protection
- [ ] Fix CRITICAL findings and rotate any exposed keys
- [ ] Confirm test fixtures use mocks, not production credentials
- [ ] Add terminal screenshot to this post before publishing (if on own blog)
Links
- GitHub: github.com/chintanshah35/secretguard
- npm: npmjs.com/package/secretguard
- GitHub secret scanning docs: docs.github.com/code-security/secret-scanning
If you run secret scanning in a monorepo or use gitleaks alongside a Node scanner, share your setup in the comments.
Top comments (0)