AgenticCLI
▮ shipguardis a free, local secure-ship agent for AI-built apps — 5 checks in ~2s.$ npm i -g @agenticcli/shipguard

What a Real Secret Scan Found in a Vibe-Coded App

7 min · jul 2026

A secret scanner checks your source files for hardcoded credentials and tells you exactly where they are. We ran @agenticcli/[email protected] against a representative vibe-coded Next.js + Supabase repo and got back 4 findings in under 200ms: 2 critical, 2 high. One of them was a live Stripe secret key sitting in two separate files at once.

TL;DR

  • A free scan of a 9-file vibe-coded Next.js + Supabase repo produced 4 findings: 2 critical, 2 high, verdict DO_NOT_SHIP
  • A live Stripe sk_live_ key appeared in both .env.local (line 5) and lib/stripe.ts (line 3)
  • The .env.local file itself was tracked in git, so the key was already committed to version control
  • A missing middleware.ts left the app's routes without an auth protection layer
  • Without --strict, ShipGuard's exit code stays 0 — the verdict lives in the JSON recommendation field; --strict is what turns a finding into a blocking exit 1

What the scan actually found

The fixture is a representative Next.js + Supabase app seeded with the mistakes vibe-coded apps ship most often: a live Stripe key pasted into .env.local and also hardcoded in the file that initializes the Stripe client, that .env.local committed to git, and no middleware.ts to enforce auth on protected routes. We ran a plain shipguard scan — no flags, no login, no --strict.

Nine files. Four findings. The real, unedited output:

   CRITICAL  🛑 2 findings
    Stripe Live Secret Key — .env.local:5 [low false-positive chance]
      Evidence: ...sk_live_***...
    Stripe Live Secret Key — lib/stripe.ts:3 [low false-positive chance]
      Evidence: ...sk_live_***...

   HIGH      ⛔ 2 findings
    .env file tracked in git — .env.local [low false-positive chance]
    No Next.js middleware.ts found — middleware.ts [moderate — review]

  Files scanned:  9
  Elapsed time:   ~0.17s
  Total findings: 4
  🛑 2 critical  ⛔ 2 high

Verdict: DO NOT SHIP. Both criticals carry ShipGuard's "low false-positive chance" confidence rating; the missing-middleware finding is rated "moderate — review," since absence-of-a-file checks can't be as certain as a literal string match.

The --json output behind that same run (trimmed to one finding for brevity — the real report has all 4):

{
  "findings": [
    {
      "id": "hardcoded_secret",
      "title": "Stripe Live Secret Key",
      "severity": "critical",
      "category": "secrets",
      "file": ".env.local",
      "line": 5,
      "confidence": "HIGH",
      "recommendation": "Remove immediately. Use process.env.STRIPE_SECRET_KEY. Rotate the key in the Stripe Dashboard.",
      "blocking": true
    }
  ],
  "riskScore": { "score": 100, "recommendation": "DO_NOT_SHIP", "totalFindings": 4, "criticalCount": 2, "highCount": 2 },
  "filesScanned": 9,
  "recommendation": "DO_NOT_SHIP"
}

The detail worth holding: the Stripe key turned up in two places, not just the environment file — it was also hardcoded into the file that initializes the Stripe client. Rotate .env.local but leave lib/stripe.ts untouched, and the exposure continues. A scanner that only checks .env files misses the second copy entirely.

For context on the other half of a scan like this, the database-layer issue (Supabase Row Level Security left disabled) is a Pro-tier rule and doesn't fire on the free scan — the Supabase RLS security check covers that gap in depth on its own.

Why does a vibe-coded app produce this output?

This pattern isn't surprising once you've watched how AI coding agents actually work. The agent's goal is a functioning app. It reaches for the Stripe API, needs credentials to authenticate, and puts them where they work: an environment variable, then directly in the code that initializes the client too, just to be safe. Both files pass tests. The app charges correctly. Nothing in the agent's feedback loop flags the placement as wrong.

The .env.local-tracked-in-git finding is a related failure mode. Next.js scaffolds a .gitignore that excludes .env.local by default, but an agent that creates .env.local before a .gitignore exists can commit it before the exclusion rule is in place. Once a file is in git history, removing it from the working tree doesn't remove it from history — every future clone pulls the credential along.

This is the pattern OWASP A02:2021 Cryptographic Failures describes: credentials committed to version control, usually because the developer assumed the exposure was temporary or private. The assumption doesn't change what's in the history.

