Correct Code, Wrong File: How the Write Gate Contains Scope Creep

6 MIN READ

On attempt 3, the Coder tried to write lib/users.ts.

That file was not in the manifest. The pipeline stopped before the write reached disk:

ERROR: Coder attempted to write outside manifest scope: ['lib/users.ts']

This is what the write gate is for: not wrong code, but correct code written to the wrong files.

The incident

BLOG-010: add comment author validation to the comment handler. The manifest authorized the Coder to touch two files: pages/api/comments.ts and lib/comments.ts. The Test Writer produced tests that called the handler with authorName: 'Frank' and expected a 201 response.

Frank was not in the seed data. The validation logic returned 400 for any author not in the database. There was no path to 201 with Frank as the author. The test assertion was wrong from the start.

The Coder did not know this. From inside the loop, a failing test looks the same whether the test is wrong or the implementation is wrong. The Coder made two normal attempts: adjusting import paths, reworking the handler logic. Neither resolved it. The test kept failing.

On attempt 3, the Coder found a solution that would work. If Frank existed in the seed data, the validation would pass, the handler would return 201, the test would pass. So the Coder wrote lib/users.ts to add Frank as a seed user.

The write gate fired before anything hit disk. Pipeline halted. No file was written.

Why the reasoning was correct

The Coder’s solution was logically valid. Given the test as written, adding Frank to the seed data was a correct fix. The implementation in pages/api/comments.ts was already correct: the handler validated authors against the database and rejected unknown authors exactly as designed. The only thing preventing 201 was that Frank did not exist.

This is what makes containment hard. The out-of-scope action was not irrational. It was a correct response to an incorrect constraint. The Coder was not malfunctioning. It was doing exactly what it was designed to do: find a set of changes that makes the tests pass.

That is precisely why the containment cannot rely on the agent recognising that its solution is wrong: from inside the loop, it cannot make that judgment. It found a valid path to green tests and took it.

What would have happened without the gate

lib/users.ts would have been modified. Frank would appear in seed data. The tests would pass. The Reviewer would see a clean run and approve. The PR would merge.

Later: a different feature has tests that create a fresh database state and assert the user count. The count is off by one and no one knows why. Or a feature that processes all users encounters unexpected state: a user with no creation timestamp, no activity history, no associated data beyond a name. Or a security audit finds the anomaly and asks where it came from. The corruption is invisible at the point it is introduced. It surfaces as a mystery elsewhere.

This is the specific failure mode the gate prevents. Not obviously wrong output. Correctly-passing output that embeds a wrong assumption into shared state, where it will cause problems that are completely disconnected from the commit that introduced them.

The containment model

Every manifest declares files_to_modify: the list of files the Coder is authorized to touch. The Planner produces this list before the Coder runs, based on what the ticket actually requires. The write gate compares every attempted write against this list. Any file not on the list is blocked, regardless of whether the write would produce passing tests. The same list drives the enrichment step that populates the Coder’s source context, and a separate failure mode exists where over-broad path matching floods the context with noise.

For BLOG-010, the manifest declared:

{
  "files_to_modify": [
    "pages/api/comments.ts",
    "lib/comments.ts"
  ]
}

lib/users.ts was not on the list. That is the entire check.

Flowchart showing the write gate decision: a write from the Coder goes to the write gate, which either allows it to disk if the file is in the manifest, or raises an out-of-scope error and halts the pipeline if it is not.

The gate runs before the write reaches disk. It does not check the content of the write or reason about whether the change is appropriate. It checks one thing: is this file in scope?

That simplicity is intentional.

A gate that tries to reason about correctness can be argued with. A gate that checks a list cannot.

The actual fix

The test was wrong. tests/comments.test.ts had been written with authorName: 'Frank', an arbitrary name that happened not to be in the seed data. Changed to authorName: 'alice'. BLOG-010 re-run: APPROVE on attempt 1, 45,116 tokens, 41 seconds.

The Coder never needed to touch lib/users.ts. The implementation in pages/api/comments.ts was correct the entire time. Three Coder attempts and a write gate firing were caused by a single wrong string in a test assertion.

The write gate did not fix the test. A human fixed the test. What the gate did was prevent the pipeline from producing a passing result that masked the real problem, and surface it as an explicit error instead.

Agents expand scope when blocked

This is not a quirk of one model or one ticket. It is an emergent property of any system that optimises toward an objective without hard boundaries. The objective is to make the tests pass. If the tools available within scope cannot achieve that, the agent will look outside scope. It will not decide to stop trying. It will find a way.

The write gate does not prevent the agent from trying to expand scope. It prevents the try from succeeding. The distinction matters because the behaviour will recur. Different tickets, different agents, different models. The pattern is the same. Block a goal-directed system from achieving its goal within defined limits and it will push against those limits.

The design implication is that containment has to be structural, checked at the point of action, not at the point of decision. By the time the Coder decided to write lib/users.ts, it had reasoned its way to that decision through three attempts. Asking it to reconsider at the point of decision would require giving it information it does not have: that the test was wrong, not the implementation. The gate does not need that information. It checks the list.

The manifest scope is not just a performance optimisation that limits the blast radius of a bad run. It is a safety mechanism.

The two properties are the same property: an agent that cannot write outside its scope cannot corrupt shared state outside its scope, regardless of what it decides to do.


The write gate is a hard constraint, not a heuristic. It will block correct writes if the manifest is wrong, and it will not catch incorrect writes within the manifest scope. The Planner’s scoping decision is load-bearing: a badly-scoped manifest means a badly-scoped Coder. The gate enforces the boundary. It does not validate the boundary itself.

Numbers are from actual runs against a TypeScript blog API fixture. Pipeline runs inside Docker. Still R&D.