Turn your best prompt into a skill: a reusable Supabase security audit for Claude Code

We took a one-off security-audit prompt, tightened it, and turned it into a global Claude Code skill you can run in any project with /security-audit. Copy the code — or the prompt to generate it yourself.

AIEngineering#claude-code#supabase#security#skills#ai-coding#rls

If you use an AI coding agent, you have probably written a really good prompt once, used it, and then lost it in your scrollback forever. The next time you need it you half-remember it, retype a worse version, and get a worse result. This is a post about fixing that — using a Supabase security audit as the worked example — and it ends with two things you can copy: the finished skill, and the prompt to generate your own.

The problem: good prompts die in scrollback

We had a solid one-off prompt for auditing a Supabase + Netlify/Next.js project: check RLS, look for unsecured API routes, find leaked secrets, and so on. It worked. But "it worked once, in one project, in one session" is not the same as "we can do this reliably across every project." A prompt living in your chat history is a prompt you will rewrite badly later.

The fix in Claude Code is a skill: a small Markdown file with some frontmatter that becomes a slash command available in every session. Write it once, invoke it forever with /security-audit. The interesting part wasn't the mechanics of creating the file — it was what we learned about hardening the prompt before freezing it in place.

A good prompt isn't a good skill yet

The original prompt covered the obvious ground. But before turning something into a reusable tool, it's worth pressure-testing it, because every weakness gets multiplied by every future run. A few gaps stood out:

  • The service_role key leak. The single most catastrophic Supabase mistake is shipping your service-role key to the browser — in a client bundle, a NEXT_PUBLIC_* var, or an edge function that returns it. The original prompt didn't call this out explicitly. For a security audit, that's the first thing to check, not an afterthought.
  • "RLS enabled" is not "RLS correct." There's a world of difference between a table with RLS enabled and no policies (locked — nothing gets through) and a table with RLS enabled and a policy of USING (true) (wide open — everything leaks). They look almost identical in a schema dump and they are opposite problems. An audit that just reports "RLS is on" is worse than useless because it's falsely reassuring.
  • SECURITY DEFINER functions. An RPC declared SECURITY DEFINER runs as its owner and bypasses RLS entirely. If you only check table policies, these slip straight past you.
  • Storage buckets. "Tables and RPCs" misses Supabase Storage, where public buckets and missing storage policies are a common hole.
  • Honest framing on rate limits. An agent reading code can spot the absence of rate limiting and tell you where to add it. It cannot verify that runtime limits actually fire without live testing. The skill should say so, rather than implying a control is confirmed working when it isn't.

The other big lever was evidence discipline. Without it, security prompts produce a wall of generic advice ("make sure you have RLS enabled"). The fix is a hard rule: every finding must cite file:line, show the offending snippet, rate severity, and give a concrete fix — and if you can't point to real code, say so separately instead of padding the report. That one constraint is the difference between a checklist you ignore and findings you act on.

What the skill looks like

A Claude Code skill is just a folder under ~/.claude/skills/<name>/ containing a SKILL.md. The frontmatter description is what the agent reads to decide when the skill is relevant, so it's worth writing carefully. Here's the shape — drop it in ~/.claude/skills/security-audit/SKILL.md and you'll have /security-audit in every project:

---
name: security-audit
description: Read-only security audit of a Supabase + Netlify/Next.js
  project — RLS, RPC/SECURITY DEFINER, Data API grants, storage buckets,
  privilege escalation, server-side auth on writes, leaked secrets/
  service_role keys, rate limiting/abuse, console logging leaks, and
  access control. Produces severity-ranked findings with file:line
  evidence. Read-only. Invoke as /security-audit [project path or area].
---

Audit the current project across its data layer (Supabase) and
app/API layer. READ-ONLY: do not modify code, run migrations, apply
fixes, or call any external API. Report findings only.

## Evidence rules (apply to every finding)

- Cite `file:line`, show the snippet, rate severity
  (critical/high/medium/low), give the concrete fix.
- No theoretical findings. If you can't point to real code, list it
  separately under "Worth checking manually."
- State confidence honestly. If something needs live testing to
  confirm (e.g. runtime rate limits), say so.

The body then lists the audit areas. The data-layer section is where the hard-won detail lives:

### Data layer (Supabase)

