AnuWorkWriting
Available
Ask me anything

Ask anything about Anu —
his work, skills, or experience.

Ask me anything

How I Set Up a SECURITY.md When Building With AI Agents

March 2026·8 min read

AI coding agents are incredible. They can scaffold features, write migrations, wire up API routes, and refactor entire modules in seconds. But they also move fast enough to introduce security holes before you notice – hardcoded keys, overly permissive database policies, service role clients exposed to the browser.

When I started building production apps with AI agents (Cursor, Claude Code, Copilot), I realised I needed guardrails that worked before code review – not after. The solution was a SECURITY.md file at the root of the repo that acts as a contract between me and any agent writing code in the project.

Here's the template I use, and the thinking behind each section.

Why a SECURITY.md?

AI agents read your repo. They index your files, scan your project structure, and use that context to generate code. A SECURITY.md gives agents explicit rules about what they must and must not do – in plain language they can parse and follow.

Think of it as a system prompt for your codebase. It doesn't replace proper security practices, but it catches the most common mistakes at the point of generation – before they ever reach a pull request.

The agent rules

No secrets in code

This is rule number one. Never commit service role keys, JWT secrets, database passwords, or API keys. Client-side environment variables must be limited to NEXT_PUBLIC_* and must be non-sensitive. AI agents love to helpfully inline a key “to get things working” – this rule stops that.

Assume public keys are public

Anyone can extract client-side keys from a web app. All data protection must be enforced via Row Level Security (RLS), safe RPCs, and server-side checks. If an agent writes a client-side query that assumes the anon key provides access control, the policy has already failed.

Service role usage is restricted

