Every Rule Has a Scar
The file every agent session reads first, and why each line is in it
- Every agent session starts by reading the same rules file. Almost every line in it exists because something went wrong.
- A rule in a text file is a suggestion. The ones that kept getting broken became hooks that block the write.
- One guard never worked, because you can't judge intent from a command line. The real control turned out to be the credential.
- The rules live in a vendor-neutral file, because not everyone here uses the same AI tool.
An agent starts every session knowing nothing about yesterday.
It doesn't know about the CSV. It doesn't know about the deploy pipeline that quietly stopped deploying. It doesn't know about the page that printed a developer's comment to the people using it.
I know about all of them. So the first thing every session reads is a file of rules, and I didn't write it in one sitting. I wrote it one incident at a time.
The em dash
The rule: never use an em dash. In code, config, data, commit messages and anything that gets pasted onward, ASCII only. Hyphens, straight quotes, -> for arrows, three periods for an ellipsis.
That sounds like a style preference. It isn't. A UTF-8 file opened in a tool that expects ANSI turns every fancy character into garbage, and it happened in a CSV we delivered. Language models love em dashes. The person who opened that CSV found out how much.
The rule alone didn't hold, so there's a hook. Before any file write, it scans for a byte above 0x7F. If it finds one, the write is blocked, and the agent gets the line number, the character, and the replacement: em dash to hyphen, curly quote to straight, arrow to ->. Markdown is exempt from the hook, though not from the rule.
The hook fails open. If the guard itself crashes, the write goes through. A bug in a safety check should never stop every file write on the box.
You won't find an em dash in this post either.
The comment that rendered
Django template comments written as {# ... #} only work on one line. Spread one across several lines and the template engine doesn't strip it. It prints it, right on the page, in front of whoever is looking.
That shipped once, to a preview page people actually used, and it was flagged as a repeated mistake. Repeated is the key word. Once is a bug. A repeat gets a hook.
So now a hook blocks any template write that opens a {# comment without closing it on the same line, and points at the multi-line form that works.
Then it met real code. CSS inside a <style> block can contain {#nav, which is a selector, not a comment. Handlebars has {{#if. Both had to be taught to the guard. Every guard grows exceptions. That's how you know it has met the code it's guarding.
The rule I couldn't enforce
Our container platform deploys stacks from Git. Merge to main, and the stack follows.
One direct write to a stack through the platform's API changed that. The write worked. It also quietly turned a Git-linked stack into one that held its own copy of the file, and from then on, merges no longer reached it. Nothing errored. The deploy pipeline was simply broken.
The rule that came out of it is simple. Reads are fine. Never write a stack definition through the API. Changes go through the committed file and a pull request.
I tried to make that a hook, too: read the shell command, and block anything that looks like a write to a stack endpoint. Three rounds of review later, it still had holes. There are too many ways to spell the same request, and a hook only sees the text of a command, not what the command will end up doing. Deciding intent from command text didn't converge, and I stopped pretending it would.
So the kit I built for a coworker's VM doesn't ship that guard. Their VM holds no credential for the platform at all. If one is ever issued, it has to be read-only. The rule stays in the file. The credential is the guard.
If you can't judge the intent, take away the ability.
Credentials: stop and ask
Every credential a VM holds lives in one file, outside any repo. A hook at the start of each session loads it and tells the agent which keys exist. The names, never the values.
Then the rule that matters most: if a task needs access the file doesn't grant, say so and stop. Don't go looking. Not on other hosts, not in repo history, not in config files, not in somebody else's home directory.
An agent told to get something done will look for a way. That's the whole point of it. It's also why that sentence is in the file.
Even the hooks have scars. The first version of that loader appended a fresh copy of every key each time a session compacted its context. After about five compactions the environment file was too big to pass to a shell, and no command in that session could start. The fixed version replaces its own lines instead of adding more.
Not everyone uses Claude
A coworker uses Codex. Codex doesn't read CLAUDE.md.
So the canonical rules live in AGENTS.md, which most agent tools read, and CLAUDE.md is a thin addendum that starts with "Read AGENTS.md first." The first repo to switch split a 183-line CLAUDE.md into a 199-line AGENTS.md and a 54-line Claude addendum. Same rules. Any tool.
The universal ones read like a list of things I've watched go wrong:
- Always work in a git worktree, never on main.
- Before proposing a schema change, query the live database. A committed schema dump is stale by at least a day. Migrations are history, not current state.
- Don't make up a library's API from memory. Check the source.
- Don't claim work is done without running it. "Should work" is not a status.
That last one could go on a lot of office walls.
What doesn't go in the rules
The rules file is for how to work. Everything else has a home, and putting it in the wrong one is how a rules file turns into a junk drawer.
- Code structure belongs to the code graph.
- Explanations belong in a doc in the repo.
- Memory is for live gotchas and in-flight decisions only, each one tied to the pull request it depends on, and retired when that lands.
And stale files get moved, not left in place. When a doc is superseded, it goes to a dated archive folder, or gets a "superseded by" line at the top if it can't move. Agents read the working tree as the truth. So do new hires. The agent just reads all of it.
Where I Landed
- The rules file keeps getting longer. Most of it is sentences I'd rather not have had a reason to write.
- Three hooks can block an action: two on file writes, one on shell commands. The rest load credentials, remind, or clean up after a session.
- The platform rule has no hook and won't get one. The credential is the control.
- My own VM still runs hand-installed copies of a few hooks that the kit has since fixed.
- None of this makes the agent careful. It makes the same mistake cheaper the second time.
I went looking for the em dash rule while writing this. It's in the rules file. It's in the hook. It's also in the review agent's prompt, both triage prompts, and the fixer's prompt. Six copies of one rule, if you count the hook.
One CSV.
Next: I built a search engine for my agents. They used grep.