DEV Community

ke jia
ke jia

Posted on

I Ran 4 Secret Scanners on the Same File. The Smallest One Found the Least — and I Still Use It Daily

Last month I was about to push a branch when a coworker asked a two-word question: "Secrets in there?"

I had added environment files for a local dev database, and I knew the answer was probably "yes" — but I didn't know which keys, on which lines, or whether anything had already made it into git history. I wanted an answer in seconds, not after a ten-minute install.

So I built a small test: one file, secrets.env, with seven planted secrets — all fake, using AWS's own documentation example values and test-format placeholders. Then I ran four tools on the exact same file:

  1. dotguard — a zero-dependency Node CLI I maintain
  2. gitleaks — the de-facto open-source standard
  3. TruffleHog — the "verified secrets" scanner
  4. GitHub's built-in secret scanning — the platform-level safety net

Here's what happened. Including one dotguard gotcha I didn't expect and now want you to know about before you rely on it.

The test file

# secrets.env — every value below is fake or a published example
NODE_ENV=development

# Cloud
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

# LLM
OPENAI_API_KEY=sk-proj-abc123def456ghi789jkl012

# Payments
STRIPE_SECRET_KEY=sk_test_4eC39HqLyjWDarjtT1zdp7dc

# Database
DB_PASSWORD=Sup3rS3cret!2026
DATABASE_URL=postgres://admin:hunter2@db.internal:5432/app

# Misc
SESSION_TOKEN=abcdef1234567890abcdef1234567890
Enter fullscreen mode Exit fullscreen mode

Seven distinct secret types: an AWS key pair, an OpenAI API key, a Stripe secret key, a hardcoded password, a credential-bearing database URL, and a long session token. A good scanner should find most of them. Let's see how each one did.

Contender 1: dotguard (zero dependencies, ~1 second)

dotguard is a single-file Node script. No Go binary, no Docker image, no config file to learn. If you have Node, you can run it:

npx @wuchunjie/dotguard .
Enter fullscreen mode Exit fullscreen mode

I ran it on the test directory. Here is the real, unedited output from my machine:

🔍  Scanning: C:\tmp\dgtest

  📄  secrets.env (5 issues)
    ⚠️  L 15 | Hardcoded password
       DB_PASSWORD=Sup3rS3cret!2026
    ⚠️  L  9 | API key
       OPENAI_API_KEY=sk-proj-***
    ⚠️  L 19 | Access token
       SESSION_TOKEN=abcdef...7890
    ⚠️  L 16 | Database URL
       DATABASE_URL=postgres://admin:***@db.internal:5432/app
    ℹ️  L  0 | Missing PORT
       Consider adding: PORT=3000

  ─────────────────────────────
  ⚠️   4 potential secrets exposed!
  💡  Add .env to .gitignore & use .env.example instead.
Enter fullscreen mode Exit fullscreen mode

Score: 4 of 7. It caught the password, the OpenAI key, the session token, and the database URL. It exited with code 1, which is exactly what you want in CI — a failing pipeline beats a committed secret.

What did it miss? The AWS key pair and the Stripe key. The reason is instructive: dotguard's seven patterns are name-based. It looks for things like api_key=, token=, password=, private key headers, and database URL schemes. The AWS and Stripe values in my test file live under variable names (AWS_ACCESS_KEY_ID, STRIPE_SECRET_KEY) that none of the patterns match, so the values — even though they're textbook key formats — sail through.

That's a real limitation, and I'd rather state it plainly than pretend otherwise. dotguard's rule set is tuned for "classic env-file mistakes," not for the full zoo of provider key formats.

It also printed something none of the other three tools do: a hint that PORT is missing from the file. That's opinionated. On a new project it's genuinely helpful; on a mature one it's noise you can ignore.

The gotcha I want you to know about

While writing this I noticed something in the source that my first test run confirmed: dotguard v1.0.2 skips dotfiles entirely. My test directory also contained a file named .env with a planted secret, and it was not scanned at all. The scanner walks directories and skips any entry starting with ., which means:

  • secrets.env, prod.env, env.staging, env.localscanned
  • .env, .env.local, .env.productionskipped ⚠️

If your project's environment file is literally named .env (which is the convention, and what I use myself), dotguard will report "No .env files found. Clean!" and exit 0 — a false all-clear. The fix for now is to make sure the files you want checked use the *.env or env.* naming, or to treat dotguard as a scanner for your committed environment templates rather than your local .env. I've filed this as a fix for the next release, but until then: don't let a green dotguard run make you feel safe about a file named .env.

Contender 2: gitleaks (the deep scan)

gitleaks is the tool I'd point most teams to, and it shows why. It's a Go binary (or a Homebrew/Chocolatey install), and it brings something dotguard structurally can't: it scans git history by default.

That matters more than people expect. The most dangerous secret in a repo is usually not the one you're about to commit — it's the one someone committed fourteen months ago, rotated in their head, and forgot about. gitleaks walks every commit looking for them.

