Adrian Saxton
Tales from the Lab

The best thing I did for my agents was make their tools boring

Cartoon: in a home lab with monitors and a small server rack, a tall man in a red polo holds a big shiny red button labelled KILL up out of reach while handing two small hoodie robots a plain grey box with STOP and START buttons. One robot stretches on tiptoe towards the red button; the other happily presses START. A huge padlocked multi-tool hangs retired on the wall behind them.

In July I had nine agents and a shell. Most of what they did for me went through that shell, because allowing one more command was always quicker than building a tool, and every week they could do something they couldn't the week before, which looked a lot like progress.

This autumn I took the shell away from the agent that looks after my containers and gave it a handful of narrow tools instead. I expected the work to be in the tools. Most of it turned out to be in making each tool hard to misuse, none of it is clever, and all of it is the kind of work that gets skipped while the shell is still there.

One of the first things I ran into was in the proxy that sits in front of the container runtime's API. It has a flag that allows restarts, and on the live proxy that flag also opened stop and kill. I didn't want any agent of mine learning the word kill, so the restart flag stays off and the tool's restart is a stop followed by a start. Verbs you didn't ask for arrive bundled with the one you did more often than you'd expect.

The update tool takes two arguments, the name of the service and the version the approval was for, and before it does anything it reads the version the update watcher currently reports. If that has moved since the approval, it refuses. The mistake it stops is an easy one to make: the agent asks, I say yes, and by the time it acts my yes belongs to a different question. Anyone who has written database code will recognise compare-and-set. It costs one read before every write, and it removes a whole class of error in which a model does the approved thing to a world that has changed since it asked. For the same reason the tool reports the state it finds after the change, rather than the status code that came back when it triggered the update.

Every action checks a policy file before it runs, and the first version of that check compared names. I was pleased with it for about a day. Then I remembered that the container runtime accepts an id, or a prefix of one, as readily as a name, and my list only had names in it. A request that named a protected service by its id would have passed the name check and done the one thing the list existed to stop. The fix is a line of design rather than a line of code: resolve whatever the model gave you to the canonical name, and only then read the list. The hole was closed before the tool shipped, there is a test case for it, and that case is most of the reason I trust the list at all.

The list itself is one file, read by the tool the agent calls and by the script that acts on an approved update, so there is one answer to whether a service may be touched rather than two that drift apart. It holds a short set of services no agent action may touch and a longer set that is off limits for updates only, and the update tool also honours anything the watcher itself has excluded or pinned. Any one of those checks is enough to refuse. If the file is missing or malformed, both readers refuse everything, because "I can't find the list" and "there's nothing on the list" are the same sentence to a model and very different sentences to me.

Anthropic's "Building effective agents" says that when building applications with LLMs "we recommend finding the simplest solution possible, and only increasing complexity when needed", and offers a rule of thumb: "think about how much effort goes into human-computer interfaces (HCI), and plan to invest just as much effort in creating good agent-computer interfaces (ACI)". I read it in July, with nine agents and a shell, and took it as a warning against over-engineering. My own evidence agrees with it, though not in the way I expected: the complexity I took out was agents, and the complexity I kept was in the interface of each tool.

The costs are small and real: the precondition is a read before every write, the canonical lookup is a round trip, and the policy file is one more thing to keep under version control, with a test suite that has to run again after every change to it.

When one of my agents is wrong about the world now, the tool refuses and reports why, and that has done more for the system than a tenth agent would have. If you're putting agents on anything that matters, tools that refuse properly are where I'd spend the effort first.

Sources

  • First-hand: the container tools in the multi-agent system I run at home, built in October 2026: the update tool's expected-version check, the canonical-name resolution before the policy check, the one policy file read by the tool and by the approval script (both refusing when it is missing), and the proxy restart flag that also opened stop and kill. No product, container, agent or network names; the policy file's contents and size are not given.
  • Anthropic, Building effective agents, undated on the page (the two quoted lines, verified 20 October 2026).