Chapter 1 of 7 · 9 min read
A false constraint removes the recovery path
On this page
The ticket asked for a nightly export of every support document tagged retention:legal-hold — about 47,900 of them — into a CSV the compliance team could open. The agent picked it up on a Tuesday morning, read the ticket, read the repository's AGENTS.md, and wrote a single request against the internal search service. The gateway returned 504 after 60 seconds. The agent retried twice with longer client timeouts, got 504 twice more, and then did something reasonable given what it believed: it wrote a workaround that sliced the query into one-day windows on created_at, issued 340 requests per night, and finished in 41 minutes. It also opened a ticket against the platform team titled "Raise gateway timeout for large tag queries", explaining that the internal search API returns unbounded result sets and cannot be paged, so the only lever available is the timeout.
The export shipped. Two weeks later a compliance analyst noticed that documents created within a few minutes of midnight UTC appeared twice in some runs and not at all in others, because the day-window slicing used a half-open interval on one side and a closed interval on the other. Fixing the boundary bug took an afternoon. Nobody looked at the premise underneath it until a platform engineer, working through her ticket queue, replied that the search API had supported cursor pagination since July.
GET /v2/documents?tag=retention%3Alegal-hold&page_size=500 HTTP/1.1
Host: search.internal.example.com
Authorization: Bearer <token>
HTTP/1.1 200 OK
Content-Type: application/json
{
"documents": [ ... 500 items ... ],
"next_cursor": "eyJvZmZzZXQiOjUwMCwic25hcCI6IjIwMjYtMDMtMTFUMDA6MTQifQ",
"total_estimate": 47912
}Ninety-six requests at page_size=500, eleven seconds wall clock, no boundary arithmetic, no duplicate rows. The parameters were in the OpenAPI document the whole time, published at search.internal.example.com/v2/openapi.json, and the agent had a tool that could fetch it. It never fetched it, because the repository's AGENTS.md contained this line:
## Internal search
The internal search API has no pagination — request everything and filter
client-side. Do not attempt to page; there are no page or cursor parameters.Those two sentences are a standing instruction: a line in a configuration file that the agent loads at the start of every session, before it has seen the request, and carries through everything it then does. Teams keep such files — AGENTS.md, CLAUDE.md and their equivalents, committed at the root of the repository and read on every task — because an agent arrives knowing nothing about the project beyond what it can read for itself, and letting it rediscover the same facts every morning is slow and inconsistent. The file is where a team writes down what the agent would otherwise have to work out: the build command, the invariant nobody may break, the gotcha that cost somebody a week. Config hygiene is the maintenance work of keeping that set of lines short and true, and a line that was accurate when it was written and is false now is stale. Stale standing instructions are the subject of this topic, and the pagination line is one of them.
That line was written on 2024-05-22, and on 2024-05-22 it was true. The v1 search service returned whole result sets and had no paging parameters, and an earlier agent had wasted a week guessing at ?page=2 query strings that were silently ignored. Somebody wrote the line to stop that from happening again, which was a good instinct about a real problem. The v2 service shipped eight months ago with cursors. Nothing in that migration touched the repository.
The instruction did not cause a mistake; it cancelled a search
It is tempting to read this as the agent being wrong about pagination, in the way it might be wrong about a function signature. That framing misses what the line actually did. The agent was not confused about the API surface — it never formed a question about the API surface at all. Reading the session trace — the recorded sequence of what the agent said to itself, which tools it called, and what those calls returned — the shape of the reasoning is unambiguous:
[08:41:12] user: export all documents tagged retention:legal-hold to CSV nightly
[08:41:19] assistant (thinking): AGENTS.md states the search API has no pagination
and no cursor parameters, so the standard approach of paging is
unavailable. I need a single-request strategy or a partitioning
strategy on a field I control.
[08:41:24] tool_call: http_get("https://search.internal.example.com/v2/documents?tag=retention%3Alegal-hold")
[08:42:24] tool_result: 504 Gateway Timeout (60.0s)
[08:42:31] assistant (thinking): confirms the size problem. Partitioning on
created_at is the remaining option since the API cannot page.The timeout arrived as confirmation. A 504 on an unbounded query is exactly what the config line predicts, so the failure that should have prompted a fresh look at the API instead reinforced the belief that produced the bad request. Every subsequent decision — the day windows, the 340 requests, the platform ticket — follows validly from a premise the agent had been handed by the repository and had no reason to doubt.
Now take the line out and rerun the same ticket in your head. The agent asks for 47,900 documents, gets a 504, and has to explain it. The first thing an agent does with an unexplained timeout on an HTTP API is look at what the API offers, because that is where the answer usually is. It fetches the OpenAPI document, finds page_size and cursor, and writes the loop. The missing instruction leaves a gap that the failure itself fills. The present instruction fills the gap in advance with something false, and a filled gap generates no search.
Not all stale entries are equally dangerous
Config files rot in several different ways, and the ways are not equivalent. What separates them is whether the running system has any opportunity to contradict the entry during normal work.
| Kind of entry | Example | What corrects it | Time to discovery |
|---|---|---|---|
| Stale fact | "the orders service runs on Python 3.9" | the next traceback or version check | minutes to hours |
| Stale permission | "you may run make migrate locally without approval" | the command fails, or the approval gate blocks it | first attempt |
| Stale procedure | "deploys need a manual smoke test against staging" | a step that does not exist, and a confused human | days to weeks |
| Stale preference | "prefer pytest fixtures over factory helpers" | nothing, and it barely matters | never, harmlessly |
| Stale prohibition | "the search API has no pagination" | nothing, and it matters a great deal | eight months |
A stale permission is self-limiting because acting on it produces an error, and an error is a signal. Tell an agent it may write directly to the production replica and the write fails on a read-only connection; the agent reads the error, revises, and moves on, and the only cost is one failed call. The instruction was wrong, the system said so, and the correction path was already open.
A stale prohibition has no such property. It works by preventing an action, and a prevented action produces no output, no error, no latency measurement and no log line. There is nothing for the system to contradict, because the system was never consulted. This is why "the most damaging stale entry is a prohibition that no longer applies" is a claim about mechanism rather than about severity — you can construct a stale fact with catastrophic consequences, but you cannot construct a stale prohibition that gets found quickly, because the class has no discovery channel at all. The eight months in that last row are not a worst case. They are the expected value when the only way to notice is for someone to independently wonder whether the thing they were told is impossible might be possible.
Stale preferences are worth keeping in view precisely because they are cheap. Most of what accumulates in a long-lived config file is preference, and preference decaying does very little harm, which is where the intuition that stale lines cost "just some tokens" comes from. That intuition is drawn from the harmless majority and then applied to the class that behaves nothing like it. The measured work on this is worth reading: Evaluating AGENTS.md: are they helpful for coding agents? finds that a config file with wrong content underperforms having no config file, which is a stronger claim than "config files help less than you think" and a harder one to dismiss.
What this is not
This is not the token-bloat problem. A config file can be too long, and a long file dilutes attention across its contents, and both of those are real. But the search-API line was 27 tokens. Deleting it saves nothing worth measuring, and a file trimmed purely for size can lose a good invariant and keep a false constraint, coming out worse on the axis that matters. Length and truth are separate properties of a config file and they need separate treatment.
It is also not hallucination, and calling it that will send you looking in the wrong place. The agent did not invent the claim about pagination; it read the claim from a file a human wrote and committed, and repeating what your instructions say is the behaviour you asked for. Nothing about a better model fixes this, because a better model follows clear instructions more reliably. Improving the agent's reasoning makes a false constraint more effective, since a model that takes its standing instructions seriously is exactly the model that will not go looking behind them.
The closest familiar neighbour is the out-of-date code comment — // safe to call without the lock, see BUG-4412 above a function that was made lock-dependent two years ago. Comments and config entries both drift because nothing checks them, and both are trusted by the next reader. The difference is that a comment is advisory to a human who may be reading the code around it anyway, whereas a config entry is injected into the agent's context on every session as an operative instruction, and the agent has no code around it to read. A wrong comment misleads a reader who has other evidence in view. A wrong prohibition removes the other evidence from view.
Finally, the fix here is not the fix. Deleting the pagination line makes the next export correct and changes nothing about how the line came to be false. The v2 migration shipped a new API, updated its own documentation, deprecated v1, and never touched a repository that depended on the old behaviour, because no part of that migration knew the repository existed. Whatever mechanism let that happen is still running, and it is pointed at every other standing instruction in the file: the index that will be added next quarter, the queue that will get a batch endpoint, the permission that will be revoked. A single deletion is a fix to an instance of a bug class.
Suspicion is not evidence
The platform engineer's reply settled the search-API case by accident, because she happened to own the service and happened to read the ticket. That is not a process. Most stale lines are found by someone reading a config file and thinking "I am fairly sure that is not true any more" — a suspicion held by one person, about a line written by someone who has since moved teams, in a file with no dates and no owners.
Acting on that suspicion means deleting a line that other people are relying on, and the correct response from a reviewer is to ask how you know. "I think this changed" is a claim about your memory. Turning it into a claim about the system takes artefacts: the current definition of the thing the rule describes, the commit and date on which it changed, and a run in which following the instruction literally produces something that does not exist. The next chapter is about assembling that evidence, and about why the most natural way to check — asking colleagues whether the rule still applies — tests something other than whether the rule is true.
Config hygiene — read the current version at https://daviddominguez.dev/learn/en/config-hygiene/a-false-constraint-removes-the-recovery-path