The missing middleware.ts is structurally similar, even though it's an auth issue rather than a secrets one. The app works without it — pages load, nothing crashes — so an agent validating against visible behavior has no signal that a route needing authentication doesn't have it. There's nothing loud about an absence.

How do secret scanner tools actually work?

A secret scanner has one job: find credentials that shouldn't be in code. Tools in this space approach it differently, and the differences matter for where you put one in your workflow.

ShipGuard runs five free modules — secrets, auth, database, payments, deployment — locally, no network call. The free-tier secrets module matches Stripe live/test keys, AWS access keys, GitHub tokens, OpenAI and Anthropic keys, Google API keys, JWT secrets, database URLs with embedded credentials, PEM private key blocks, .env files tracked in git, and generic high-entropy strings (medium confidence). All matching — free-tier and Pro rules alike — runs through RE2JS v2.8.3: linear-time guarantees against ReDoS, patterns compiled once per scan rather than per file.

gitleaks specializes in git history — the right choice for auditing whether a credential was ever committed, even if it was removed months ago from the current tree.

TruffleHog combines pattern matching with entropy analysis. High-entropy strings that don't match a named pattern still get flagged for review.

detect-secrets is built primarily for pre-commit hook integration — it catches secrets at the moment of commit, before they enter git history at all.

GitHub Secret Scanning runs on GitHub-hosted repos against pushed commits. Push protection can block a push containing a recognized secret, but it needs a GitHub-hosted repo and the right plan.

Tool Primary scope Blocks CI?
ShipGuard Working tree, five free modules With --strict — exit 1
gitleaks Git history + working tree Yes — exit 1 on findings
TruffleHog Git history + working tree Yes — exit 1 on findings
detect-secrets Working tree (pre-commit) At commit time
GitHub Secret Scanning Pushed commits (GitHub-hosted) Optional, plan-dependent

How to run a secret scan before you ship

No prerequisites — no account, no API key:

npx @agenticcli/shipguard scan

That's the exact command behind the output above. For a CI gate or pre-deploy step, add --changed --strict --json:

npx @agenticcli/shipguard scan --changed --strict --json

--changed restricts the scan to files modified since the last commit, keeping CI time short in large repos. --strict turns any finding into exit code 1, failing the pipeline step. --json gives you the machine-readable shape shown above.

Exit codes are contractual, and worth being precise about: exit 0 means the scan completed — read the JSON recommendation field for the actual verdict, since a DO_NOT_SHIP scan still exits 0 unless --strict is set. Exit 1 fires only with --strict, when at least one blocking finding exists. Exit 2 signals a config or degraded-scan condition. Exit 3 is a runtime crash. These are stable and safe to branch on in CI scripts or agent workflows.

What the free scan doesn't cover

The four findings above came from the free tier, and that's worth being direct about. The five free modules — secrets, auth, database, payments, deployment — run locally with no network dependency, plus 5 cloud scans per month included at no cost; once those are used, the CLI falls back to local scanning automatically, no error, no block.

The Supabase Row Level Security check (supabase.rls.table-rls-disabled) is a Pro-tier rule. If your fixture has RLS disabled on a table, that finding won't appear in free output — catching it needs the broader Pro rule corpus. For a Next.js app backed by Supabase, that's a real gap: the free scan tells you a Stripe key is exposed; whether your database is world-readable needs the Pro corpus to confirm.

When should you add a secret scanner?

Any project touching external APIs needs one — that includes most vibe-coded apps from the first commit, since agents reach for credentials early in scaffolding.

The real question is placement: pre-commit, CI, or pre-deploy gate. Pre-commit hooks catch secrets before they enter history. CI gates catch what slips through a commit hook. A pre-deploy scan is the last line before production. These layers stack rather than compete.

What to do after a finding

Rotate first, clean second. Revoke or roll the exposed key with the provider before touching git. For a Stripe live key, go to the Stripe dashboard under Developers → API Keys, roll it, and confirm the new key works. Rotation caps the exposure window while the rest of the cleanup happens. OWASP A07:2021 Identification and Authentication Failures names credential exposure in version control as a common authentication-failure pattern — rotation is the first response, not an optional follow-up.

Purge from git history. Removing a file from your working tree doesn't remove it from history — every future clone still pulls the committed credential. Use git filter-repo (the maintained tool, replacing the deprecated git filter-branch) to rewrite history and strip the file, then force-push and have collaborators re-clone.

