← all posts
// architecture · architecture

Designing tools for an agent: the interface is the leash

The first agent I built had exactly one tool: bash. It felt elegant. The model could do anything a shell could do, which is everything, so why clutter the design with a dozen narrow tools? It worked beautifully right up until I needed to stop it from doing one specific dangerous thing, and discovered I couldn't, because by the time the dangerous thing reached my code it was an opaque string that looked exactly like every safe thing.

That's the lesson that took me a while to absorb. The model emits tool calls and your harness executes them, and the shape of those calls is the only thing your harness has to work with. Design the tools badly and you've handed the model power you can't supervise. The tool interface is the leash.

Breadth versus control

A bash tool maximizes what the agent can do and minimizes what you can do about it. Every action arrives in the same shape, a command string, whether it's a harmless grep or a git push --force to production or a curl that exfiltrates your environment variables. Your harness sees a string. It can't tell them apart without parsing shell, which is a losing game, so it ends up either allowing everything or interrupting the user to approve everything, and both are bad.

Promote that same action to a dedicated tool and the picture inverts. A send_email(to, subject, body) tool gives your harness typed arguments it can inspect, a specific hook it can gate, a clear thing to render in a UI, and an unambiguous line in an audit log. The model lost no real capability. You gained the ability to supervise it.

So the design tension is breadth against control, and the resolution isn't to pick one. Start with bash for breadth, because early on you don't know what the agent needs and a shell lets it figure that out. Then promote specific actions to dedicated tools the moment you need to do something the harness can only do with structure.

When to promote an action to its own tool

There are four signals, and they're all about what the harness needs to do that an opaque command won't allow.

The clearest one is a security boundary, and reversibility is the test. Hard-to-reverse actions, sending a message, deleting data, spending money, hitting an external API, are the ones you want to gate behind confirmation, and you can only gate cleanly what arrives as a distinct, typed call. send_email is trivial to put a human in front of. bash -c "curl -X POST ..." is not, because you'd have to recognize it as an email-equivalent inside arbitrary shell first.

Staleness is a subtler one. A dedicated edit tool can refuse a write when the file changed since the model last read it, which catches a whole class of bugs where the agent edits a stale version. Bash has no way to enforce that invariant, because it doesn't know an edit is happening.

Rendering is the reason Claude Code makes asking the user a question its own tool rather than just printing text. As a tool, the harness can render it as a real modal with options and block the loop until there's an answer. As printed text, it's just more output the agent talks over.

And scheduling. A read-only grep or glob can be marked parallel-safe so the harness fans several out at once. The same operations through bash can't be distinguished from a parallel-unsafe git push, so the harness has to serialize everything to be safe, and you lose the concurrency.

The descriptions are prompts, too

One thing that surprises people: a tool's description is instruction for the model, not documentation for you, and the model leans on it heavily to decide when to reach for the tool. Recent models in particular reward you for being prescriptive about the trigger condition, not just describing what the tool does. "Fetches weather data" is weaker than "Call this when the user asks about current conditions or a forecast for a specific place." The second one tells the model when, and when is what it's actually deciding.

The corollary is that more tools is not better. A model facing forty tools chooses worse than one facing six, so keep the set focused, and if you genuinely have a large library, load the relevant few on demand rather than putting all of them in context at once.

A couple of things round it out. Treat every argument the model generates as untrusted input, because an injected instruction can make the model produce a malicious tool call, and sanitize before it reaches a shell or a query. And bound your tool outputs, because a tool that dumps fifty thousand tokens of result back into the loop has just poisoned the context window and torched your cache for the sake of data the model mostly won't use.

The throughline is that tool design is where you encode your control and security policy, not in the prompt and not in the model. The model is general and will try whatever its tools allow. What you allow, and what you can see and gate and parallelize when it does, is decided entirely by the shape of the interface you handed it. Design the leash before you need it, because the moment you need it is the moment it's too late to add.

#architecture#agents#tools