Skip to main content
Glama

The problem

You built something real with Claude Code, Cursor, Lovable or Bolt. It works. Now you are about to put it in front of real users, and you have no security team.

This is how apps like yours actually get breached, in order of how often it happens:

What goes wrong

What it costs

A service_role key, Stripe secret or LLM key ends up in a committed .env or the JS bundle

Full read/write of your database, forged charges, an API bill you did not agree to

Row Level Security is off, or a policy says USING (true), or it checks "logged in" but not "owns this row"

Any signed-up user reads or edits everyone's data by changing an id

A route trusts a userId from the request body, or an /admin page is only hidden in the UI

Same thing, one curl away

A Stripe webhook never verifies its signature, or the price comes from the client

Free product

A dependency pinned to whatever version the model remembered

A CVE you never wrote

No rate limit on the LLM route you shipped

One user loops it and drains your credits

In 2025 one scan of 1,645 apps built with a single AI builder found 170 with wide-open databases. Independent testing puts the share of AI-generated code with an OWASP Top 10 flaw at around 45%. The r/vibecoding thread with the most upvotes this year is a list of the same seven mistakes found in "every single" AI-built app a consultancy cleaned up.

Every one of those is a known, catchable defect class. keysnag catches them before the push.

Related MCP server: Security Scanner MCP

How it works

  1. You (or your agent) run git push.

  2. keysnag's pre-push hook scans the tracked tree and, if you gave it a URL or database, the live site and the schema.

  3. A critical finding (a leaked secret, RLS off on user data, a proven cross-account read) blocks the push with the file, the line, and a fix written so a coding agent can apply it. Inside Claude Code, the plugin hook wakes Claude with the findings so it fixes them before continuing.

  4. Everything else is saved to keysnag.report.md as advisories for you to triage when you choose. Clean code goes through untouched.

It is a plain git hook, so it behaves identically whether you push from Cursor, VS Code, a terminal, or CI.

Install in 60 seconds

No install step. Run it straight from npm inside your project:

# one-off scan of the current repo
npx keysnag scan

# add the pre-push gate (chains any hook you already have, gitignores its reports)
npx keysnag install

Prefer a local checkout? git clone https://github.com/Sheldon-desouza/keysnag.git && cd keysnag && npm install && npm run build, then node dist/cli.js scan --repo /path/to/your/app.

Optional: point it at your live site and database to unlock the live checks. Copy .env.example to .env in the keysnag folder and fill in what you have:

KEYSNAG_SITE_URL=https://your-app.vercel.app           # live-URL probes
KEYSNAG_PG_URL=postgres://readonly:...                  # RLS + storage audit (read-only role)
KEYSNAG_SUPABASE_URL=https://xxxx.supabase.co          # cross-account test
KEYSNAG_SUPABASE_ANON_KEY=eyJ...
KEYSNAG_TOKEN_A=eyJ...                                  # two test users' JWTs
KEYSNAG_TOKEN_B=eyJ...

Any check whose inputs are missing is skipped and the report tells you exactly which variable would turn it on. Nothing is ever a false failure.

What it checks

13 checks, grouped by what they need. "Blocks a push?" reflects the default gate (--fail-on critical): only critical findings stop a push; everything else is reported and left for you.

Repo / static (no inputs beyond the code itself)

Check

What it catches

Blocks a push?

secrets

API keys, service_role JWTs and other credentials hardcoded or committed where they reach a client bundle. Git-aware, so it never flags a gitignored .env

yes

config

Secrets in NEXT_PUBLIC_*, TLS verification off, production source maps, CORS * with credentials, insecure cookies, Math.random tokens, MD5/SHA1 passwords, hardcoded JWT secrets, debug flags

yes (TLS-off, public secret)

authz

API routes and server actions that hit the database with no auth check, routes that trust a client-supplied user id, /admin routes with no real role check

yes (writes with no auth)

injection

SQL string concatenation, dangerouslySetInnerHTML fed by user data, eval, shell commands built by interpolation, SSRF, path traversal, prototype pollution

yes (request-derived SQL/eval/exec)

payments

Stripe webhooks that never verify the signature, charge amounts taken from the client, missing idempotency keys

yes

backdoor

Obfuscated base64-eval chains, hardcoded password bypasses, auth-skip flags, process.env dumped to a response, calls to tunnels, paste sites and bot APIs, leftover prompt-injection text

yes (obfuscated eval, literal bypass)

ai-endpoints

LLM calls made from the browser, AI routes with no auth or no spend bound, prompts built from raw input, unsanitised LLM output rendered as HTML

warn

deps

