From vibe-app-audit
Audit a vibe-coded app (Lovable, v0, Bolt, Cursor, Claude Code output) against a security checklist and produce a markdown report with severity-tagged findings. Use this skill whenever a user wants to security-review, audit, or "find the security issues in" an AI-generated app, a no-code/low-code project, an exported Lovable/Bolt/v0 codebase, or any web app where they suspect AI tooling skipped security basics. Trigger even if the user doesn't say "audit" — phrases like "is my app safe," "did Cursor leave any secrets in here," "check this for security issues," "review my Supabase setup," or "what's wrong with this codebase security-wise" all qualify. Covers exposed secrets, database access controls (Supabase RLS and plain Postgres), input validation, authorization, security headers, static analysis, dependencies, and runtime monitoring gaps. Produces a markdown report grouped by severity, not a list of opinions.
How this skill is triggered — by the user, by Claude, or both
Slash command
/vibe-app-audit:vibe-app-auditThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill audits a local repository (run from Claude Code/Codex) against eight security categories that AI coding tools commonly fumble, and produces a markdown report with severity-tagged findings.
This skill audits a local repository (run from Claude Code/Codex) against eight security categories that AI coding tools commonly fumble, and produces a markdown report with severity-tagged findings.
The skill is built around a specific failure mode: AI coding tools ship code that looks secure but isn't. Validation is in the browser only. RLS is "enabled" but has no policies. The service role key is in NEXT_PUBLIC_*. Authorization checks ask "is the user logged in" instead of "does this user own this row." This skill catches those.
Use this when the user wants to security-review a repo. They may say "audit my app," "find the security issues," "is this safe to ship," "review my Lovable export," or just paste a path and ask "what's wrong with this." If they're pointing at a real codebase and want a security perspective, this is the skill.
Don't use this for: generic security questions (just answer them), reviewing a single function or PR (do it inline), or non-web apps. This skill assumes a web app with a backend, a database, and dependencies.
If the deployed URL isn't provided, do the static checks and explicitly mark the headers check as "needs deployed URL."
Work through the eight checks in order. Each check is independent — finish one, write findings to a running list, move on. Don't go deep on one check at the expense of the others; breadth-first is the goal. At the end, render the findings into the report template.
For each finding, capture: severity (Critical / High / Medium / Low / Info), the check it came from, file path + line number where applicable, a one-sentence description of the issue, and a one-sentence fix. The report template (references/report-template.md) shows the exact shape — read it before writing the report.
Before running checks, take ~30 seconds to understand the stack. This shapes which references to read.
# What kind of project? (some manifests are expected to be absent)
ls package.json pyproject.toml requirements.txt Gemfile go.mod 2>/dev/null
# What's in package.json (if present)?
[ -f package.json ] && head -50 package.json
# Supabase?
grep -rl "supabase" --include="*.{ts,tsx,js,jsx,py}" . | head -5
# Postgres directly?
grep -rlE "pg|psycopg|sequelize|prisma|knex|drizzle" --include="*.{ts,js,py}" . | head -5
# Framework? (manifest absence is expected; suppress only those errors)
grep -hE "next|vite|express|fastapi|django|flask|rails" \
package.json pyproject.toml Gemfile 2>/dev/null
Note what you found. The reference files live alongside this SKILL.md; resolve their absolute path so reads work in both Claude Code and Codex:
SKILL_DIR="${CLAUDE_SKILL_DIR:-${SKILL_DIR}}"
# References to load on demand:
# $SKILL_DIR/references/supabase.md
# $SKILL_DIR/references/postgres.md
# $SKILL_DIR/references/report-template.md
If Supabase is in the stack, you'll need references/supabase.md for items 2 and 4. If it's plain Postgres + an app framework, you'll need references/postgres.md. If both are present (rare but possible), read both. If neither — record the items that depend on a database backend as "skipped: no recognized database backend detected" and move on.
Look for secrets that are baked into client-side code or otherwise committed.
# Public env vars containing secrets — the dangerous pattern
grep -rE "(NEXT_PUBLIC_|VITE_|REACT_APP_|EXPO_PUBLIC_|PUBLIC_)[A-Z_]*(SECRET|KEY|TOKEN|PASSWORD|SERVICE_ROLE)" \
--include="*.{ts,tsx,js,jsx}" --include=".env*" .
# Hardcoded common secret formats. Note: `eyJhbGciOi` matches any base64-encoded
# `{"alg":` JSON, including legitimate sample JWTs in fixtures and docs —
# investigate matches before flagging, don't flag fixture tokens.
grep -rE "(sk_live_|sk_test_|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|xox[baprs]-|eyJhbGciOi)" \
--include="*.{ts,tsx,js,jsx,py}" --include=".env*" .
# Supabase service role key — the headline incident
grep -rE "service_role|SERVICE_ROLE_KEY" --include="*.{ts,tsx,js,jsx,py}" .
# Is .env committed? (only-noise stderr is expected outside a git repo)
git ls-files 2>/dev/null | grep -E "^\.env($|\.)" || echo ".env not tracked"
# .env in git history (even if removed). Suppress: missing files are expected.
git log --all --full-history --source -- .env .env.local .env.production 2>/dev/null | head -20
Severity guide:
NEXT_PUBLIC_*, anything imported into a client component, anything in pages//app/ that isn't 'use server' / API route): Critical..env in git history but currently gitignored: High — secrets need rotation, gitignore alone doesn't help.A NEXT_PUBLIC_SUPABASE_ANON_KEY is expected and not a finding by itself — the anon key is meant to be public. The finding is when it's the service role key that's been exposed, or when RLS isn't enabled to make the anon key safe to expose (that's check 2).
This check is backend-specific. Read the relevant reference file before proceeding:
references/supabase.md and follow the RLS audit procedure there.references/postgres.md and follow the access control audit there.Don't try to do this check from memory — the per-backend procedures are specific and the reference files exist for a reason.
The failure mode: forms validate in the browser, but the API endpoint accepts whatever the client sends.
# Find API endpoints / route handlers
find . -path ./node_modules -prune -o \
\( -path "*/api/*" -o -path "*/routes/*" -o -name "route.ts" -o -name "route.js" \) \
-print | head -30
# Validation libraries present? (manifest absence is expected.)
grep -hE '"(zod|yup|joi|valibot|class-validator|pydantic|marshmallow)"' \
package.json pyproject.toml requirements.txt 2>/dev/null
For each handler found, open it and look for: does it parse/validate the request body before using it, or does it pass req.body / request.json() straight into a database call or business logic?
Patterns to flag:
req.body.x used directly in a query or response without a .parse() / .validate() first.as MyType) standing in for validation — TypeScript types vanish at runtime.Severity: Missing validation on an endpoint that writes to the database or returns user data: High. Missing validation on a read-only endpoint with no user-controlled query: Medium. Type-assertion-only "validation": High (false sense of security is worse than none).
Like check 2, this depends on the backend.
references/supabase.md.references/postgres.md.Read the relevant reference and follow it. The headline failure mode is the same across backends: the code checks "is the user authenticated" but not "does this user own the row they're asking about." IDOR (insecure direct object reference) is the canonical AI-coded-app bug.
Severity guide (apply regardless of backend; the references show the patterns to look for):
| Pattern | Severity |
|---|---|
| Write endpoint (POST/PUT/PATCH/DELETE) accepts a resource ID and skips ownership check | Critical |
| Read endpoint returns user-owned resource by ID with no ownership filter (IDOR) | Critical if data is sensitive (orders, messages, files, PII); High otherwise |
userId / user_id taken from request body or query string instead of the authenticated session | Critical |
Admin/role-gated endpoint with only requireAuth (no role check) | Critical |
RLS policy USING (true) or missing WITH CHECK on writes | Critical |
| Service-role / admin DB client used in a code path without an explicit ownership check | High–Critical by data sensitivity |
| Authorization enforced inconsistently across endpoints (some have it, some don't) | High |
| Authorization correct but only enforced in middleware that can be bypassed by a sibling route | High |
| Read endpoint exposes non-sensitive shared data with no ownership check (e.g., public catalog) | Low / Info |
If a deployed URL was provided:
curl -sI <DEPLOYED_URL> | grep -iE "content-security-policy|x-frame-options|x-content-type-options|strict-transport-security|referrer-policy|permissions-policy"
Score what's missing. Severity guide:
Strict-Transport-Security on an HTTPS production site: Medium.Content-Security-Policy: Medium (XSS mitigation, but not a vulnerability by itself).X-Frame-Options (or CSP frame-ancestors) on a site that handles auth: High (clickjacking risk).X-Content-Type-Options: nosniff: Low.Referrer-Policy: Low.If no deployed URL, check the framework config for header configuration. Manifest absence is expected — 2>/dev/null is scoped to that:
# Next.js
grep -A20 "headers" next.config.js next.config.mjs next.config.ts 2>/dev/null
# Vercel / Netlify
cat vercel.json netlify.toml 2>/dev/null
# Express helmet
grep -r "helmet" --include="*.{ts,js}" . | head -5
If you can't probe a live URL and there's no header config in the repo, record this as "Headers: cannot verify without deployed URL — no header configuration found in repo, which suggests defaults are in use." Severity: Info (a verification gap, not a finding).
Run a SAST scan if a scanner is available. Prefer opengrep (LGPL-2.1, the OSS fork of semgrep maintained by ex-Semgrep contributors); fall back to semgrep CE if opengrep isn't installed. Both accept the same ruleset shorthands. Lead with the security-focused rulesets — --config=auto includes a lot of code-quality rules that aren't security findings and dilute the report:
# Prefer opengrep (LGPL-2.1, OSS); fall back to semgrep CE if unavailable.
if command -v opengrep >/dev/null; then
SCANNER=opengrep
elif command -v semgrep >/dev/null; then
SCANNER=semgrep
else
SCANNER=
fi
if [ -n "$SCANNER" ]; then
# Security-focused rulesets first; --config=auto only as fallback.
"$SCANNER" --config=p/security-audit --config=p/owasp-top-ten --json --quiet .
# Fallback if those rulesets fail to fetch (offline, etc.):
# "$SCANNER" --config=auto --json --quiet . | head -200
fi
If neither opengrep nor semgrep is installed, note that and skip — don't try to recreate a SAST scanner with grep. Record as "Static analysis: skipped (install opengrep — https://github.com/opengrep/opengrep — or semgrep, then re-run)."
If results are returned, group findings by rule and severity. Don't dump raw scanner JSON into the report — pick the highest-severity 5–10 findings and summarize them. The report-template has a slot for "scanner findings" that handles this.
Detect the lockfile and pick one tool — running npm audit against a non-npm lockfile produces noise:
# Node — the lockfile picks the tool
if [ -f bun.lock ]; then bun audit --json
elif [ -f pnpm-lock.yaml ]; then pnpm audit --json
elif [ -f yarn.lock ]; then yarn npm audit --json
elif [ -f package-lock.json ]; then npm audit --json
fi
# Python
[ -f requirements.txt ] && pip-audit -r requirements.txt --format json
[ -f pyproject.toml ] && [ ! -f requirements.txt ] && pip-audit --format json
# Ruby
[ -f Gemfile.lock ] && bundle-audit check --update
# Go
[ -f go.mod ] && govulncheck ./...
Report counts by severity from the audit tool's own classification. Highlight any Critical or High with a known exploit (audit tools usually note this). Don't list every Low — just count them.
If no audit tool is available for the detected stack, record as "skipped: no audit tool available for " and recommend the relevant one.
This one is mostly observational — most vibe-coded apps have nothing here, and "nothing here" is itself the finding.
# Logging libraries (manifest absence is expected; suppress only that)
grep -hE '"(winston|pino|bunyan|sentry|datadog|loglevel)"' package.json 2>/dev/null
grep -hE "(sentry-sdk|loguru|structlog)" requirements.txt pyproject.toml 2>/dev/null
# Error tracking
grep -rE "Sentry\.(init|captureException)" --include="*.{ts,js,py}" . | head -5
# Auth event logging
grep -rE "log.*(login|signin|signup|auth|failed)" --include="*.{ts,js,py}" . | head -10
Severity guide:
This check is necessarily heuristic — absence of monitoring code doesn't mean absence of monitoring (could be at the platform layer, e.g., Vercel logs). Note this caveat in the report.
Read references/report-template.md for the exact format, then render the findings into it. Key principles:
references/report-template.md for the exact rules and acceptable phrasings. Skip it entirely on clean-ish reports — pitching after few findings reads as cynical.Save the report to the working directory as vibe-app-audit-<YYYY-MM-DD>-<HHMM>.md — always include the time, so same-day re-runs after fixes don't overwrite previous reports. Use the local time the audit started. Example: vibe-app-audit-2026-05-07-1430.md. Tell the user the exact path of the report. Don't try to file Linear issues, send Slack messages, or do anything else with the findings unless the user explicitly asks — this skill produces a report, full stop.
If the user wants any of the above, say so and stop. Don't pretend to do something this skill doesn't do.
Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub fencer-security/skills --plugin vibe-app-audit