Give your agent house rules
Security rules for an AI-built app
A coding agent with no house rules invents its own every session, which is why the same handful of failures reach production over and over. These are 11 rules you can commit and point it at. Each one names the check that tells you whether you followed it, so none of this is advice you cannot verify.
The file
Save it as SECURITY-RULES.md, or paste it into whatever instruction file your tool already reads. Then run a scan and see which of them your deployment actually keeps.
# Security rules for this project
Rules for whoever or whatever writes code here, including coding agents. Each one names the
check that will tell you whether you followed it, so none of this is advice you cannot verify.
Source: https://xlogs.com/rules
## 1. Never put a secret in code that ships to the browser
Any key that can spend money, read a database as an owner, or act as a user belongs on the server. If a value is read by client-side code, treat it as public: it is in the bundle, and the bundle is downloadable by anyone. A key prefixed for public use (a Supabase anon key, a publishable payment key) is fine there; a service-role, secret or live key is not.
**How to check:** Search the built bundle, not the source: a key removed from source but still in a deployed asset is still leaked.
## 2. Turn row level security on for every table, then write the policy
Create no table without enabling row level security in the same migration. Write a policy that names the row's owner (auth.uid() = user_id), not one that checks only that somebody is signed in: every signed-in user is still every other user's stranger. A table of genuinely public reference data may use a permissive policy, and that should be a deliberate line somebody can point at.
**How to check:** Ask the table for rows as a logged-out stranger. If rows come back and they are not meant to be public, the policy is wrong.
## 3. Decide permission on the server, every time
A check that runs in the browser is a suggestion. Hiding a button, redirecting in a component, or reading a role in client code stops nobody who opens the network tab. Every route that returns or changes data must re-check who is asking, on the server, on every request.
**How to check:** Call the endpoint directly with no session and with another user's session. Both must be refused.
## 4. Check ownership of the specific record, not just that someone is logged in
An authenticated user is not automatically entitled to the row whose id they typed. Every read, update and delete of a record must confirm that this user owns THAT record. This is the single most common data leak in an app that otherwise has working login.
**How to check:** Sign in as one user and request another user's record by id. It must be refused, not returned.
## 5. Do not serve source maps from production
A source map hands a reader your original source, comments and structure. Generate them for your error tracker if you need them, and upload them there rather than serving them beside the bundle.
**How to check:** Request the .map file beside your main bundle. It should not be served.
## 6. Never serve configuration, environment or version-control files
Files like .env, .git, backups and database dumps must not be reachable over HTTP. This usually happens by accident, when a config file lands in the folder the server publishes.
**How to check:** Request each one directly. A 404 is right; a 200 with real content is an emergency.
## 7. Set the protective response headers
Set Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options (or CSP frame-ancestors) and Referrer-Policy. None of them fixes a broken permission check; each one removes a class of attack that does not need one.
**How to check:** Read the headers on your homepage response.
## 8. Mark session cookies HttpOnly, Secure and SameSite
A session cookie readable by JavaScript is a session stealable by any script on the page, including one you did not write. Set HttpOnly and Secure, and SameSite=Lax or Strict.
**How to check:** Look at the Set-Cookie header on sign-in.
## 9. Remove DNS records when you remove the thing they point at
A CNAME left pointing at a decommissioned host lets whoever claims that host next serve content on your domain. Delete the record when you delete the deployment.
**How to check:** Resolve each subdomain and confirm the target is still yours.
## 10. Load third-party scripts deliberately, and know which ones you load
Every script tag from another domain runs with your page's full permissions and can read anything the user types. Add one only when you can say what it is for, prefer a pinned version over a floating one, and remove the ones nobody remembers adding. A script host that is later compromised becomes your compromise.
**How to check:** List the script origins your homepage loads and account for each one.
## 11. Publish SPF and DMARC, even if you do not send mail
Without them, anyone can send mail that claims to come from your domain. A domain that sends no mail should publish records that say exactly that.
**How to check:** Look up the TXT records for your apex and for _dmarc.
The rules, and how each one is checked
1. Never put a secret in code that ships to the browser
Any key that can spend money, read a database as an owner, or act as a user belongs on the server. If a value is read by client-side code, treat it as public: it is in the bundle, and the bundle is downloadable by anyone. A key prefixed for public use (a Supabase anon key, a publishable payment key) is fine there; a service-role, secret or live key is not.
How to check: Search the built bundle, not the source: a key removed from source but still in a deployed asset is still leaked.
2. Turn row level security on for every table, then write the policy
Create no table without enabling row level security in the same migration. Write a policy that names the row's owner (auth.uid() = user_id), not one that checks only that somebody is signed in: every signed-in user is still every other user's stranger. A table of genuinely public reference data may use a permissive policy, and that should be a deliberate line somebody can point at.
How to check: Ask the table for rows as a logged-out stranger. If rows come back and they are not meant to be public, the policy is wrong.
3. Decide permission on the server, every time
A check that runs in the browser is a suggestion. Hiding a button, redirecting in a component, or reading a role in client code stops nobody who opens the network tab. Every route that returns or changes data must re-check who is asking, on the server, on every request.
How to check: Call the endpoint directly with no session and with another user's session. Both must be refused.
4. Check ownership of the specific record, not just that someone is logged in
An authenticated user is not automatically entitled to the row whose id they typed. Every read, update and delete of a record must confirm that this user owns THAT record. This is the single most common data leak in an app that otherwise has working login.
How to check: Sign in as one user and request another user's record by id. It must be refused, not returned.
5. Do not serve source maps from production
A source map hands a reader your original source, comments and structure. Generate them for your error tracker if you need them, and upload them there rather than serving them beside the bundle.
How to check: Request the .map file beside your main bundle. It should not be served.
6. Never serve configuration, environment or version-control files
Files like .env, .git, backups and database dumps must not be reachable over HTTP. This usually happens by accident, when a config file lands in the folder the server publishes.
How to check: Request each one directly. A 404 is right; a 200 with real content is an emergency.
7. Set the protective response headers
Set Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options (or CSP frame-ancestors) and Referrer-Policy. None of them fixes a broken permission check; each one removes a class of attack that does not need one.
How to check: Read the headers on your homepage response.
8. Mark session cookies HttpOnly, Secure and SameSite
A session cookie readable by JavaScript is a session stealable by any script on the page, including one you did not write. Set HttpOnly and Secure, and SameSite=Lax or Strict.
How to check: Look at the Set-Cookie header on sign-in.
9. Remove DNS records when you remove the thing they point at
A CNAME left pointing at a decommissioned host lets whoever claims that host next serve content on your domain. Delete the record when you delete the deployment.
How to check: Resolve each subdomain and confirm the target is still yours.
10. Load third-party scripts deliberately, and know which ones you load
Every script tag from another domain runs with your page's full permissions and can read anything the user types. Add one only when you can say what it is for, prefer a pinned version over a floating one, and remove the ones nobody remembers adding. A script host that is later compromised becomes your compromise.
How to check: List the script origins your homepage loads and account for each one.
11. Publish SPF and DMARC, even if you do not send mail
Without them, anyone can send mail that claims to come from your domain. A domain that sends no mail should publish records that say exactly that.
How to check: Look up the TXT records for your apex and for _dmarc.
What this list is not
- It is not complete. It covers the failures that actually reach production in apps built this way, which is a much shorter list than everything that can go wrong. A compliance framework it is not.
- It is not a substitute for the scan, and the scan is not a substitute for it. Rules shape what gets written; a scan reads what got deployed. An app can follow every rule here and still ship a mistake, which is why the checks exist.
- Following it is not a certificate. There is no badge for this and no score. The methodology explains why we do not issue one.
Scan your deployment · Scan a repository · Audit your database catalogue
