The day I moved our rules to a place they can't be broken even if you try

日本語

Contents of this article

What happened that day

Even if you hand an AI agent the rules as a document, once a session grows long the rules stop getting read, and the same violations recur. In development where several agents read and write the same repository in parallel, when the rules stop being followed like this, it leads to accidents: unsaved changes disappear, work proceeds against a stale main, writes to the same file collide.

So I stopped relying on getting the rules obeyed at all, and instead moved the rules to a place where they can’t be broken even if you try. This article is a record of running that countermeasure across three repository groups, and the three, which I had been operating separately, have settled into the same pattern: isolation via git worktree, which carves each task out into an independent directory.

Of the rules I had written in documents, I moved three into a layer that takes effect without going through the agent’s judgment: stopping state-changing git commands with a pre-execution hook, forbidding direct pushes to main with a setting on the hosting side, and connecting the environment-variable file to the worktree with a hook that runs when a session starts. Once I did, the violations that had kept recurring under the document-based approach stopped happening. Carving out one working directory takes 0.25 seconds, so even splitting it per agent, the carving itself barely costs any time.

If you are considering development that lets several agents touch the same repository, this record gives you an isolation pattern you can try as it is, along with a way to tell apart the accidents you can prevent by construction from the accidents you can only guard against by procedure.

Parallel-development accidents and the worktree isolation pattern

I have kept up development that has several AI agents read and write the same repository in parallel, across three repository groups, combining ones I develop through my personal company and ones I run personally. In this kind of development, accidents can happen: unsaved changes disappear, an agent reads a stale main and returns a stale analysis, writes to the same file collide, a git checkout or reset breaks the state of the repository itself. This article is about where to put the rules that prevent such accidents. The answer I arrived at was not to rely on writing the rules in a document and getting the agents to follow them, but to move the rules into a layer that takes effect without going through the agent’s judgment, such as hooks, settings on the hosting side, and directory structure. In this article I will call this layer the mechanism layer.

git has a mechanism called worktree that can carve out several independent working directories from a single repository. The isolation pattern that prevents accidents is an operation that creates this worktree inside the repository’s directory hierarchy and confines all work there: for read-only investigation I cut a detached-HEAD worktree that belongs to no branch, and for work that produces a PR I cut a worktree with a branch. This pattern converged to the same shape as I operated it across the three repository groups. Some repositories have a rule that forbids creating worktrees under /tmp, because the permission settings for what the agent is allowed to do are not carried over there.

Under the hood, the object database that stores commits, and references such as branches, are shared across all worktrees, and only the local state, meaning HEAD, the index, and the working tree, is independent per worktree. In other words, the history stays single while only the local state can be split per agent. The time to create a worktree was, measured on a repository of a few hundred commits and a Mac with Apple Silicon, an instant 0.25 seconds for git worktree add –detach. What actually ate time was the dependency install, not the worktree creation. On projects that use Node.js, I dodge that wait by reusing the parent repository’s node_modules in the worktree through a symbolic link, and I reinstall only on the days the lockfile that pins dependency versions changes.

Alongside the worktree isolation, there are two operational rules. One is to always run git fetch origin main before referring to anything. Other sessions and automated runs keep advancing main, so I work on the premise that the local checkout is always stale. The other is how to handle parallel writes: in order from lowest conflict risk, I choose among three methods, which are having each agent return data in a fixed format so that a single aggregating agent writes it all in one place, splitting the files each agent writes to, and isolating via worktrees, and I limit operations that cannot be undone, like commit and push, to the aggregating agent. For these three methods too, the shape I ended up with did not differ across the repository groups.

Rules written in documents stop getting read

But writing these operations into document-based rules and getting them followed by instruction was a means whose adherence I could only speak of in terms of probability. Once a session grows long, the rules stop getting read, and the same violation recurs. Looking back over the history, rules read at the start of a session would sometimes stop being referred to in the latter half of a long stretch of work. This is what I mean when I say document-based rules are a probabilistic means.

Moving the rules into the mechanism layer

So I decided to move the rules from documents into the mechanism layer. Three rules moved. A hook that intervenes before the agent runs a tool mechanically refuses state-changing git commands issued without a worktree specified. The hosting side has a setting called branch protection that forbids direct pushes to a specified branch, and I used it to forbid direct pushes to main. For the environment settings, a hook that runs when a session starts hands them over by laying a symbolic link in the worktree to the parent repository’s environment-variable file. Violations that had recurred even when written in documents have not happened since I put the hooks in. I use this before-and-after relationship, that the recurrence stopped after the change was introduced, as the basis for whether it worked. Apart from these three rules, there was also one settings fix that comes with running worktrees. When a worktree is created inside the repository’s directory hierarchy, the static analysis that mechanically inspects the code would, left as is, scan into the worktree as well, so the setting that excludes the worktree from that scan is also placed in a config file rather than as a note in a document. The three rules I decided to move, refusing state-changing git commands, forbidding direct pushes to main, and handing over the environment settings, have thus ridden from document instructions onto hooks and hosting-side settings that take effect without passing through the agent’s judgment, that is, onto the mechanism layer.

Can’t be broken even if you want to is what works.

What lay at the point where the three operations converged was a shift in thinking: rather than preventing accidents by raising the agents’ reliability, put the rules in the mechanism layer, meaning hooks, branch protection, and directory structure, and change the construction side so that accidents cannot occur structurally.

What fit the mechanism layer and what stays a matter of procedure

That said, only some of the rules could be placed in the mechanism layer. I managed to place the restriction on state-changing git commands, the ban on direct pushes to main, and the handover of the environment settings, but the accident of skipping git fetch and reading a stale main is not stopped by a hook, and I still rely on following the procedure, and which of the three parallel-write methods to choose also remains an operational judgment. The speed-up of reusing node_modules through a symbolic link also breaks when the lockfile changes and needs a reinstall. Both sticking to git fetch and choosing among the three methods are rules that stayed on the judgment side, ones you cannot stop in the form of a command, and how far I can push them down onto the settings or hook side is something I have not managed to fully tackle yet, and I won’t know until I try.

Even so, rules belong in a place where they can’t be broken. I’ll keep going in that direction without changing course.