Known CVEs for your exact lockfile versions with the upgrade that fixes them, unpinned deps, install-time scripts, lockfile drift

yes (critical CVE with a fix)

Before a repo goes public

Check

What it catches

Inputs

Blocks a push?

leaks

Things that are not secrets but should not be public: your local home-directory paths, names you mark as private (a client, an employer, an internal project), committed scaffolding such as .vercel/ or agent notes, and those private names in commit messages and old file history

optional private-terms list

no (run with --fail-on high before flipping a repo public)

List private names in a gitignored .keysnag-private file, one per line, or in KEYSNAG_PRIVATE_TERMS as a comma-separated list. Never put them in keysnag.config.json, because that file is usually committed and the list would leak itself. Then run:

npx keysnag scan --checks leaks --fail-on high

Live site

Check

What it catches

Inputs

Blocks a push?

urlprobe

Exposed .env/.git/source maps, admin routes reachable logged out, missing security headers, no rate limiting on a real auth endpoint (with a control request so a framework's default-deny is never mistaken for one)

KEYSNAG_SITE_URL

partial

Database

Check

What it catches

Inputs

Blocks a push?

rls

RLS off, USING (true) policies, login-only policies with no ownership check, anon grants on RLS-off tables. "RLS on, no policy" is reported as the safe locked-down default it is, not a bug

KEYSNAG_PG_URL (read-only role)

yes

storage

Public Supabase storage buckets, wide-open storage policies, edge functions with JWT verification off, cron routes with no shared secret

KEYSNAG_PG_URL plus repo

yes (public bucket with user files)

Accounts

Check

What it catches

Inputs

Blocks a push?

twoaccount

User B reading, or no-op-writing, user A's rows through the Supabase REST API. Proves the gap the way an attacker would, without changing real data

Supabase URL, anon key, two test users' JWTs

yes

The full threat model, the evidence behind each check, severity rules and what is planned next: docs/THREAT-MODEL-AND-CHECKS.md.

Why you can trust it

Most scanners get switched off within a week because they cry wolf. keysnag was built around one rule: a check may block a push only after it has proven zero false positives on a real production codebase. Until then it warns.

That rule was tested, not assumed. Every check was run against a real production Next.js + Supabase app with 1,400 tracked files:

  • The first full run produced 144 findings. Hand-verification showed all but 28 were false positives, each with a systematic cause: a storage-key name mistaken for a secret, a Next.js JSON-LD tag mistaken for XSS, a framework's blanket 401 mistaken for an unprotected endpoint, m.role === 'user' mistaken for a backdoor.

  • Each cause became a fix with a regression fixture reproducing the real line. The final run is 16 findings, all genuine, zero critical: ten real dependency advisories with their upgrade versions and six medium code advisories.

  • Then three rounds of independent review by a fresh reviewer that built none of it, actively trying to silence the rules. It found and forced fixes for a CVE check that was throwing away severity data, a hook that could never wake Claude, a raw password printed into a report, and a service client being counted as an admin check.

The other trust rules, all enforced in code and tests:

  • Never prints a secret. Every value in a finding is masked to its first and last four characters.

  • Deterministic. Rules, catalogues and live probes. No model guessing at your code, so results are the same every run.

  • Local only. No telemetry. Network only to the site, Supabase project and database you point it at, plus one opt-out call to OSV.dev for CVE data.

  • Exceptions must justify themselves. You can allow a finding you have reviewed, but every allow entry must state a reason and the control that bounds abuse, or keysnag refuses to run. Allowed findings still appear, as info. See Suppressing a finding.

Controlling what blocks

keysnag scan --fail-on critical   # default: only critical findings exit non-zero
keysnag scan --fail-on off        # never block, just report (warn mode)
keysnag scan --diff main          # scope static checks to files changed since main
keysnag scan --no-osv             # skip the one third-party call (OSV.dev CVE lookup)
keysnag install --pre-commit      # also run the secret sweep on every commit
keysnag uninstall                 # remove the hooks, restore anything they chained

--diff <ref> speeds up large repos; secrets always sweeps the full tree, because a secret anywhere is a secret. Install adds keysnag.report.md and .json to your .gitignore so the report never gets swept into a commit.

Install in Claude Code

claude plugin marketplace add Sheldon-desouza/keysnag
claude plugin install keysnag@keysnag

Once installed, keysnag auto-runs on every git commit and git push you make from inside Claude Code. If it finds a blocking finding it feeds it straight back to Claude, which fixes it before continuing, so you never have to notice a failed push. It also adds an on-demand command:

/security-check

That runs the scan and walks you through fixing what it finds, one finding at a time, critical first. It pairs well with Anthropic's own security-guidance plugin: that one reviews the diff as Claude writes; keysnag gates what actually leaves the machine, and it also works in Cursor, Codex and CI where an editor plugin cannot.

Install as an MCP server

keysnag runs as an MCP stdio server exposing run_security_check (the full report) plus one check_<name> tool per check, so Cursor, Claude Desktop, Codex or any MCP client can call it directly:

{
  "mcpServers": {
    "keysnag": {
      "command": "npx",
      "args": ["-y", "keysnag-mcp"],
      "env": {
        "KEYSNAG_REPO_DIR": "/absolute/path/to/your/project",
        "KEYSNAG_SITE_URL": "https://your-app.vercel.app"
      }
    }
  }
}

Suppressing a finding

Sometimes a finding is a real match on code that is, on inspection, acceptable: an admin tool behind its own gate, a webhook that verifies signatures a different way, a public helper with no user data. keysnag lets you record that decision instead of ignoring the finding. Every suppression must say why it is acceptable and, separately, what stops it being abused.

Add an allow array to keysnag.config.json, or use the helper:

keysnag allow authz.route_without_auth src/app/api/public/contact/route.ts \
  --reason "Public contact form, unauthenticated by design." \
  --bound "Per-IP rate limit of 5 per hour, returns 429."
  • id is a concrete finding id (secret.stripe_key) or a dotted prefix (secret.).

  • location is a file, a file:line, or a glob. A location that matches everything is only allowed with a full concrete id, never a prefix.

  • reason and bound are mandatory. keysnag exits 1 before scanning, naming the entry, if either is missing.

A matched finding is never dropped: it becomes info, gets an [allowed] prefix, and carries the reason and bound in every report. It just never blocks.

Privacy

  • No telemetry, no analytics, no phone-home.

  • Network access only to the site, Supabase project, or database you provide. Never to a keysnag server.

  • One exception: deps looks up known CVEs for your exact lockfile versions via OSV.dev's public API. Opt out with --no-osv or KEYSNAG_ALLOW_OSV=false; the offline hygiene rules still run.

  • Secrets are always masked in findings, reports, logs and the CLI.

  • The RLS and storage audits are designed for a read-only Postgres role.

What it does not do

  • It targets Next.js + Supabase apps on Vercel today. Other stacks get a clean skip, never a false result.

  • The static rules are heuristics tuned to stay quiet rather than catch everything. A rule that cannot be made low-false-positive ships as a warning, not a blocker. If a finding does not apply to your code, that belongs in the allow-list with a reason, not a bug report.

  • It is automated scanning, not a substitute for a professional penetration test. keysnag will never tell you your app is unhackable. It removes the defect classes behind most real breaches of AI-built apps, and nothing more.

  • The cross-account write test is a bounded no-op: it writes back the exact value already there, on at most one row, only to prove the gap exists.

Roadmap

  • Tier 2: AST-based analysis to replace the regex heuristics in authz, injection and config; secrets in git history; GitHub Actions workflow injection; Vercel and Supabase auth-config assertions (rate limits, leaked-password protection); an authenticated crawl that replays every request as the other test user.

  • Tier 3: an optional LLM review layer at push time as a plug-in check, and continuous post-deploy monitoring.

Contributing

Every check ships with a fixture that trips it and a real-repo run that does not. If you add a rule, add both, and run npm test. Bug reports for a false positive should include the line that fired; that is the most valuable issue you can file.

Licence

MIT. Built by Sheldon de Souza with Keegan de Souza, a recent cybersecurity graduate who contributes to the threat model and check catalogue.


Looking for: Claude security skill · Claude cybersecurity · Codex cybersecurity · Cursor security check · Claude Code security plugin · vibe coding security · security for AI-built apps · Lovable security scanner · Bolt security · Supabase RLS checker · Supabase security audit · Next.js security gate · pre-push security hook · git pre-push security scan · secret scanner for AI-generated code · IDOR checker · Stripe webhook verification check · OSV dependency scanner · AI founder security tool · security for non-technical founders · MCP security server. keysnag is a free, open-source pre-push security gate for apps built with AI coding agents.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables comprehensive security scanning of code repositories to detect secrets, vulnerabilities, dependency issues, and configuration problems. Provides real-time security checks and best practice recommendations to help developers identify and prevent security issues.
    7 npm
    2
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Automatically detects security vulnerabilities in AI-generated code, scanning for hardcoded secrets, injection flaws, XSS, weak cryptography, authentication issues, path traversal, and vulnerable dependencies across JavaScript, Python, Java, and Go.
    30 npm
    2
    MIT