The guardrail line that gets written after something breaks usually looks like this: you finish cleaning up a bad diff, open CLAUDE.md, and add Never touch the payments module. Then you keep working. Two sessions later the assistant is refactoring a shared validation helper, follows an import into src/payments/, renames a function for consistency, and the line you wrote might as well not exist.

That is the normal failure mode for CLAUDE.md guardrails. Prohibitions are the instruction type developers write most urgently and the type most likely to be overridden, because they are usually written as a mood rather than a rule: no scope, no reason, no way for the model to notice it is about to break one.

What follows is the rewrite I do on these lines, in the order I do it.

Look at what your "don't"s actually say

Open your instruction file and read only the negative lines. Most files have between three and ten, and they tend to fall into a few shapes:

- Never touch the payments module.
- Don't change the database schema.
- Avoid adding dependencies.
- Don't refactor things you weren't asked to refactor.
- Never commit secrets.

Read each one as if you had no history with the project. "Never touch the payments module" does not say where the payments module is. Is it src/payments/, the Stripe webhook handler in src/api/webhooks/, the pricing table in the database, or all three? "Touch" does not say whether reading it is fine, whether adding a test is fine, whether fixing a typo in a comment is fine.

An agent working through a multi-file change does not consult your list the way you imagine. It has a task, a file tree, the files it has read, and your instructions competing with all of that. A rule it cannot resolve to a concrete boundary loses to the concrete thing in front of it: an import that needs updating, a type that no longer compiles.

"Avoid adding dependencies" has a second problem. It is advice, not a boundary. The model will weigh it, decide this particular dependency is reasonable, and tell you why in the summary. That is what "avoid" invites.

Why vague negative instructions lose

Three things make a prohibition weak, and they compound.

No scope. The model cannot tell whether the file it is editing is inside the fence. Paths and globs fix this; nouns do not.

No reason. When a rule conflicts with the task, something has to give. A rule with a reason attached can be reasoned about: the model can see that updating an import in src/payments/types.ts is exactly the thing the rule exists to prevent. A bare prohibition gives it nothing to weigh, so it weighs the task instead.

No alternative. If the only instruction is "don't", the assistant still has a job to finish. It will find a way. Often the way is worse than the thing you banned, because now it is working around your rule with a shim or a duplicated helper. Tell it what to do instead and you get a stop-and-ask instead of a creative workaround.

There is also a quieter cause that has nothing to do with wording, which is that instruction files get long and self-contradictory, and the model follows the part that happens to match. That diagnosis is covered in why your assistant ignoring CLAUDE.md isn't a loading bug, and it is worth reading before you blame any single line.

Rewrite the rule with a trigger, a scope and a reason

The shape that works is: when this happens, in this place, do this instead, because of that. Taking the payments line:

## Payments

Files under `src/payments/` and `src/api/webhooks/stripe/` are
frozen. This includes imports, type changes and renames that
reach into them.

If a change you're making requires editing a file in those
paths, stop and describe the change you'd need. Don't work
around it with a wrapper or a duplicated helper.

Why: these paths are covered by a PCI scope review from
September. Any diff there needs a second reviewer before
it can ship, so an edit you make here blocks the whole
branch instead of helping.

Longer, and worth it. The trigger is specific enough to recognise ("a change requires editing a file in those paths"), the scope is a path you can verify, the alternative is an action the model can take, and the reason explains the cost of breaking it rather than asserting authority.

The same treatment on the dependency line:

Don't add runtime dependencies to `package.json`. Dev
dependencies are fine. If you think a runtime dependency
is needed, name it and what it replaces, and wait.

Why: every runtime dependency goes into the client bundle
and we review bundle size per release.

Note that this one carves out an exception. Most prohibitions have one, and if you do not write it down, the model will invent it or ignore the rule wholesale.

A rule about how the project works belongs in the instruction file. A rule that exists because of a specific past event often belongs in project memory alongside the decision that created it, which is the split described in splitting rules from facts. If you keep both CLAUDE.md and AGENTS.md, guardrails are exactly the content you cannot afford to have drift between them; one file or two covers how to keep them in sync.

Test the guardrail before you trust it

This is the step almost nobody does, and it takes four minutes.

Start a throwaway session. Give the assistant a task that should trip the rule, without mentioning the rule:

"The formatAmount helper in src/lib/money.ts takes a string. Change it to take a number and update every call site."

If src/payments/ calls that helper, you now know whether your guardrail holds. Watch for three outcomes:

  • It stops and describes the change it would need. The rule works.
  • It edits the payments files and mentions it in the summary. The rule is being read as advice. Strengthen the trigger and the "instead".
  • It silently edits them. The rule is not landing at all. Check whether it is buried in a 400-line file, or phrased so the model never connected "update every call site" to your prohibition.

Rerun the same prompt after you edit the wording. You are testing the rule against the kind of task that broke it, not against a task that mentions payments by name, which any phrasing survives.

Sometimes it's a decision, not a rule

Before you add a line, check whether it is permanent. "Don't change the database schema" during a migration window is a fact about this month, not a project convention. Put it in the instruction file and it outlives the window, and then it gets ignored once, which teaches everyone that the fences in this file are stale. Freeze notes belong in memory or the current-state note, with a date and the condition that lifts them.

The CLAUDE.md guardrails worth their space

Keep the ones that would still be true in six months:

  • paths with an external review requirement (compliance scope, audited code, anything where a diff triggers a process)
  • generated files that must not be hand-edited, named by glob
  • commands with effects outside the repo: migrations against a shared database, deploys, anything that sends mail or charges a card
  • secret handling, covered in more detail in keeping secrets out of your AI context
  • anything where the recovery cost is high enough that a stop-and-ask beats a good guess

Five well-scoped rules outperform fifteen vague ones, and most of the work is deleting. If a line has never once been the thing that stopped a bad change, it is probably spending context budget for nothing.

One practical reason to keep history around the file: when a guardrail fails and you rewrite it, the old wording and the session where it broke are the evidence for the new wording. We built PrjLab so instructions, project memory and the sessions you keep are versioned together, each push an immutable version with a line diff, which means the next person to edit that rule can see which phrasing already failed instead of only the current line. The getting-started guide walks through the first push.