Free · Prompt pack
The BuiltSafely security prompt pack
AI coding tools build fast. These 8 prompts ask the same tool to stop and check its own work, plus two rules files that keep the guardrails on in every session.
Paste one prompt at a time into your AI coding tool (chat or agent mode), on a project you own. Every prompt starts with the same ground rules, so each one works on its own. Prompts 1, 2 and 6 are for Supabase projects.
About the SQL: we tested the read-only queries in prompts 1 and 2 on a throwaway local Postgres 17 database on our own machine, with stand-in anon and authenticated roles and made-up tables. That's not a full Supabase stack. On the same throwaway database we checked the UPDATE-policy behavior described in prompt 1:
- A policy with only
USING (owner = current_user)blocked a user from reassigning their row to another user ("new row violates row-level security policy"). - Adding
WITH CHECK (true)let the reassignment through. USING (true)let one user edit another user's row. The storage query in prompt 6 is untested; its column names come from Supabase's storage schema docs.
- RLS on every table
- The Oct 30 Supabase GRANTs
- Secrets in frontend bundles
- Provider keys: rotation and spend limits
- Auth and redirects
- Storage bucket policies
- Input validation and rate limits
- Dependencies and slopsquatting
How to use it
- Start with prompt 1, then go in order. Prompts 1, 2 and 6 assume Supabase; the rest apply to most stacks.
- Paste one prompt at a time. Read the report before you approve any change.
- Approve changes one at a time, and test as a logged-out visitor and as a second account afterwards. Our RLS guide shows how.
- Add the rules file for your tool so new code follows the same rules.
Three rules every prompt repeats
- Never disable RLS to make an error go away. Supabase says a secret key "bypasses every Row Level Security policy you have" (Supabase: API keys), and turning RLS off does the same for everyone.
- **No secret or
service_rolekeys in client-side code.** Supabase's rule: secret keys are only for "backend components of your app" (Supabase: API keys). - Show changes before applying them. You stay the reviewer.
Prompt 1 of 8
RLS on every table
Purpose: find every table that RLS doesn't protect, or protects with policies that are too loose.
Security check: Row Level Security (RLS) on every table. Report first, change nothing yet.
Ground rules:
- Do not disable RLS on any table, even temporarily, even to fix an error.
- Never use a Supabase secret key (sb_secret_...) or service_role key in client-side code.
- Show me every proposed change (SQL migration or code diff) and wait for my OK before applying anything.
- If you're not sure about something, say so instead of guessing.
Steps:
1. Run this read-only query (SQL editor, or your database tool) and show me the results:
select c.relname as table_name, c.relrowsecurity as rls_enabled,
has_table_privilege('anon', c.oid, 'select') as anon_select,
has_table_privilege('anon', c.oid, 'insert,update,delete') as anon_write,
has_table_privilege('authenticated', c.oid, 'select') as auth_select,
has_table_privilege('authenticated', c.oid, 'insert,update,delete') as auth_write
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r','p')
order by c.relrowsecurity, c.relname;
2. Run this read-only query and show me every policy:
select tablename, policyname, cmd, roles, qual as using_expr, with_check,
(coalesce(qual, '') = 'true' or coalesce(with_check, '') = 'true') as has_true_expression
from pg_policies where schemaname = 'public' order by tablename, cmd;
3. Make a table with one row per table: RLS on or off, who can read, who can write, and the problem in plain English.
4. Flag: tables with RLS off; anon access to data that isn't meant to be public; USING (true) on private data (too broad); WITH CHECK (true), which loosens the new-row check so a user can hand a row to someone else; UPDATE policies on tables with an owner column (like user_id) that don't check ownership on both the old and new row; policies that read user_metadata (users can edit it); views without security_invoker = true. Note: an UPDATE policy with only USING is not a gap on its own, because Postgres applies USING to the new row too.
5. For each problem, propose a migration that keeps RLS on and adds the narrowest policy that fits: INSERT policies use WITH CHECK ((select auth.uid()) = user_id); UPDATE policies use USING and WITH CHECK both on (select auth.uid()) = user_id, so user_id can't be changed to someone else's. Add a different WITH CHECK only when the new-row rule should differ from USING. Wait for my OK.
6. After I approve, tell me how to test as a logged-out visitor and as a second account.A good result looks like: a table covering every public table, with nothing skipped. Every RLS-off table is flagged, and every fix enables RLS, not disables it. INSERT policies check ownership in WITH CHECK. UPDATE policies check ownership in both USING and WITH CHECK, so a row can't be handed to another user. Anything with USING (true) or WITH CHECK (true) on private data is flagged. Nothing changes until you say so.
Why, with sources
- Supabase: an INSERT policy's
with checkstops a user creating "a row that belongs to someone else". Supabase's UPDATE example puts(select auth.uid()) = user_idin bothusingandwith check. Views bypass RLS unless they're created withsecurity_invoker = true(Postgres 15+).raw_user_meta_data"can be updated by the authenticated user". All from Supabase: Row Level Security. - Postgres: "An INSERT policy cannot have a USING expression". For UPDATE, "If only a USING clause is specified, then that clause will be used for both USING and WITH CHECK cases" (PostgreSQL: CREATE POLICY). So a USING-only UPDATE policy already blocks moving a row to another user. The real risks are
USING (true)andWITH CHECK (true). We tested this on a throwaway local Postgres 17 (see the note at the top). - The built-in advisor flags "RLS disabled in public" and "RLS references user metadata" (Supabase: Database Advisors).
- OWASP's top web risk is broken access control: "Except for public resources, deny by default" (OWASP Top 10:2025 A01).
- More: our Supabase RLS guide, empty array with RLS on, new row violates RLS.
Prompt 2 of 8
The Oct 30 Supabase GRANTs
Purpose: make sure tables created on or after Oct 30, 2026 still work through the Data API, without opening them to everyone.
Security check: Supabase table grants (the October 30, 2026 change). Report first, change nothing yet.
Ground rules:
- Do not disable RLS on any table, even temporarily, even to fix a "42501 permission denied" error.
- Never use a Supabase secret key (sb_secret_...) or service_role key in client-side code.
- Show me every proposed change (SQL migration or code diff) and wait for my OK before applying anything.
- If you're not sure about something, say so instead of guessing.
Context: from October 30, 2026, new tables in the public schema on existing Supabase projects are no longer exposed to the Data API automatically. Each new table needs explicit GRANT statements, or queries fail with 42501 "permission denied for table".
Steps:
1. List my migrations and every CREATE TABLE in the public schema. For each table, check whether the same migration has: (a) GRANTs to the roles that need them, (b) ALTER TABLE ... ENABLE ROW LEVEL SECURITY, (c) at least one policy.
2. Run this read-only query and show me which tables anon and authenticated can reach:
select c.relname, c.relrowsecurity as rls_enabled,
has_table_privilege('anon', c.oid, 'select') as anon_select,
has_table_privilege('authenticated', c.oid, 'select') as auth_select
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind in ('r','p');
3. For each table my app reads or writes through the Data API, propose a migration in this order: GRANT only the privileges my app actually uses (don't grant anon anything unless the data is meant to be public), then ENABLE ROW LEVEL SECURITY, then policies.
4. From now on, whenever you create a table, put the grants, RLS and policies in the same migration.
5. Wait for my OK before applying anything.A good result looks like: each table gets one migration with grant, RLS and policy together. Grants are as narrow as the app needs, with no anon grants on private data. The AI never suggests "disable RLS" or "use the service role key" as the fix for a 42501 error.
Why, with sources
- Supabase's changelog: from October 30, 2026 the new behavior applies to existing projects. It says to add grants "alongside your ENABLE ROW LEVEL SECURITY and policy statements in the same migration", and that AI tools need their prompt updated to include the grants step (Supabase changelog).
- The 42501 error and its hint ("GRANT SELECT ON public.your_table TO anon") come from Supabase: Database API 42501 errors.
- Grants are checked first, then RLS (Supabase: API keys).
- More: our Oct 30 guide, fix: 42501 permission denied.
Prompt 3 of 8
Secrets in frontend bundles
Purpose: prove no secret key ended up in the JavaScript your users download.
Security check: secrets in my frontend bundle. Report first, change nothing yet.
Ground rules:
- Do not disable RLS on any table.
- Never use a Supabase secret key (sb_secret_...), service_role key, or any AI/payment provider secret key in client-side code, or in VITE_, NEXT_PUBLIC_ (or similar public) environment variables.
- Show me every proposed change and wait for my OK before applying anything.
- If you're not sure about something, say so instead of guessing.
Steps:
1. List every environment variable my app uses. Mark which are exposed to the browser (for example VITE_ or NEXT_PUBLIC_ prefixes) and which are server-only.
2. Flag any public variable that holds a secret: Supabase sb_secret_ or service_role keys, AI provider keys, payment secret keys, database URLs with passwords.
3. Run a production build, then search the BUILD OUTPUT (not just the source) for key patterns, for example:
grep -rnoE 'sb_secret_[A-Za-z0-9_-]+' dist/
and for legacy Supabase JWT keys, decode the middle part and check whether the role is service_role (anon is expected).
Adjust dist/ to my build folder.
4. Search the code for secret keys hard-coded in client components, and check .env files are in .gitignore.
5. For each problem, propose: move the call to a server route or server function, rename the variable without the public prefix, rebuild and search again. Remind me that any key that was ever in a bundle, a commit or a log must be rotated, not just moved.
6. Wait for my OK before applying anything.A good result looks like: a list of every env var, marked public or server-only. A search of the built files (not just the source) that finds no secret keys. Any key that was exposed comes with a reminder to rotate it. The publishable key showing up in the bundle is expected and fine.
Why, with sources
- Vite: "Variables prefixed with VITE_ will be exposed in client-side source code after Vite bundling" (Vite: Env variables).
- Next.js inlines variables prefixed with
NEXT_PUBLIC_into the browser bundle (Next.js: Environment variables). - Supabase: secret keys must never go in "a browser, a shipped application, or source control". Server-only variables should never get these prefixes, "or your bundler will ship the key" (Supabase: API keys).
- More: fix: secret key in frontend bundle (commands we tested on a demo build), is the publishable key safe?.
Prompt 4 of 8
Provider keys: rotation and spend limits
Purpose: keep AI provider keys server-side, make rotation painless, and get a checklist for spend caps and alerts.
Security check: AI provider API keys (OpenAI, Anthropic, Google Gemini / Vertex AI, or others). Report first, change nothing yet.
Ground rules:
- Do not disable RLS on any table.
- Never put a provider API key, Supabase secret key (sb_secret_...) or service_role key in client-side code or in VITE_ / NEXT_PUBLIC_ variables.
- Show me every proposed change and wait for my OK before applying anything.
- You can't see my provider dashboards. Don't guess menu names: say "look for ..." if you aren't sure.
Steps:
1. Find every place my code calls an AI provider. For each, tell me: which key or env var it uses, and whether the call runs on the server or in the browser.
2. Flag any provider call made from the browser, any hard-coded key, and any key in a public env var.
3. Propose moving browser calls to a server route that checks the user is signed in, and reading each key from a single server-only env var so rotating it is a config change, not a code change.
4. Suggest separate keys per app and environment (dev, preview, production) so usage can be traced and a leak can be rotated without breaking everything.
5. Give me a short checklist to do by hand in each provider's dashboard: create a new key, deploy it, revoke the old key once the new one works, set a spend limit or budget alert, and check usage for activity I don't recognize. Mark which steps you aren't sure of.
6. Wait for my OK before changing code.A good result looks like: a map of every AI call and where it runs, with no keys in browser code. One server-only env var per key, so you can rotate by changing config. A manual checklist that admits what the AI can't see.
Why, with sources
- OWASP: "Configure spending limits for all service providers/API integrations". Where that isn't possible, "billing alerts should be configured instead" (OWASP API4:2023).
- OpenAI: create the replacement key, update your apps, then revoke the old one (OpenAI: production best practices). Project and organization spend limits can enforce a hard limit; "Spend alerts do not enforce a cap" (OpenAI: spend limits).
- Anthropic: disable (reversible) or delete keys on the API keys page (Anthropic: authentication). Workspace spend limits and alerts (Anthropic: workspaces).
- Google: the Gemini API's leaked-key steps are new key, deploy, disable the old key, audit usage (Gemini API keys). Cloud Billing spend caps cover the Gemini API and Agent Platform (formerly Vertex AI) (Cloud Billing spend caps).
- None of these providers offers a hard per-key spend cap in the docs we checked. Caps are per project, workspace or organization.
- More: fix: AI provider key leaked (exact dashboard steps per provider).
Prompt 5 of 8
Auth and redirects
Purpose: close open redirects, tighten the redirect allow-list, and make sure protected pages are checked on the server.
Security check: authentication and redirects. Report first, change nothing yet.
Ground rules:
- Do not disable RLS on any table.
- Never use a Supabase secret key (sb_secret_...) or service_role key in client-side code.
- Show me every proposed change and wait for my OK before applying anything.
- If you're not sure about something, say so instead of guessing.
Steps:
1. Find every redirect in my app: auth redirectTo / emailRedirectTo values, and any ?next=, ?redirect=, ?returnTo= or similar parameter used after login, logout or sign-up.
2. For each one, tell me whether a user-supplied value can send someone to another site. Flag full URLs taken from the query string, and checks that only use startsWith or string matching.
3. Propose a fix: allow only same-site relative paths, or parse the URL and compare the scheme, host and port against an explicit allow-list. Reject anything else and fall back to a safe default page.
4. Give me a checklist for my auth provider's settings: the Site URL is my production URL (not localhost); the redirect allow-list uses exact production URLs, with wildcards only where I need them for local development or previews.
5. List every page and API route that should require sign-in or a role. Check that each one is enforced on the server (or by RLS), not only by hiding buttons in the UI. Flag any route that trusts a role or user ID sent by the client.
6. Wait for my OK before applying anything.A good result looks like: every redirect is listed, and none accepts an arbitrary external URL. URLs are checked with a parser and an allow-list, not string matching. You get a short settings checklist. Every protected route is checked on the server, not just hidden in the UI.
Why, with sources
- OWASP: if external destinations are needed, "allow-list explicitly approved destinations". Compare the parsed "scheme, canonical host, and effective port against an explicit allowlist" (OWASP: Unvalidated Redirects and Forwards).
- Supabase:
redirectTo"should match the Redirect URLs list". Change the Site URL from localhost to production. For production, set "the exact redirect URL path" rather than**(Supabase: Redirect URLs). - OWASP: access control "is only effective when implemented in trusted server-side code or serverless APIs" (OWASP Top 10:2025 A01).
- More: the admin-roles section of our RLS guide.
Prompt 6 of 8SQL untested
Storage bucket policies
Purpose: make sure private files are private, and that uploads, overwrites and deletes are limited to the right users.
Security check: file storage buckets and policies (Supabase Storage). Report first, change nothing yet.
Ground rules:
- Do not disable RLS on any table, including storage.objects.
- Never use a Supabase secret key (sb_secret_...) or service_role key in client-side code to get around a storage error.
- Show me every proposed change and wait for my OK before applying anything.
- If you're not sure about something, say so instead of guessing.
Steps:
1. List every bucket with: public or private, file size limit, allowed file types. (Read-only, for example:
select id, name, public, file_size_limit, allowed_mime_types from storage.buckets;)
2. List every policy on storage.objects (read-only, for example:
select policyname, cmd, roles, qual, with_check from pg_policies where schemaname = 'storage' and tablename = 'objects';)
3. Flag: public buckets that hold anything personal or private (anyone with the URL can download from a public bucket); policies that let any signed-in user read, overwrite or delete other users' files; uploads with no size or type limit.
4. Propose fixes: make sensitive buckets private and serve files with short-lived signed URLs; restrict policies to the user's own folder (for example the first folder in the path equals auth.uid()); add size and file type limits.
5. Wait for my OK before applying anything.A good result looks like: every bucket is listed as public or private, with a reason. Private user files sit in private buckets, and each user's policies cover only their own folder. Size and type limits are set. Nobody "fixes" an upload error with the service key.
Why, with sources
- Supabase: in a public bucket, "anyone who possesses the asset URL can readily access the file", though access control still applies to uploads, deletes, moves and copies. Private buckets are the default, served by authorized download or signed URLs (Supabase: Storage buckets).
- Storage "does not allow any uploads to buckets without RLS policies" on
storage.objects. Upsert also needs SELECT and UPDATE. The service key bypasses RLS and is only for trusted servers. All from Supabase: Storage access control. - Bucket columns (
public,file_size_limit,allowed_mime_types) come from Supabase: Storage schema. - More: fix: new row violates RLS (covers the storage upload error).
Prompt 7 of 8
Input validation and rate limits
Purpose: validate inputs on the server, and stop one user or bot from running up costs or flooding your app.
Security check: input validation and rate limits. Report first, change nothing yet.
Ground rules:
- Do not disable RLS on any table.
- Never use a Supabase secret key (sb_secret_...) or service_role key in client-side code.
- Show me every proposed change and wait for my OK before applying anything.
- If you're not sure about something, say so instead of guessing.
Steps:
1. List every place my app accepts input: forms, API routes, server functions, webhooks, file uploads.
2. For each, tell me whether the input is validated ON THE SERVER (type, length, format, allowed values), not only in the browser. Flag raw SQL built from strings and HTML rendered from user input without escaping.
3. Propose server-side validation using a schema (allow-list what's accepted; don't try to block "bad" strings), with request size limits.
4. List endpoints that cost money or are easy to abuse: AI calls, email/SMS sending, sign-up, password reset, file uploads, search, exports. For each, say whether there's a rate limit, a per-user cap, a max page size or a max file size.
5. Propose limits that fit my stack (per user and per IP), and caps on AI requests (for example max tokens per request and per user per day).
6. Wait for my OK before applying anything.A good result looks like: a full list of inputs, each validated on the server against a schema. A list of the expensive endpoints, each with a limit. Anything left unprotected is called out.
Why, with sources
- OWASP: "Client-side validation is bypassable". Validate on the server. Define what you accept rather than trying "to recognize every malicious string". Apply request size limits before parsing (OWASP: Input Validation Cheat Sheet).
- OWASP lists missing limits (timeouts, upload size, records per page, third-party spend) as a top API risk (OWASP API4:2023).
- Supabase Auth already rate-limits its own endpoints (sign-ups, OTPs, password resets, emails) and returns 429 when a limit is hit. Your own API routes and functions are not covered by those limits (Supabase: Auth rate limits).
Prompt 8 of 8
Dependencies and slopsquatting
Purpose: catch packages that don't exist, have look-alike names, or have known vulnerabilities, before they land in your app.
Security check: dependencies, including packages an AI may have made up ("slopsquatting"). Report first, change nothing yet.
Ground rules:
- Do not disable RLS on any table.
- Never use a Supabase secret key (sb_secret_...) or service_role key in client-side code.
- Do not install, add, remove or upgrade any package until I approve the list.
- Show me every proposed change and wait for my OK.
- If you're not sure a package is real, say so. Don't guess.
Steps:
1. List every direct dependency (package.json, requirements.txt, pyproject.toml, or similar). For each, check that it exists in the official registry (for npm: npm view <name>; for Python: its page on pypi.org) and that it's the package I meant: right name, a real maintainer and repository, and download history that matches what I'd expect.
2. Flag names that are one letter off from a popular package, packages that are very new or barely used, and any package you suggested that you can't confirm exists.
3. Check there's a lockfile (package-lock.json or equivalent) and that it's committed. For CI and deploys, recommend npm ci (it installs exactly what the lockfile says and fails if it doesn't match package.json). For pip, mention hash-checking mode (--require-hashes).
4. Run npm audit (and npm audit signatures) or the equivalent for my stack, and summarize what's serious and what's noise.
5. List packages with install scripts. Don't approve them in allowScripts, and don't use --dangerously-allow-all-scripts, without showing me why each one needs them.
6. Wait for my OK before changing anything.A good result looks like: every dependency is confirmed real and correctly named, and anything that can't be confirmed is flagged. A lockfile is committed, and CI uses npm ci. Audit results are explained in plain English. Nothing is installed until you approve the list.
Why, with sources
- OWASP's 2025 LLM risk list describes attackers who find "commonly hallucinated package names" from coding assistants and publish malicious packages under them (OWASP LLM09:2025 Misinformation).
- npm describes typosquatting, where someone registers "a package with a similar name to a popular package" (npm: Threats and mitigations).
npm cirequires an existingpackage-lock.json, and fails rather than updating it when it doesn't matchpackage.json(npm 12: npm ci). The same page covers theallowScriptsinstall-script policy and--dangerously-allow-all-scripts, whose "use is strongly discouraged".npm auditchecks for "known vulnerabilities", andnpm audit signaturesverifies registry signatures and provenance (npm 12: npm audit).- pip's hash-checking mode (
--require-hashes) protects "against remote tampering" (pip: Secure installs). - "Slopsquatting" is a community term, not an official one. The OWASP page above describes the attack without using the word.
Rules file
Cursor project rule: builtsafely-security.mdc
Save it in your project's .cursor/rules/ folder. Download builtsafely-security.mdc
---
description: Security guardrails for AI-built web apps (Supabase RLS and grants, secret keys, provider keys, redirects, storage, input validation, dependencies). Always applied.
alwaysApply: true
---
# Security guardrails
These rules guide the agent; they are not an enforcement layer. Review every change yourself.
## Always
- Show proposed database migrations and code diffs before applying them. Wait for explicit approval for anything that changes the schema, policies, grants, auth settings, storage policies, environment variables or dependencies.
- Say when you're unsure. Never invent package names, dashboard menu labels or API options.
## Database (Supabase / Postgres)
- Never disable Row Level Security on any table (including `storage.objects`), not even temporarily or to fix an error.
- When creating a table in `public`, put these in the same migration, in this order: `GRANT` only the privileges the app uses, `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`, then policies.
- Don't grant `anon` access unless the data is meant to be public.
- INSERT policies check ownership in `WITH CHECK` (INSERT takes no `USING`). UPDATE policies on owned rows use `USING` and `WITH CHECK` both set to `(select auth.uid()) = user_id`, so `user_id` can't be changed to someone else's. Add a different `WITH CHECK` only when the new-row rule must differ from `USING`.
- Never use `USING (true)` or `WITH CHECK (true)` on private data. `WITH CHECK (true)` loosens the new-row check.
- Don't base authorization on `user_metadata` / `raw_user_meta_data`; users can edit it.
- Create views with `security_invoker = true` so RLS applies to them.
- Treat `42501 permission denied` as a missing grant. Fix it with a narrow `GRANT`, never by disabling RLS or switching to a secret key.
## Keys and secrets
- Never use a Supabase secret key (`sb_secret_...`) or legacy `service_role` key in client-side code.
- Never put secrets in browser-exposed env vars (`VITE_`, `NEXT_PUBLIC_` or similar). Only the Supabase URL and publishable key may be public.
- Call AI and payment providers only from server code. Read each key from one server-only env var so rotating it is a config change.
- Never hard-code keys, and never print them in logs or error messages. Keep `.env` files out of git.
## Auth and redirects
- Accept only same-site relative paths for `next` / `redirect` / `returnTo` parameters, or parse the URL and check scheme, host and port against an allow-list. Never redirect to an unchecked user-supplied URL.
- Enforce sign-in and roles on the server or with RLS. Hiding UI is not access control. Never trust a role or user ID sent by the client.
## Storage
- Keep user files in private buckets and serve them with short-lived signed URLs.
- Restrict `storage.objects` policies to the user's own folder. Set file size and type limits on buckets.
## Input and abuse
- Validate all input on the server with a schema (allow-list what's accepted), with length and size limits.
- Add rate limits or per-user caps to endpoints that cost money or send messages: AI calls, email/SMS, sign-up, password reset, uploads, exports.
## Dependencies
- Before adding a package, confirm it exists in the official registry and is the one intended (check the name, maintainer and repository). Flag look-alike names.
- Keep the lockfile committed. Use `npm ci` in CI.
- Don't approve dependency install scripts or use `--dangerously-allow-all-scripts` without explaining why.
## References
- BuiltSafely guides: /guides/supabase-rls/, /guides/supabase-oct30-grants/, /fixes/secret-key-in-frontend-bundle/, /fixes/ai-provider-key-leaked/
- Supabase RLS: https://supabase.com/docs/guides/database/postgres/row-level-security
- Supabase API keys: https://supabase.com/docs/guides/api/api-keys
- Postgres CREATE POLICY (USING vs WITH CHECK): https://www.postgresql.org/docs/current/sql-createpolicy.html
Rules file
Claude Code rules: CLAUDE.md
Copy the contents into your project's CLAUDE.md (or .claude/CLAUDE.md). Download CLAUDE.md
<!-- BuiltSafely security rules for Claude Code. Copy into ./CLAUDE.md or ./.claude/CLAUDE.md in your project. This comment is stripped before Claude sees the file. CLAUDE.md is guidance, not enforcement: use hooks or permissions settings for hard blocks. Draft, last checked Oct 18, 2026. -->
# Security rules
## Working style
- Show proposed migrations and code diffs before applying them. Wait for my approval before changing the schema, RLS policies, grants, auth settings, storage policies, env vars or dependencies.
- If you're unsure, say so. Never invent package names, dashboard menu labels or API options.
## Database (Supabase / Postgres)
- Never disable Row Level Security on any table, including `storage.objects`, not even temporarily or to fix an error.
- New `public` tables: one migration with, in order, `GRANT` (only the privileges the app uses), `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`, then policies.
- No `anon` grants unless the data is meant to be public.
- INSERT policies check ownership in `WITH CHECK` (INSERT takes no `USING`), e.g. `(select auth.uid()) = user_id`.
- UPDATE policies on owned rows: `USING` and `WITH CHECK` both `(select auth.uid()) = user_id`, so `user_id` can't be changed to someone else's. Use a different `WITH CHECK` only when the new-row rule must differ.
- No `USING (true)` or `WITH CHECK (true)` on private data.
- Never base authorization on `user_metadata` / `raw_user_meta_data`; users can edit it.
- Views use `with (security_invoker = true)`.
- `42501 permission denied for table` means a missing grant. Fix it with a narrow `GRANT`, never by disabling RLS or switching to a secret key.
## Keys and secrets
- Never use a Supabase secret key (`sb_secret_...`) or `service_role` key in client-side code.
- No secrets in `VITE_`, `NEXT_PUBLIC_` or other browser-exposed env vars. Only the Supabase URL and publishable key may be public.
- Call AI and payment providers from server code only. One server-only env var per key, so rotation is a config change.
- Never hard-code keys, and never log them. Keep `.env*` files out of git.
- After a production build, search the build output for `sb_secret_` and for legacy JWTs whose role is `service_role`.
## Auth and redirects
- `next` / `redirect` / `returnTo` values: allow same-site relative paths only, or parse the URL and check scheme, host and port against an allow-list.
- Enforce sign-in and roles on the server or in RLS. Never trust a role or user ID sent by the client.
## Storage
- User files go in private buckets and are served with short-lived signed URLs.
- `storage.objects` policies are limited to the user's own folder. Buckets have size and type limits.
## Input and abuse
- Validate input on the server with a schema (allow-list), with length and size limits.
- Rate-limit or cap endpoints that cost money or send messages: AI calls, email/SMS, sign-up, password reset, uploads, exports.
## Dependencies
- Before adding a package, confirm it exists in the official registry and is the intended one (name, maintainer, repository). Flag look-alike names.
- Commit the lockfile. CI uses `npm ci`.
- Don't approve dependency install scripts or use `--dangerously-allow-all-scripts` without explaining why.
## Background
- Supabase RLS: https://supabase.com/docs/guides/database/postgres/row-level-security
- Supabase API keys: https://supabase.com/docs/guides/api/api-keys
- Postgres CREATE POLICY (USING vs WITH CHECK): https://www.postgresql.org/docs/current/sql-createpolicy.html
- Oct 30, 2026 grants change: https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically
Limits, said plainly
- An AI check is a second pair of eyes, not a guarantee. AI tools can be confidently wrong, and OWASP lists "overreliance" on LLM output as a risk (OWASP LLM09:2025).
- Rules files guide the AI; they don't enforce anything. Cursor's docs say AI guidance "should not be your only security control" (Cursor: Rules). Claude Code's docs say CLAUDE.md is "not a hard enforcement layer" and suggest hooks or settings for hard blocks (Claude Code: memory).
- Only run these on projects you own or are authorized to work on.
Your turn
Which prompt found the biggest surprise in your project? Tell us, and tell us which check you'd add as prompt 9.
This prompt pack is educational. It isn't a security audit, and results depend on your tool and your app. Only run these on projects you own or are authorized to work on. Disclaimer.