1. RLS state per table — classify each public table as:
   (a) RLS disabled → critical;
   (b) RLS enabled, no policies → locked (note it);
   (c) RLS enabled with an over-permissive policy like
       `USING (true)` → critical leak.
   Don't conflate (b) and (c) — opposite problems.
2. RPC / functions — flag every `SECURITY DEFINER` function
   (bypasses RLS, runs as owner) and check it's safe to expose.
3. Data API grants — check new tables grant only the privileges
   they need, not blanket grants.
4. Storage buckets — flag public buckets and missing storage
   RLS policies.
5. Privilege escalation — confirm users can't upgrade their own
   role or read/edit/delete other users' rows.

The app-layer section covers server-side enforcement on every write, the service_role secret check, rate limiting and abuse, and console-logging leaks. Two small but important guardrails sit at the end: never print the value of any secret it finds (reference it by location, flag it for rotation), and for large projects go deep on the data layer first and report before moving on — shallow coverage of everything is worse than thorough coverage of the part that actually holds your data.

Don't want to copy-paste? Generate it.

The better way to own a skill is to have your agent build it, so you can shape it to your stack as it goes. Paste this into Claude Code and it'll create the file for you:

Create a global Claude Code skill at
~/.claude/skills/security-audit/SKILL.md.

It's a READ-ONLY security auditor for a Supabase + Netlify/Next.js
project — it must never modify code, run migrations, apply fixes, or
call external APIs. It reports findings only.

Frontmatter: name `security-audit`, and a description that lists what
it covers so you know when to use it. Invoke as
/security-audit [project path or area].

Evidence rule for EVERY finding: cite file:line, show the snippet,
rate severity (critical/high/medium/low), give a concrete fix. No
theoretical findings — if you can't point to real code, list it
separately under "Worth checking manually." State confidence honestly;
if something needs live testing to confirm (e.g. runtime rate limits),
say so.

Data layer to audit: per-table RLS state, distinguishing RLS-disabled
(critical) vs RLS-enabled-no-policies (locked) vs RLS-enabled-with-
USING(true) (critical leak); SECURITY DEFINER functions (they bypass
RLS); Data API grants scoped to only needed privileges; public storage
buckets and missing storage policies; privilege escalation and access
to other users' rows.

App layer to audit: server-side auth + validation on every write
(POST/PUT/PATCH/DELETE), not just client checks; secrets — especially
SUPABASE_SERVICE_ROLE_KEY — never in client bundles, NEXT_PUBLIC_*
vars, or anything sent to the browser; missing rate limiting / abuse
protection; console logs or error responses leaking tokens, PII, or
stack traces.

Never print the VALUE of any secret it finds — reference it by
location and flag for rotation. For large projects, go deep on the
data layer first and report before moving to the app layer.

Output: severity-ranked findings grouped by area, a checklist
tailored to the project's actual stack, and the top 3–5 immediate
high-priority fixes.

Adjust the stack line if you're not on Supabase/Netlify, and add any failure modes specific to your projects. The point is that the prompt is the spec — once it's good, the skill is just that spec made permanent.

Why a skill beats a saved prompt

You could keep the prompt in a notes file and paste it in. A skill is better for three reasons:

  1. It's always there. Skills in ~/.claude/skills/ are global — every project, every session, no copy-paste. You invoke it by name.
  2. The agent knows when to reach for it. Because the description describes the trigger, the agent can suggest or select it when you ask to "check this project for vulnerabilities," without you remembering the exact command.
  3. It improves in one place. When you hit a new gotcha, you edit one file and every future run gets smarter. Your audits compound instead of resetting to zero each time.

We tested it against a real project and it held up — grounded findings with file references, not boilerplate.

The transferable takeaway

The security content here is genuinely useful if you run on Supabase, and you should feel free to lift either the code or the prompt above. But the bigger point is the pattern: when a prompt earns its keep, don't just save it — harden it and promote it to a tool. Pressure-test it for the failure modes that matter (for us: the service-role leak, the RLS "enabled vs correct" trap, SECURITY DEFINER). Add an evidence rule so the output is actionable. Then drop it in ~/.claude/skills/ so it's one slash command away, forever, in every repo you touch.

The best prompt you wrote last month shouldn't be lost in your scrollback. Make it a skill.

Enjoyed this? Get Hurricane Signal

Field notes on building real things with LLMs — occasional, practical, no hype.