Service role keys may only be used in Next.js Route Handlers (src/app/api/**), Edge Functions, or trusted backend workers. Never in the browser. This is the single most common mistake AI agents make when working with Supabase – they reach for the service role client because it “just works” without RLS getting in the way.

RLS is mandatory

Any public or semi-public table must have RLS enabled. No permissive write policies like WITH CHECK (true) for INSERT, UPDATE, or DELETE. AI agents will sometimes generate these to “unblock” a feature – the security file makes it clear that's not acceptable.

Server endpoints for public writes

Any endpoint that records analytics, clicks, or user actions must validate input (e.g. with Zod), apply rate limits, and use service-role DB writes on the server. Client components must not call write RPCs directly. This pattern prevents the entire class of “unauthenticated write” vulnerabilities.

Function hardening

All SECURITY DEFINER functions must have a fixed search_path and restricted execution grants. Only grant EXECUTE to roles that actually need it – often service_role only.

Pre-merge checklist

Rules only work if you verify them. The security file includes a merge checklist that I run before every PR lands:

  • Repository scan – search for leaked service role keys, JWTs, and tokens
  • Database advisors – run security and performance advisors after any DB, RLS, or function change
  • Verify public write paths – confirm no public tables allow anonymous inserts without validation
  • Confirm all public write actions route through a server endpoint with rate limiting

Enforced architecture

Server routes for privileged writes

Direct writes to protected tables from client code are not allowed. All privileged writes must go through Next.js Route Handlers that validate input, rate limit, authenticate via JWT, and use the service role for DB writes. This is the backbone of the architecture – it means an AI agent can't accidentally expose a write path by generating a client-side Supabase call.

Anonymous activity stays local

Anonymous activity must remain local until login. Cloud state for user data – watchlists, recently viewed items, watch progress – is authenticated-only. This prevents a whole category of bugs where an agent creates a “save to cloud” feature that accidentally works without auth.

The template

Here's the full template I drop into every production repo. Adapt it to your stack – the specific tools (Supabase, Next.js, Zod) are less important than the patterns (server-only writes, mandatory RLS, no secrets in code).

SECURITY.md
1# Security Template
2
3This repository handles production user data and privileged database access. Treat security requirements as **non-optional**.
4
5## Agent Rules (MUST FOLLOW)
6
7- **No secrets in code**
8 - Never commit: Service role keys, JWT secrets, database passwords, API keys.
9 - Client-side env vars must be limited to `NEXT_PUBLIC_*` and must be non-sensitive.
10
11- **Assume public keys are public**
12 - Anyone can extract client-side keys from a web app.
13 - All data protection must be enforced via **RLS**, safe RPCs, and server-side checks.
14
15- **Service role usage**
16 - Service role keys may only be used in:
17 - Next.js Route Handlers (`src/app/api/**`)
18 - Edge Functions or serverless functions
19 - Trusted backend workers
20 - Never expose service role keys to the browser.
21
22- **RLS is mandatory**
23 - Any public or semi-public table must have RLS enabled.
24 - Avoid permissive write policies like `WITH CHECK (true)` for `INSERT/UPDATE/DELETE`.
25
26- **Prefer server endpoints for public write actions**
27 - Any endpoint that records analytics/clicks must:
28 - Validate input (e.g. `zod`)
29 - Apply rate limits
30 - Use service-role DB writes on the server
31 - Client components must not call write RPCs directly. Use a server route.
32
33- **Function hardening**
34 - All security definer functions must have a fixed `search_path`:
35 ```sql
36 SET search_path = public, extensions
37 ```
38 - Restrict function execution grants:
39 - Only grant `EXECUTE` to roles that need it (often `service_role` only).
40
41- **Do not increase attack surface**
42 - Do not attach privileged objects to `window` unless strictly required.
43 - Avoid broad CORS (`*`) except where explicitly intended and reviewed.
44
45## Required Checks Before Merge
46
47- **Repository scan**
48 - Search for leaked keys:
49 - Service role keys
50 - JWTs (`eyJ...`)
51 - Any secret keys or tokens
52
53- **Database advisors** (required after any DB/RLS/function change)
54 - Run:
55 - Security advisor
56 - Performance advisor
57
58- **Verify public write paths**
59 - Confirm no public tables allow anonymous inserts without validation/rate limiting.
60 - Confirm any public write action is routed through a server endpoint.
61
62## Patterns To Use
63
64- **Server-side click tracking**
65 - Use a Next API route with rate limiting and service-role DB writes.
66 - Example endpoints:
67 - `/api/ad-click` -> calls `record_ad_click(...)` via service role
68 - `/api/affiliate-click` -> calls `record_affiliate_movie_click(...)` via service role
69
70- **Restrict click-tracking RPC execution**
71 - Click tracking functions must be `service_role`-only.
72 - Never grant these functions to `anon` or `authenticated`.
73
74- **Public analytics**
75 - Prefer a server endpoint that accepts sanitized payloads and enforces bot checks.
76
77- **Admin-only RPC actions**
78 - Admin endpoints must use service role client and proper authorization.
79 - Avoid using the anon client inside admin routes.
80
81## Enforced Architecture
82
83### Option A (Enforced): Server routes for any privileged writes
84
85- Direct writes to protected tables from client code are not allowed.
86- All privileged writes must go through Next.js Route Handlers under `src/app/api/**`.
87- Server routes must:
88 - Validate input (e.g. `zod`)
89 - Rate limit
90 - Authenticate via `Authorization: Bearer <JWT>` (enterprise/stateless)
91 - Use service role for DB writes
92
93### Option B (Enforced): Anonymous activity is local-only
94
95- Anonymous activity must remain local until login.
96- Cloud state for user data is **authenticated-only**:
97 - User watchlists
98 - Recently viewed items
99 - Watch progress
100 - Any user-specific data
101
102## Key Implementation Points
103
104- **Authorization header helpers**
105 - Use `authFetch` for user-authenticated endpoints.
106 - Use `apiFetch` for management portal calls.
107
108- **Admin endpoints**
109 - All `/api/admin/**` endpoints must use proper authorization and service role client.
110
111- **Management interfaces**
112 - Management UI must use server endpoints for all privileged operations.
113
114## Database Hardening Rules (Enforced)
115
116- **Protected tables** (examples):
117 - Admin tables
118 - Push tokens
119 - User data tables
120 - Any sensitive business data
121
122- **RLS and grants**
123 - Admin tables are service-role only (no `anon`/`authenticated` table privileges).
124 - Public tables allow read-only access where appropriate.
125 - Table writes are service-role only (via server endpoints).
126
127- **Function grants + hardening**
128 - Sensitive RPCs are `service_role` execute only:
129 - User registration functions
130 - Click tracking functions
131 - Admin functions
132 - User data retrieval functions
133 - SECURITY DEFINER functions must have fixed `search_path`.
134
135## Incident Response (If a key is exposed)
136
137- Rotate impacted keys immediately.
138- Audit RLS policies and function grants.
139- Review logs for unusual access patterns.
140- Add a regression test/checklist entry to prevent recurrence.

Why this works

AI agents are context-driven. They read your files, follow your patterns, and generate code that matches the style of your repo. A security file doesn't guarantee perfect output – but it dramatically reduces the chance of an agent generating an insecure pattern in the first place.

The key insight is that this isn't just documentation for humans. It's a directive for machines. When an agent sees “service role keys may only be used in Route Handlers,” it follows that rule. When it sees “RLS is mandatory,” it generates policies instead of skipping them.

The file also serves as a shared contract between you and anyone else on the team. It's the source of truth for “how we handle security here” – whether the code is written by a person or a model.

If you're shipping production apps with AI agents, a security file isn't optional. It's the cheapest, highest-leverage safety net you can add.

SecurityAI AgentsDeveloper ExperienceSupabaseNext.js