What a Real Secret Scan Found in a Vibe-Coded App
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) andlib/stripe.ts(line 3) - The
.env.localfile itself was tracked in git, so the key was already committed to version control - A missing
middleware.tsleft the app's routes without an auth protection layer - Without
--strict, ShipGuard's exit code stays 0 — the verdict lives in the JSONrecommendationfield;--strictis 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?
What does a free ShipGuard scan catch?
Is a live Stripe secret key in a private git repo actually dangerous?
How does ShipGuard detect hardcoded API keys?
How does ShipGuard compare to gitleaks and TruffleHog?
How do I add a secret scanner to my CI pipeline?
Does ShipGuard scan git history for previously committed secrets?
────────[ ▮ gate ]────────
Don't ship the next one.
Free, local, no account. Catches this exact bug class before deploy.
$ npx @agenticcli/shipguard scan