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 how the same handful of mistakes can reach production again and again. These are 16 rules you can commit and point it at. Each one says how to check it: the scan can check most of them, and the two about authorization are yours to test by hand.

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. It is a leak that working login does not prevent.

**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. Publish only your build output, never the project root

package.json and package-lock.json belong to the build step. Served from the live site, the lockfile states every exact dependency version, which turns finding a known vulnerability from a guess into a lookup. Point your host at the build folder, not the repository.

**How to check:** Request /package-lock.json on your live site. It should not be served.

## 7. 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.

## 8. 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.

## 9. 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.

## 10. Keep auth tokens out of localStorage and sessionStorage

Anything in localStorage or sessionStorage can be read by any script on the page, including one injected through a bug or a dependency, and it survives the tab closing. Do not write access, refresh or session tokens there. Let your server set them as httpOnly, Secure cookies the browser will not expose to JavaScript. If your auth library stores its own session in localStorage by default, that is its choice, not yours; a token you write by hand is the one to move.

**How to check:** Search your served JavaScript for setItem with a token-shaped key.

## 11. Give every form that holds a password method="post"

A form with no method submits by GET, and a GET submit puts every named field in the page address, where browser history, server logs, analytics and the Referer header keep it. Your submit handler usually stops that, but not before your script has loaded. Set method="post" on the form as well as handling submit in code.

**How to check:** Every <form> in your served HTML that holds a password field says method="post".

## 12. Load each third-party script from the service's own domain

The end of a hostname decides who owns it: cdn.jsdelivr.net.example.com belongs to example.com, not to jsDelivr. Copy a script's address from the service's own documentation, never from a snippet you found, and remove any script tag you cannot account for.

**How to check:** For every <script src> on another domain, the host ends in the domain of the service you meant to load.

## 13. 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.

## 14. 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.

## 15. Give every version-pinned CDN file an integrity hash

When you load a fixed version of a library from a public CDN, add its integrity attribute and crossorigin="anonymous". The browser then refuses the file if the CDN ever serves something different. Do not add a hash to a loader that changes on purpose (a payment SDK, an analytics tag, @latest): it will break.

**How to check:** Every <script> and stylesheet <link> whose URL names an exact version on jsDelivr, unpkg or cdnjs carries an integrity attribute.

## 16. 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. It is a leak that working login does not prevent.

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. Publish only your build output, never the project root

package.json and package-lock.json belong to the build step. Served from the live site, the lockfile states every exact dependency version, which turns finding a known vulnerability from a guess into a lookup. Point your host at the build folder, not the repository.

How to check: Request /package-lock.json on your live site. It should not be served.

7. 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.

8. 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.

9. 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.

10. Keep auth tokens out of localStorage and sessionStorage

Anything in localStorage or sessionStorage can be read by any script on the page, including one injected through a bug or a dependency, and it survives the tab closing. Do not write access, refresh or session tokens there. Let your server set them as httpOnly, Secure cookies the browser will not expose to JavaScript. If your auth library stores its own session in localStorage by default, that is its choice, not yours; a token you write by hand is the one to move.

How to check: Search your served JavaScript for setItem with a token-shaped key.

11. Give every form that holds a password method="post"

A form with no method submits by GET, and a GET submit puts every named field in the page address, where browser history, server logs, analytics and the Referer header keep it. Your submit handler usually stops that, but not before your script has loaded. Set method="post" on the form as well as handling submit in code.

How to check: Every <form> in your served HTML that holds a password field says method="post".

12. Load each third-party script from the service's own domain

The end of a hostname decides who owns it: cdn.jsdelivr.net.example.com belongs to example.com, not to jsDelivr. Copy a script's address from the service's own documentation, never from a snippet you found, and remove any script tag you cannot account for.

How to check: For every <script src> on another domain, the host ends in the domain of the service you meant to load.

13. 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.

14. 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.

15. Give every version-pinned CDN file an integrity hash

When you load a fixed version of a library from a public CDN, add its integrity attribute and crossorigin="anonymous". The browser then refuses the file if the CDN ever serves something different. Do not add a hash to a loader that changes on purpose (a payment SDK, an analytics tag, @latest): it will break.

How to check: Every <script> and stylesheet <link> whose URL names an exact version on jsDelivr, unpkg or cdnjs carries an integrity attribute.

16. 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 a short list of failures that are easy to make when building this way, much shorter 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