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

Secret Scanner for Next.js + Supabase Apps: Scan Before You Deploy

5 min · jul 2026

I built a representative demo Next.js + Supabase booking app and seeded it with the security mistakes that show up most often in AI-built code. Then I ran npx @agenticcli/shipguard scan against it with the real CLI (@agenticcli/[email protected], the version at the time of this capture). The scan took under 200ms across 9 files and came back with a DO_NOT_SHIP verdict. Here's exactly what it found, why each finding is production-dangerous, and what it didn't catch.


Answer capsule: ShipGuard is a local CLI that checks a Next.js/Supabase app for hardcoded API keys, committed .env files, and missing auth guards before you deploy. On a demo app seeded with common AI-coding mistakes, the free tier flagged 4 findings in under 200ms — 2 critical (a Stripe live secret key hardcoded in two places) and 2 high (.env tracked in git, no middleware.ts) — and returned a DO_NOT_SHIP recommendation. Your code never leaves your machine.


The demo app

The fixture is a Next.js App Router + Supabase booking app, hand-seeded — not generated by any AI tool — with the specific mistakes that turn up most often in vibe-coded apps:

  • A live Stripe secret key pasted directly into .env.local, and also referenced directly in lib/stripe.ts
  • That .env.local file committed to the git repository
  • No middleware.ts to enforce authentication on protected routes
  • No Supabase Row Level Security (RLS) enabled — not caught by the free tier (more on that below)

The point isn't to shame any particular tool or workflow — it's that these mistakes are easy to make, easy to miss in a PR review, and genuinely damaging in production. The question worth answering: does a sub-second local scan catch them before a deploy?

The scan

npx @agenticcli/shipguard scan

No configuration, no API call, no code upload. Here's the real, unedited terminal output from that run:

   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

What it caught

Four findings. Nothing added, nothing omitted.

Severity Finding Location Confidence
CRITICAL Stripe Live Secret Key hardcoded .env.local:5 High — low false-positive chance
CRITICAL Stripe Live Secret Key hardcoded lib/stripe.ts:3 High — low false-positive chance
HIGH .env file tracked in git .env.local High — low false-positive chance
HIGH No Next.js middleware.ts found middleware.ts (absent) Medium — review

Verdict: DO_NOT_SHIP — files scanned: 9, elapsed under 200ms, total findings: 4, critical: 2, high: 2. Exit code without --strict: 0 — the scan completed and the verdict lives in the JSON recommendation field. With --strict, that same run exits 1.

Why each finding matters

CRITICAL: Stripe live secret key hardcoded, in two places

A sk_live_ key in a source file or .env.local is not a theoretical risk. Anyone with repo access — a co-founder, a contractor, a CI runner — can drain the Stripe balance or issue refunds to arbitrary accounts. The key showing up in two places (.env.local:5 and lib/stripe.ts:3) is a secondary signal worth noticing on its own: once a secret gets pasted around a codebase, rotation gets harder and the attack surface multiplies, because rotating one copy silently leaves the other live.

The fix — read it from an environment variable set in your deploy platform, never write it into source — takes a few minutes. Undoing the damage from skipping that step can take months.

HIGH: .env file tracked in git

Git history is permanent. Delete the file in the next commit and the secret still lives in the commit graph — anyone with a clone, now or years from now, can git log their way to it.

This is one of the most common AI-coding mistakes: neither Cursor nor Claude Code nor Codex reliably adds .env* to .gitignore before the first commit. The file gets created, everything gets staged, and it ships. A pre-deploy scan catches this before it reaches the remote.

HIGH: No middleware.ts found

Next.js App Router doesn't enforce authentication by default. Without a middleware.ts at the project root protecting authenticated routes, every route under /dashboard, /account, /admin is reachable by direct URL regardless of what the UI shows a logged-out user.

ShipGuard flags the absence of middleware.ts as HIGH because a missing auth guard is one of the most common classes of AI-generated vulnerability, right behind secrets. It doesn't prove your routes are unprotected — it's a signal to verify, which is why its confidence is "medium — review" rather than "low false-positive chance."

What the free tier doesn't catch

Worth being direct about this, because honesty is worth more than a clean scorecard.

The demo app also has no Row Level Security enabled on its Supabase tables, and in a fuller version of this fixture, a service_role key exposed in a public API route. The free tier does not catch either of these — RLS detection (supabase.rls.table-rls-disabled) is a documented rule in ShipGuard's Pro corpus, gated behind the paid tier's authenticated rule fetch, not part of the five free modules (secrets, auth, database, payments, deployment).

