# Writing a path-scoped rule that agents actually follow

> Rules fail for two reasons: they load when nobody needs them, or they are unfalsifiable. Scope by path, write checkable statements, and give every rule an escape hatch.

Every team that works with coding agents writes a conventions file. Most of them
look like this after three months:

```md
# Conventions

- Write clean, maintainable code
- Follow best practices
- Use meaningful variable names
- Handle errors appropriately
- Consider performance
- Prefer composition over inheritance
- Don't over-engineer
- Keep it simple
```

Nine hundred words of it, loaded into every request, and the agent still does
the thing you told it not to do. Then someone concludes that rules do not work.

Rules do work. These ones cannot, for two specific reasons.

## Why this file fails

**It is unfalsifiable.** Take "handle errors appropriately". You cannot look at a
diff and determine whether it complies. Neither can a model. A statement that
cannot be checked cannot be followed: it can only be agreed with, which is not
the same thing.

**It is always loaded.** Nine hundred words of generic advice sits in every
request, including the one where you are editing a CSS file. It costs context
and it trains the reader (human or model) to skim, so when a rule that
genuinely matters appears in the same file, it gets skimmed too.

Both problems have the same root: the file was written to be *comprehensive*
rather than to *change behaviour at a specific moment*.

## The fix: scope by path, state a check

A rule in this repo has two required parts. `paths` decides when it loads, and
the body is a list of statements you could tick off against a diff.

```md
---
title: Never construct Stripe objects in the browser
paths:
  - src/lib/billing/**
  - src/app/api/webhooks/stripe/**
---

- The `Stripe` server SDK is imported only in Route Handlers, Server Actions and
  files under `src/lib/billing/`. If a file has `"use client"`, importing it is
  a bug, not a style preference.
- Prices are read from the database, never hardcoded in a component. Changing a
  price must not require a deploy.
- Every webhook handler verifies the signature with
  `stripe.webhooks.constructEvent` before it reads the body, and returns 400 on
  failure.
- Webhook handlers are idempotent: look up the event id, return 200 if it has
  been processed, otherwise process and record it in the same transaction.
```

Four statements, all checkable, and the reader only ever sees them while touching
billing code. That is a rule that changes behaviour.

The compiler turns this one file into the format each tool wants: Claude Code
gets `.claude/rules/<id>.md` with `paths:` frontmatter (the only key it reads
there, and a rule without it loads every session), Cursor gets
`.cursor/rules/<id>.mdc` with `globs:`, Codex gets a nested `AGENTS.md` in the
closest common directory. You author once.

## Scoping, concretely

| Scope | `paths` | Use for |
|---|---|---|
| Repo-wide | `["**"]` | Things genuinely always true: no secrets in source, conventional commits |
| Feature area | `["src/lib/billing/**"]` | The bulk of your rules |
| File kind | `["src/**/*.test.ts"]` | Testing conventions |
| Single file | `["next.config.ts"]` | Config invariants |

`paths` defaults to `["**"]` when you leave it out, which is exactly the failure
mode described above. Set it deliberately every time. If you cannot name the
paths, the rule is probably advice rather than a rule, and advice belongs in a
solution doc, where someone reads it once and understands the reasoning, rather
than in a rule that fires on every request forever.

## Write statements, not adjectives

The test: could a reviewer hold this line against a diff and get a yes or a no?

| Instead of | Write |
|---|---|
| Handle errors appropriately | Every Route Handler returns a typed error body `{ error: string }` and a 4xx/5xx status. Never `throw` into the framework. |
| Keep components small | A component over 150 lines is split, or carries a comment saying why it is not. |
| Use good types | No `any` in an exported signature. Use `unknown` and narrow. |
| Be careful with the database | Every query that reads a tenant resource filters on `workspaceId` from the session, in the query, not after it. |

Notice the right-hand column is longer. That is fine. Ten specific rules beat
sixty vague ones, and the specific ones are shorter to read than they look
because you only load them when they apply.

## Give every rule an escape hatch

A rule with no exception path gets broken silently, and you never find out. A
rule with a documented exception gets broken loudly, in a way you can review:

```md
- Route Handlers default to the Node runtime. `export const runtime = "edge"` is
  allowed only when the handler uses fetch-compatible APIs exclusively: add a
  comment saying which, so the next reader does not have to work it out.
```

Now the exception is visible in the diff instead of hidden in someone's head.

## Where a rule comes from

Not from a brainstorm. The good ones have a specific origin: **you corrected the
same thing twice.**

The first correction is a conversation. The second is evidence of a systemic gap,
and that is when it becomes a rule: one sentence, in the file whose `paths`
cover where the mistake happened. Ask the `system-manager` agent, whose entire
trigger is repetition.

Rules that come from real corrections have a property invented rules never do:
they are about things that actually go wrong in *this* codebase.

## When a rule is not enough

Rules are read by a model that is also holding a task, a diff and a long
conversation. Most of the time that is enough. When the cost of the mistake is
unrecoverable (a deleted database, a leaked key, a force push) do not rely on
reading. Write a hook. Hooks run in the harness and cannot be reasoned around,
and `docs/solutions/nextjs-vercel/guard-hooks-and-how-to-extend-them.md` shows
the shape.

The division is: rules for things that should be true, hooks for things that
must never happen.

## Keep them pruned

Delete rules that no longer apply. A rule referring to a directory that was
deleted in March teaches the reader that this file is stale, and a stale rules
file is ignored wholesale, including the rules that still matter. Pruning is
maintenance, not admission of failure.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
