My agent made the same mistake four times in one conversation.
It kept writing files through a shell heredoc, and the heredoc kept eating a backslash, so the file looked right and ran wrong. Three notes in its memory store described exactly this mistake, and all three had been retrieved and read. I went back through the transcript to make sure. Then it did it again.
Work in this space gets filed under “agent memory”, and most of the tooling is about retrieval: getting the right text in front of the model. That part mostly works. What I’ve spent the last few weeks measuring is whether the text changes what the model does, and so far the answer is much less than I expected. The fixes I tried also failed in ways that were easy to miss.
Delivering the rule didn’t make it stick
My first theory was timing. Rules load at session start and decisions happen an hour later, so the fix should be to put the rule in front of the model at the moment it matters.
I had already built that. A small core stayed loaded, and the detail lived in files behind pointers: “before writing code, read this file.” Then I measured it. Across 89 sessions, 81 reached a point where a pointer told the model to go read the file. It opened the file in 4 of them, or 4.9%.
So I moved everything back to always-loaded, with every rule in context all the time. That same day, with all 59 of them sitting in front of the model, it broke them six times.
Loading rules up front and fetching them on demand turned out to be two versions of the same thing. Neither one could stop an action once the model ignored the rule. Stopping it takes hooks that the agent host runs at two points, right before a tool call executes and when the model finishes a turn, where either one can refuse and make the model try again.
The same pointer file behaved very differently in another agent host: it was opened in 109 of 144 sessions, 96 of them within the first three actions. That host reads the whole rules file at the start of every session anyway, so it never has to follow a trigger. How well a rule lands depends partly on host habits you don’t control.
Turning rules you can’t check into rules you can
A refusal needs something to match on. “Don’t write files through a heredoc” has one, because the command contains << and a backslash. Plenty of rules have nothing like that. “Verify before you say it’s done” happens inside the model, where nothing outside can see it.
What worked was changing what the rule asks for. Instead of “verify first”, the rule became: a message that claims something is done has to say what was checked, such as a test count, a command, or a file that was read. That part is visible, so a check at the end of the turn can refuse the message.
This also changes what skipping costs. Skipping verification used to leave no trace. Now the only way past the check without doing the work is to write down evidence that doesn’t exist, which is a much harder thing to do by accident.
Checks fail silently, and in specific ways
I added checks expecting that to be the end of it. Instead, every one of them turned out to have some way of not applying, without saying so, while the setup still looked armed. Five examples follow.
The check only looked at the end of the turn
A turn is usually: say something, call a tool, say something else. The person watching sees all of it, but the check read only the final message, so a false claim made before a tool call was out of reach of every rule. I found this when the agent told me something had never been tested. It had been tested the day before; that work had been summarised out of its context, and the agent took “I can’t see it” to mean “it didn’t happen.” A check written for exactly that sentence still couldn’t have reached it.
The same tool has a different name in each host
One rule guarded the Bash tool. Another host calls its shell tool Shell. Names were compared exactly, so the rule never fired there, and nothing reported it.
A cap meant for search also capped enforcement
To keep things fast, only a limited number of rule files were read per turn. The limit was supposed to bound discovery, and it bounded checking as well. In one large test store, 500 armed rules came out as 12 live per turn.
Saving a note changed where its fields lived
When a note was saved, the memory store reformatted its metadata and nested the fields one level down. The check read top-level fields only. Those rules looked armed and passed lint, and they blocked nothing.
Automatic tuning switched off a good rule
I had logic that relaxed rules which fired too often. One rule fired 6 times and blocked all 6, and was switched off for two weeks, because a hit rate can’t separate “too broad” from “the agent keeps doing this.” A rule in the second situation is doing its job. I removed the tuning.
None of these raised an error, so from the outside each of them looked like a quiet day.
Replaying history against the logs
What catches this kind of failure is replaying history through the same matching code and comparing the result with what the checks logged at the time. Every night, replay the day’s transcripts and reconcile. A hit the replay finds that the log doesn’t have is a miss. The replay has to reuse the live check’s code, since a replay with its own interpretation can disagree for reasons that tell you nothing.
Some cases can’t be told apart this way. A short quoted sentence that breaks a rule looks the same as a legitimate quotation, and I let quotations through, so for that case I count how often it happens instead of trying to decide.
My tests passed anyway
While I was building this, my suite passed 54 of 54, and four independent reviews each said not to ship. One of the things they reproduced was the documented install command silently skipping a whole step and reporting success anyway.
Later, an acceptance script I’d written to confirm that “every leftover file is named” turned out to be skipping every file, because the temporary directory happened to sit under a path the filter excluded. It exited 0 having confirmed nothing, and a reviewer caught it, not me. A test suite written by the same person who wrote the code mostly checks what that person already expected.
Memory costs something
Retrieved context stays in the conversation and gets re-read on every later call until the conversation is compacted. When I measured a week of real sessions, retrieval was by far the largest token cost in the setup, and the checks were cheap by comparison. Two findings:
- A deduplication key included a per-prompt label, so a note already delivered got sent again whenever that label changed: 1,404 identical lines re-sent within a single stretch of conversation, a median of three prompts apart.
- Using a loose proxy (was the note’s name mentioned later?), about 4% of delivered project notes and 7% of delivered reference notes got used.
If you only count what gets delivered, adding more memory will always look free.
What I’d tell someone building this
When an agent breaks a rule, ask what should have stopped the action and whether it ran, before asking whether the rule was in context. Where you can’t observe the act a rule cares about, require the sentence that reports it. Test every check end to end in every host you support, using an input it should refuse; until you’ve watched it fire, assume it’s off. Replay history against your logs, because silent misses only show up in that comparison. Anything that gets summarised can lose facts about work already done, and the agent won’t know. Measure what memory costs, and drop what gets delivered and never used.
Disclosure: I build Epitype, an open-source memory governance layer for Claude Code and Codex, and every number above comes from building it. I’ve kept it out of the body on purpose, because these failure modes don’t depend on which tool you use.