The Bouncer
Nothing gets through the door without a check. This is how Fable 5 checks code before it ships. Follow it exactly. Do not skip checks to save time, and do not soften findings to be agreeable. You work for the user's safety, not their mood.
When to run
- Any deploy, publish, or "make it live" moment. First ship of an app: full sweep.
- Updates to something already live: sweep the changed files plus everything that calls them.
- On request: "run the bouncer", "is this safe to ship".
If a check cannot be run (no access, missing tool, can't execute code), report it as UNCHECKED with the reason. Never silently skip a check.
The sweep (in this order)
1. Secrets in the code
Run this from the project root:
grep -rEn --exclude-dir={node_modules,.git,.next,dist,build,.vercel} \
"(sk-[A-Za-z0-9]{8,}|sk_live_|pk_live_|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{20,}|xoxb-|service_role|-----BEGIN[A-Z ]*PRIVATE KEY|password[[:space:]]*[:=][[:space:]]*[\"'][^\"']{4,}|api[_-]?key[[:space:]]*[:=][[:space:]]*[\"'][A-Za-z0-9_-]{12,})" .
Then confirm env files are not tracked by git:
git ls-files | grep -Ei "(^|/)\.env" && echo "BLOCKER: env file is tracked"
- Any real credential in a tracked file is a BLOCKER.
- If a secret was ever committed and the repo is (or will be) public, deleting it now is not enough. Git history keeps it forever. The key must be rotated. Say this explicitly.
2. The client/server line
Anything that runs in the browser is public. Treat it that way.
- Search frontend code (components, pages, client scripts) for API keys, admin tokens, and database service keys.
- Next.js: every
NEXT_PUBLIC_ variable ships to the browser. Verify nothing sensitive uses that prefix.
- Supabase: the anon key in the client is fine. The
service_role key anywhere client-side is a BLOCKER. It bypasses every security rule the database has.
- Rule of thumb: if a stranger opened DevTools on this page, what would they walk away with?
3. Injection points
Wherever user input meets a system, check how it gets there.
- SQL built with string concatenation or template literals around user input: BLOCKER. Require parameterized queries or the ORM's query builder.
- Shell commands built from user input: BLOCKER.
dangerouslySetInnerHTML, innerHTML, or eval fed by anything a user typed or uploaded: BLOCKER unless the content is sanitized with a real library (not a homemade regex).
4. Auth on every door
List every API route, server action, and webhook endpoint in the project. For each one, answer two questions in writing:
- Who is allowed to call this?
- What happens if a logged-out stranger calls it directly, with someone else's ID in the request?
- Any endpoint that reads or writes user data must verify the session server-side. A hidden button is not security. Client-side checks are decoration.
- Ownership check (the classic miss): if a request says "give me record 47", the server must confirm the requester owns record 47, not just that they are logged in.
- Supabase: confirm Row Level Security is ON for every table with user data. A table without RLS and a public anon key is an open database.
5. Data exposure
- API responses: are they returning whole database rows when the page needs three fields? Password hashes, tokens, other users' emails in a response payload: BLOCKER.
- Error handling: raw stack traces or database errors sent to the browser leak your internals. Log the detail server-side, send the user something generic.
- Logs: no passwords, tokens, or full card numbers in console output or log files.
6. Money and webhooks
- Every webhook (Stripe, payment processors, form providers) must verify the signature before trusting the payload. An unverified webhook endpoint means anyone can forge "payment succeeded".
- Prices and amounts are computed server-side. Never trust a total that arrived from the client.
7. Abuse and cost attacks
- Any endpoint that sends email or SMS, calls a paid AI API, or writes to the database without limits: what happens if someone hits it 10,000 times tonight? If the answer is "a huge bill" or "a full database", it needs rate limiting or auth. FIX SOON at minimum, BLOCKER if payment or AI credits are exposed.
- Public forms: server-side validation on every field, not just HTML
required.
8. Dependencies
npm audit --omit=dev 2>/dev/null || pip list --outdated 2>/dev/null
Flag critical and high vulnerabilities in production dependencies. Do not cry wolf over low-severity noise in dev tooling.
Severity levels
- BLOCKER: do not ship until fixed. Someone gets hurt or robbed.
- FIX SOON: ship is allowed, fix within days. Name the deadline.
- NOTE: worth knowing, no action forced.
The report (exact format)
BOUNCER REPORT
Verdict: CLEAR TO SHIP | SHIP AFTER FIXES | DO NOT SHIP
Blockers:
- [file:line] What it is. What a stranger could do because of it. The exact fix.
Fix soon:
- [file:line] Same format.
Notes:
- ...
Unchecked:
- [check name] Why it could not be run.
Rules of the door
- Every finding gets one plain-English sentence: what a stranger could actually do because of this. The user may not read code. They deserve to understand their own risk.
- Never soften a blocker because the user is excited to launch. "DO NOT SHIP" is a complete sentence; follow it with the fix, not an apology.
- Never pass something you did not check. "Probably fine" is not a verdict.
- When everything is clean, say so plainly and let them ship. The bouncer is not paranoid theater. The job is a true verdict, fast.
1---2name: bouncer3description: Pre-ship security check written by Claude Fable 5. Use before deploying, publishing, pushing to production, or sharing any app, page, API, or feature with real users. Triggers on "deploy", "ship it", "push this live", "publish", "go live", "is this safe", "security check", "run the bouncer". Checks secrets, client/server boundaries, injection, auth, data exposure, webhooks, and abuse vectors, then gives a clear ship or no-ship verdict.4---56# The Bouncer78Nothing gets through the door without a check. This is how Fable 5 checks code before it ships. Follow it exactly. Do not skip checks to save time, and do not soften findings to be agreeable. You work for the user's safety, not their mood.910## When to run1112- Any deploy, publish, or "make it live" moment. First ship of an app: full sweep.13- Updates to something already live: sweep the changed files plus everything that calls them.14- On request: "run the bouncer", "is this safe to ship".1516If a check cannot be run (no access, missing tool, can't execute code), report it as **UNCHECKED** with the reason. Never silently skip a check.1718## The sweep (in this order)1920### 1. Secrets in the code2122Run this from the project root:2324```bash25grep -rEn --exclude-dir={node_modules,.git,.next,dist,build,.vercel} \26 "(sk-[A-Za-z0-9]{8,}|sk_live_|pk_live_|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{20,}|xoxb-|service_role|-----BEGIN[A-Z ]*PRIVATE KEY|password[[:space:]]*[:=][[:space:]]*[\"'][^\"']{4,}|api[_-]?key[[:space:]]*[:=][[:space:]]*[\"'][A-Za-z0-9_-]{12,})" .27```2829Then confirm env files are not tracked by git:3031```bash32git ls-files | grep -Ei "(^|/)\.env" && echo "BLOCKER: env file is tracked"33```3435- Any real credential in a tracked file is a **BLOCKER**.36- If a secret was ever committed and the repo is (or will be) public, deleting it now is not enough. Git history keeps it forever. The key must be rotated. Say this explicitly.3738### 2. The client/server line3940Anything that runs in the browser is public. Treat it that way.4142- Search frontend code (components, pages, client scripts) for API keys, admin tokens, and database service keys.43- Next.js: every `NEXT_PUBLIC_` variable ships to the browser. Verify nothing sensitive uses that prefix.44- Supabase: the anon key in the client is fine. The `service_role` key anywhere client-side is a **BLOCKER**. It bypasses every security rule the database has.45- Rule of thumb: if a stranger opened DevTools on this page, what would they walk away with?4647### 3. Injection points4849Wherever user input meets a system, check how it gets there.5051- SQL built with string concatenation or template literals around user input: **BLOCKER**. Require parameterized queries or the ORM's query builder.52- Shell commands built from user input: **BLOCKER**.53- `dangerouslySetInnerHTML`, `innerHTML`, or `eval` fed by anything a user typed or uploaded: **BLOCKER** unless the content is sanitized with a real library (not a homemade regex).5455### 4. Auth on every door5657List every API route, server action, and webhook endpoint in the project. For each one, answer two questions in writing:58591. Who is allowed to call this?602. What happens if a logged-out stranger calls it directly, with someone else's ID in the request?6162- Any endpoint that reads or writes user data must verify the session server-side. A hidden button is not security. Client-side checks are decoration.63- Ownership check (the classic miss): if a request says "give me record 47", the server must confirm the requester owns record 47, not just that they are logged in.64- Supabase: confirm Row Level Security is ON for every table with user data. A table without RLS and a public anon key is an open database.6566### 5. Data exposure6768- API responses: are they returning whole database rows when the page needs three fields? Password hashes, tokens, other users' emails in a response payload: **BLOCKER**.69- Error handling: raw stack traces or database errors sent to the browser leak your internals. Log the detail server-side, send the user something generic.70- Logs: no passwords, tokens, or full card numbers in console output or log files.7172### 6. Money and webhooks7374- Every webhook (Stripe, payment processors, form providers) must verify the signature before trusting the payload. An unverified webhook endpoint means anyone can forge "payment succeeded".75- Prices and amounts are computed server-side. Never trust a total that arrived from the client.7677### 7. Abuse and cost attacks7879- Any endpoint that sends email or SMS, calls a paid AI API, or writes to the database without limits: what happens if someone hits it 10,000 times tonight? If the answer is "a huge bill" or "a full database", it needs rate limiting or auth. **FIX SOON** at minimum, **BLOCKER** if payment or AI credits are exposed.80- Public forms: server-side validation on every field, not just HTML `required`.8182### 8. Dependencies8384```bash85npm audit --omit=dev 2>/dev/null || pip list --outdated 2>/dev/null86```8788Flag critical and high vulnerabilities in production dependencies. Do not cry wolf over low-severity noise in dev tooling.8990## Severity levels9192- **BLOCKER**: do not ship until fixed. Someone gets hurt or robbed.93- **FIX SOON**: ship is allowed, fix within days. Name the deadline.94- **NOTE**: worth knowing, no action forced.9596## The report (exact format)9798```99BOUNCER REPORT100Verdict: CLEAR TO SHIP | SHIP AFTER FIXES | DO NOT SHIP101102Blockers:103- [file:line] What it is. What a stranger could do because of it. The exact fix.104105Fix soon:106- [file:line] Same format.107108Notes:109- ...110111Unchecked:112- [check name] Why it could not be run.113```114115## Rules of the door1161171. Every finding gets one plain-English sentence: what a stranger could actually do because of this. The user may not read code. They deserve to understand their own risk.1182. Never soften a blocker because the user is excited to launch. "DO NOT SHIP" is a complete sentence; follow it with the fix, not an apology.1193. Never pass something you did not check. "Probably fine" is not a verdict.1204. When everything is clean, say so plainly and let them ship. The bouncer is not paranoid theater. The job is a true verdict, fast.