If you want to check RLS status today without Pro, look in the Supabase dashboard under Authentication → Policies, or search your API routes for supabase.from('your_table') calls that aren't scoped with a .filter() against auth.uid().

Everything in the findings table above, though, came from the free tier, running locally, with no code leaving the machine.

Gate it in CI

Running the scan manually before every deploy isn't reliable. The fix is one line in your pipeline:

npx @agenticcli/shipguard scan --strict

With --strict, ShipGuard exits 1 when a blocking finding is present — that fails the step. Without --strict, the default is advisory: ShipGuard prints the report and exits 0 regardless of what it found, so the pipeline continues. Use advisory mode on an existing codebase where you want visibility before you enforce; use --strict from day one on a new project.

In GitHub Actions:

- name: ShipGuard security gate
  run: npx @agenticcli/shipguard scan --strict

Wired in on every PR, a committed secret fails the PR before review — not after deploy.

Fix each finding

Stripe live key hardcoded — rotate it immediately in the Stripe Dashboard under Developers → API keys. Remove it from lib/stripe.ts and replace with process.env.STRIPE_SECRET_KEY. Remove it from .env.local and set the real value as an environment variable on your deploy platform instead. Confirm .gitignore includes .env.local.

.env committed to git — add .env* to .gitignore, then git rm --cached .env.local to stop tracking it. Rotate every secret that was in the file — assume the contents are compromised the moment they hit a remote, public or private.

No middleware.ts — create one at the project root:

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // your auth check here — e.g. verify a session cookie or JWT
}

export const config = {
  matcher: ['/dashboard/:path*', '/account/:path*'],
}

The exact check depends on your auth provider (WorkOS, Supabase Auth, NextAuth, Clerk) — the part that matters is that matcher explicitly lists every route that needs authentication.


Try it

npx @agenticcli/shipguard scan

It runs in a fraction of a second on most projects. That's not enough time to read a PR diff — it's enough time to catch a live Stripe key before it reaches production.

FAQ

How do I check a Next.js + Supabase app for leaked secrets before deploying?
Run npx @agenticcli/shipguard scan at your project root. It scans your local files for hardcoded API keys, committed .env files, missing auth guards, and other common AI-coding mistakes. It takes under a second on most projects, needs no account, and never uploads your code.
What is a secret scanner?
A secret scanner is a tool that searches your source code for accidentally committed credentials — API keys, database passwords, tokens, and other secrets that should never appear in code. It catches the class of mistake where a working value gets copied from a .env file directly into source, or .env never made it into .gitignore before the first commit.
Does ShipGuard send my code anywhere?
No. ShipGuard runs entirely on your machine on the free tier. The scan reads your local files with regex pattern matching — no files are uploaded, no code is sent to a server. The verdict is computed locally and printed to your terminal (and written to a local JSON/Markdown report).
How is ShipGuard different from gitleaks or GitGuardian?
gitleaks and GitGuardian focus primarily on git history and repo-level credential detection. ShipGuard is a pre-deploy gate built for the AI-coding workflow: it scans your working directory before you push, checks beyond secrets (auth guards, payments, deployment config), and gives a single score plus a ship/do-not-ship recommendation designed to sit in CI as a blocking gate. It's tool-agnostic — it works the same whether the code came from Cursor, Claude Code, Codex, or a human.
What does "vibe coding security" mean?
Vibe coding means shipping AI-generated code at pace without deeply reviewing every line. "Vibe coding security" is the class of vulnerability that shows up most often in that workflow: hardcoded secrets (an AI tool pastes a working example), missing auth guards (AI-generated routes default to public), and exposed .env files (a file gets staged and committed without anyone deciding that on purpose). ShipGuard's free tier is built around exactly this threat model.
Will ShipGuard catch Supabase RLS issues?
Not on the free tier. Row Level Security detection (supabase.rls.table-rls-disabled) is a documented Pro-tier rule, not part of the free CLI's five modules (secrets, auth, database, payments, deployment). The free tier will catch an adjacent signal — a tracked .env file, an unguarded route reading from an admin client — but it won't tell you RLS specifically is off on a table.
What happens if ShipGuard blocks my pipeline?
Fix the finding — ShipGuard names the exact file and line for each one. The common path: rotate the exposed secret (don't just delete it from source; assume it's compromised the moment it's in git), add .env* to .gitignore, move credentials to your deployment platform's environment variables, and add middleware.ts if it's missing. Then re-run the scan to confirm it's clean.

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

Don't ship the next one.

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

$ npx @agenticcli/shipguard scan