ADR 0041 / Running AI agents against production systems / 3 min read

One primary agent instruction file, a byte-identical shared layer, and per-host environment facts beside it

Status: Accepted · in production (neutral primary files across the fleet; shared-layer checks cover five web hosts, with two environments verified whole)

Context

Four different agent harnesses run across the fleet. Only one of them reads its vendor-specific instruction file; the other three read the vendor-neutral AGENTS.md and nothing else.

Every host-specific warning lived in the vendor-specific file. So a session started under a different harness on one staging-tier host was never told the one fact that mattered most about that machine: its database connection is the production one. The environment's most consequential fact was absent from the file that harness read.

Looking at the files made it worse. The five web-host instruction files shared 93 byte-identical lines, while their supposedly generic 125-line middle had drifted into five distinct versions.

Decision

  • AGENTS.md is the single primary agent file everywhere. The vendor-specific file becomes a symlink to it.
  • Per-environment facts live in a separate ENVIRONMENT.md, reached by a one-line pointer from AGENTS.md. It states which database this environment touches and whether an agent may write to it, and little else.
  • The shared layer stays its own byte-identical file on every web host, checked by the sync tool. Membership follows identity: if a sentence needs different wording in an environment, that environment is not in the shared set and its whole file is verified against its own source.
  • ENVIRONMENT.md is untracked and gitignored. That is safe because the deploy script refuses only tracked dirtiness, so an untracked file never blocks a deploy.
  • State the command, not the answer. Documentation that restates a current value is now treated as a defect.

Three alternatives were rejected. Renaming the vendor file per host collides with an AGENTS.md the application repository already tracks, and overwriting it dirties the worktree the deploy script checks. A per-host file with no pointer leaves the harness gap open, because the pointer has to live in the file the other harnesses already read. And rendering one composed file per host hides drift: keeping the shared layer byte-identical is what makes drift one sha256sum away from detection, and that is the only reason the shared/per-host split can be trusted.

Consequences

  • The harness gap closes for every current and future harness that reads the neutral file, without my enumerating them.
  • A whole class of divergence goes away. Host-specific content is no longer duplicated into files that were supposed to be generic.
  • Four stale claims surfaced while making the change, all of them prose caching a fact the environment already knew. One described a database as read-only when its grants did not make it so. A paragraph that says "this is read-only" is not a control, and an agent will believe it.
  • Explanations that are properties of the code move to the repository that owns the code, reached by pointer. Migration discipline lives once.
  • One line had to be added to a file another repository owns, by pull request. That is the cost of putting the pointer where it will be read.

Amendment - 2026-10-16

The shared-layer check now covers five web hosts. Two other environments take their whole source files without the layer and are checked whole by the normal verification run. A claim that was true on the web hosts was false elsewhere; sharing follows the truth of the text, not the convenience of one template.

The sandbox fleet also puts a per-role permission pointer in the generated ENVIRONMENT.md header. The permission had lived in a later role file that transcript inspection found was never read. The shared file stays byte-identical; the generated header carries the role's exception pointer. The pointer belongs in the file the harness loads, even when the full rule belongs elsewhere.

When I'd revisit

If harnesses converge on a standard for layered or included instruction files, the pointer becomes an include and the symlink goes away. If per-host facts grow past a screen, they are turning back into documentation and should be replaced with commands that print the truth.