DEV Community

Chintan Shah
Chintan Shah

Posted on

How to Scan for Hardcoded Secrets in a Node.js Project (GitHub Actions Guide)

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.x are 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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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:

  1. gitleaks or trufflehog in CI weekly (or on main) for git history
  2. secretguard on every PR for the current diff's files on disk
  3. 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 }}
Enter fullscreen mode Exit fullscreen mode

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.

  1. Go to Repository → Settings → Code security and analysis
  2. Enable Secret scanning (public repos: free)
  3. 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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
chmod +x .git/hooks/pre-commit
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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

If you run secret scanning in a monorepo or use gitleaks alongside a Node scanner, share your setup in the comments.

Top comments (0)