Move secrets to environment variables, loaded at runtime via process.env.STRIPE_SECRET_KEY, never hardcoded in source. Add .env.local to .gitignore before re-creating it.

Verify with a re-scan. Run npx @agenticcli/shipguard scan again after the history rewrite and variable migration to confirm the finding is gone. A clean scan after remediation is the confirmation step, not an assumption.

For teams wanting broad coverage across both current files and history, a practical stack is a pre-commit tool like detect-secrets, a history scanner like gitleaks for periodic audits, and ShipGuard as the deploy gate — each layer catches what the others can miss.

Finding a live Stripe key in a vibe-coded app isn't remarkable. The key appearing in two separate files at once is the more useful detail — it's exactly the case a scanner that only checks .env files would miss. The infrastructure for catching this class of mistake isn't complicated. The harder part is running the scan before the deploy, not after.


Published by AgenticCLI — developer tools for teams shipping AI-assisted code. ShipGuard is a CLI-first release gate that runs locally, with no code leaving your machine, and gates deploys via --strict in CI.

FAQ

What is a secret scanner?
A secret scanner is a tool that examines your source code for hardcoded credentials — API keys, database passwords, private signing keys, provider tokens like Stripe's sk_live_ prefix. When it finds a match, it reports the file and line number. Some tools exit non-zero so they can block a CI pipeline or a deploy step. The goal is to catch credentials before they reach a public repository, a built container image, or a production deployment where they can be extracted and misused. Most secret scanners match patterns against file content; some also use entropy analysis to flag high-randomness strings that look like credentials even when they don't match a named format.
What does a free ShipGuard scan catch?
The free tier scans for five categories: secrets, auth gaps, database risks, payment-handler mistakes, and deployment configuration problems. No login is required and no code leaves your machine. Running a free scan (ShipGuard v0.5.0) against a representative vibe-coded Next.js + Supabase app returned 4 findings in under 200ms: a live Stripe secret key hardcoded in two separate files, a tracked .env.local, and a missing middleware.ts. Pro cloud scans cover a broader rule corpus that includes database-layer checks like Supabase Row Level Security, which does not fire on the free tier.
Is a live Stripe secret key in a private git repo actually dangerous?
Yes, meaningfully so. A private repository is access control, not a security boundary, and access control can fail — former collaborators keep history access until it's explicitly revoked, and leaked tokens from private repos do turn up in breach databases later. Stripe's sk_live_ keys authorize charges, refunds, and subscription changes without a second factor. A key in git history is a real financial exposure for as long as it stays unrotated, regardless of whether you believe the repo was ever accessed externally.
How does ShipGuard detect hardcoded API keys?
ShipGuard scans every file in your project path and matches against known credential patterns — Stripe live/test keys, AWS access keys, GitHub tokens, OpenAI and Anthropic keys, and more. With the free tier, all matching runs locally: no file content leaves your machine. Both the free-tier secrets scan and Pro rules run on RE2JS v2.8.3, which gives linear-time matching guarantees against ReDoS — patterns that can hang naive regex engines on crafted input. Patterns are compiled once before the file loop, not per file, so scan time stays consistent regardless of project size.
How does ShipGuard compare to gitleaks and TruffleHog?
gitleaks and TruffleHog both specialize in git history scanning — strong for surfacing secrets that were committed and later deleted from the current tree. ShipGuard scans the current working tree and files, with a gate that also covers auth, payments, and deployment configuration alongside secrets. They're complementary, not competing: run gitleaks against your git history and ShipGuard against your working tree before a deploy to cover both. ShipGuard also exits 1 on `--strict`, the same CI-blocking pattern gitleaks and TruffleHog use through their own exit codes.
How do I add a secret scanner to my CI pipeline?
One command: npx @agenticcli/shipguard scan --changed --strict --json. --changed restricts the scan to files modified in the current push, keeping CI time short. --strict exits 1 on any finding, failing the CI step. --json produces machine-readable output for downstream tooling or agents. For a pre-commit hook, drop --changed and scan the full working tree at commit time.
Does ShipGuard scan git history for previously committed secrets?
No. ShipGuard scans your current working tree and files, not git history — if a secret was committed months ago and later removed from the files, the free scan won't surface it. That's the job gitleaks and TruffleHog are built for. The practical split: run ShipGuard as your pre-deploy gate on current files, and run a git-history scanner periodically as an audit of what was there before.

────────[ ▮ gate ]────────

Don't ship the next one.

Free, local, no account. Catches this exact bug class before deploy.

$ npx @agenticcli/shipguard scan