Every gate in my system is a scar

A long queue of white paper slips stretching into the distance toward one narrow doorway whose frame is held together with visible metal staples, in flat editorial shapes.
Illustration generated with AI (Flux 1.1 Pro).

Last week a Claude Code session told me my order queue had 180 open items, that none was older than 13 days, and that 221 of the 254 entries had been written by Claude Code sessions. It had counted itself. The biggest single mailbox in the system was the system working on itself.

I had not looked at that list in a while.

Ten projects and one Markdown file

I started working across several projects with Claude Code at the end of last year, and back then the whole system was a TODO.md per project folder. I opened a session, said what needed doing, the file kept the rest. For two or three projects that is completely fine and I would recommend it.

Then it became ten, and three things went wrong at once.

Ideas got lost on the way home, because the place to write them down was a Markdown file on a machine I was not sitting at. Mornings turned into a decision I did not want to make: ten lists, no shared view, no ranking across them, so the project that got my attention was usually whichever one I had touched last. Which is the opposite of a priority.

And projects went quiet for weeks. Not because they were finished. Because nothing in my setup ever raised its hand, and a file does not tell you it has been ignored.

What it looks like now

Last week I wrote the manual, which I had been putting off. Not documentation of the code, a manual for the operator, and the operator is me. Where I do what, which commands exist, how a note becomes an order, which four places stop and wait for a human. Eight sections. A tool that needs a manual for its only user is telling you something about how it grew.

Anything that could become work goes into one collection point, and from there it has exactly one path.

Four sources feed the system: a mail to myself, mail from outside, something said in a session, and a fault that turned up. Mail goes through a post office step that either files it, drafts a reply, or turns it into work. Everything that is work lands in one drop box, which is emptied into the project's own order list every 15 minutes. From there an order passes two gates: it waits until I approve it, then it either runs alone in its own worktree or we do it together. Autonomous work is merged in by me before it counts as closed.

Two clockworks run behind this, and neither of them thinks. A launch agent empties the drop box every 15 minutes (StartInterval 900), sorting entries into the right project’s list. A second one every 30 minutes (StartInterval 1800) picks up approved orders and runs them headless in a git worktree under ~/.cache/master-dispatch-worktrees, deliberately outside every repository. No model is involved in the sorting step, which is why it is free to run it that often.

The part I use most is the least clever one. I send myself an email.

A mail I send myself, for example that an upload box stutters when scrolling, arrives in my inbox. A post office step recognises it as work and tags it with the project it belongs to. It goes into the drop box, is picked up within 15 minutes into that project's order list, and sits there waiting for my approval.

That is the fix for the first of my three problems. The thing I can always reach is my own inbox, so that became the entry point. What it does not do is turn a note into work. It becomes an order and then it stops, which brings me to the reason any of this has gates at all.

Three sessions, one file

On 17 July three parallel sessions each read the same shared config file, changed their own line, and wrote the whole thing back. Last writer won, one session’s exception silently erased. I wrote that one up separately, so the short version here: shared resources are only writable through a lock now, and a hook refuses any command that tries to skip it.

The realisation behind it is the part that stuck. I had been treating parallel sessions as if they were parallel humans, who would notice. They do not notice. They are processes, and the moment two of them share state you owe them the discipline you would owe two threads.

The tool that produced more work than it finished

Which brings me back to the count at the top.

By 28 July the queue held 180 open orders and none of them was older than 13 days. That means the system had produced roughly 20 new orders per day since 20 July, while I was closing somewhere between two and five. 221 of 254 drop-box entries had been written by AI sessions rather than by me, 163 of them from sessions in the orchestration repo itself.

Nothing was broken. Every one of those orders was reasonable. That is exactly the problem. A Claude Code session wants to be useful, and if you open one in any real repository it will find ten genuine improvements: a refactor that would be cleaner, a doc that is out of date, a test that could be tighter. Multiply ten reasonable observations by fifteen repositories and you get a stream no single person will ever drain. My inbox was not filling up with mistakes. It was filling up with good ideas.

The fix inverts the burden of proof. Until then an order existed unless somebody actively deleted it. Now it only comes into existence if a trigger can be named: a fault that hits a user or customer, a risk to security, money or data, a deploy blocker, or me saying so out loud. “Would be cleaner”, “noticed in passing”, “for consistency” is not a trigger. Those go into the session summary I read, not into the pipeline. Applying it backwards archived 19 meta-orders in one go and took the total from 165 to 144.

The thing I deliberately did not build here is the interesting one. The obvious move is an AI gatekeeper: let a model judge each incoming order and reject the noise. I did not, because the same week a badly worded order had come through that was long, well structured and entirely plausible, and which, if taken literally, would have routed the outgoing mail of 186 customer mailboxes through a service that was never meant to carry it. A model gatekeeper would have waved that one straight through. It would have contained precisely the failure class it was supposed to catch. Some problems do not get better by adding another model to them.

The guard that was looking at the wrong thing

The most recent one is from yesterday, and it is the fourth time I have hit this same class of failure.

One session writes only to its own repository, enforced by a pre-tool hook, and until yesterday that hook worked by inspecting commands. It knew the shapes that write to a foreign path: output redirection, tee, git -C, sed -i. On 28 July a rollout script wrote into 21 foreign repositories through a Python heredoc. The hook saw python3 with a string attached and waved it through, because a heredoc is not on the list of things that look like writing.

So I stopped guessing at intent. A check now records the git state of every repository before an autonomous run and compares it afterwards. It does not care how a change was made, only whether one appeared where it should not have, and it fails closed: a missing check blocks the dispatcher, a failed baseline aborts the order, an absent baseline counts as a discrepancy rather than an all-clear.

Pattern matching guesses what a command will do. Comparing before and after measures what it did. It took me four attempts to learn that.

What it costs and what is still open

The orchestration layer is 32 shell scripts and about 6,300 lines, with 145 tests that run against copies and never touch the real orders. Twenty-one hooks fire around tool calls: repository boundaries, resource locks, one that blocks a database dump flag which once destroyed 199 of 200 rows, one that refuses outgoing mail containing facts that are not on a verified list. None of it was designed. Each piece sits where something went through.

Four places still stop and wait for me. An order does nothing until I approve it, including the ones I filed myself. Work marked dialogue does not run without me. Finished work never merges itself. And anything mentioning payments, customers or a production deploy is held even when approved, because on 27 July a fifty-euro customer refund was sitting in the queue, approved, marked autonomous, ready to go.

Today: 149 orders on the books, 122 approved and waiting. The trigger rule slowed the inflow, it did not reverse it, and I am not sure it can be reversed, because this is not a bug in the tooling. A machine that generates candidate work faster than a human can evaluate it always ends in a queue. Every gate I have built is a place where I chose to be the bottleneck on purpose.

If I were starting again at the end of last year with ten projects and one Markdown file each: build the drop box and the approval gate first, and nothing else. Those two are the only parts I have never had to repair. Everything else I built twice.

← All posts