Its default rule set is two orders of magnitude larger than dotguard's, with dedicated rules for AWS access tokens, Stripe keys, OpenAI keys, generic passwords, and credential-bearing connection strings. On the same test file, gitleaks flags all seven lines — the AWS pair, the OpenAI key, and the Stripe key that dotguard missed — and does it in a couple of seconds.

You can point it at a report format for CI dashboards:

gitleaks detect --no-git -r gitleaks-report.json
Enter fullscreen mode Exit fullscreen mode

The trade-offs: you install a second runtime (Go binary), you'll eventually want to maintain a .gitleaks.toml with allowlists to tame false positives, and its default config will flag your own test fixtures, which is annoying in a repo that contains examples. None of that is fatal — teams run gitleaks in CI daily — but it is a heavier commitment than npx-ing a script.

Score on the test file: 7 of 7, plus the unique ability to find secrets in history.

Contender 3: TruffleHog (the verifier)

TruffleHog takes a different philosophical position: a pattern match is not proof. A 40-character string that looks like an AWS secret access key might be a placeholder from a tutorial. TruffleHog tries to verify candidates by actually calling the provider's APIs and checking whether the credential works.

That's the killer feature in a specific scenario: you find 30 candidate secrets in a leaked repo and need to know which five are live. TruffleHog is the only one of the four that will tell you "this one is real."

It's also the heaviest option here. It's a Go tool (or Docker image), verification means network calls with all the rate-limit and etiquette implications that carries, and a full scan with verification takes a long time. On my test file it would have detected all seven candidates, then tried to verify them — and, because every value is fake, the verification step would have failed, correctly telling me "detected but not valid." That's actually a lovely teaching moment: verification is how you separate noise from a real incident.

For a pre-commit check, though, TruffleHog is a sledgehammer. I use it for incident response, not for daily development.

Score: 7 of 7 detected (verification correctly rejects all fakes).

Contender 4: GitHub secret scanning (the safety net)

If your code lives on GitHub, you already have a secret scanner. Built-in patterns for AWS, OpenAI, Stripe, and dozens of other providers run automatically on pushes to public repositories at no cost; for private repositories it's included with GitHub Pro and above, and push protection can block the push that contains the secret.

Its strengths are obvious: zero CLI, zero config, and it runs whether or not your developer remembers to run anything. Its weaknesses are equally obvious: it only sees what reaches GitHub (your local working tree is invisible to it), it's platform-locked, and its built-in patterns won't catch a generic DB_PASSWORD=... line unless you add a custom pattern.

GitLab and Bitbucket ship comparable features, so the "platform safety net" idea applies beyond GitHub — it just has a different name.

Score: 7 of 7 for the provider keys (the generic password line needs a custom pattern).

The scoreboard

dotguard gitleaks TruffleHog GitHub scanner
Install npx, zero deps Go binary / package mgr Go / Docker None
Scans git history ✅ (pushed only)
Rule coverage (test file) 4/7 7/7 7/7 7/7*
Typical time on test file ~1 s ~2–5 s minutes (verify) async
Works fully offline partial
Extra signal missing-key hints SARIF/JSON reports live-secret verification push blocking

* with built-in patterns; generic lines need custom patterns.

So what do I actually use?

The honest answer is: all of them, at different layers. This is the setup I run:

Layer 1 — pre-push, seconds. dotguard as a git hook or a quick manual check. Its job is the question "is this file I'm touching right now clean?" It's fast enough that I actually run it, which is more than I can say for tools I have to think about. It's the bouncer at the door, not the security team.

#!/bin/sh
# .git/hooks/pre-push
npx @wuchunjie/dotguard .
Enter fullscreen mode Exit fullscreen mode

Layer 2 — CI, every push. gitleaks with history scanning, failing the pipeline on a new finding. This is the layer that catches the fourteen-months-ago secret.

- name: Deep secret scan
  uses: gitleaks/gitleaks-action@v2
Enter fullscreen mode Exit fullscreen mode

Layer 3 — platform. GitHub's secret scanning as the backstop for anything that slips past both, plus push protection on private repos.

Layer 4 — incident response. TruffleHog, only when something has actually leaked and I need to know which of the exposed candidates are live.

dotguard sits at layer 1 and does exactly one job well: instant, zero-setup triage of environment files on any machine that can run Node. It will never replace gitleaks — its rule set is small and it doesn't see history — and the .env-skipping quirk means you should know exactly what it's checking. But for the question my coworker asked me — "secrets in there?" — it's the fastest "here's the line number" I have, and the line numbers are what let you fix things before the push instead of after the incident.


dotguard is free and open source: npm package, npx @wuchunjie/dotguard. If the .env naming behavior bit you too, or you want a provider-key-format rule (AWS/Stripe detection by value instead of variable name), the issue tracker is open — those are exactly the two gaps this comparison exposed.

Top comments (0)