Back to blog
·by Patrick Hofmann

Every Agent Installed Its Own bun at Spawn

Per-agent tooling isolation sounded like security. On a single-user host it was 100MB and a minute of spawn time that isolated nothing. What fell away when I took the real dividing line seriously.

OpenApeAI AgentsInfrastructureToolingBuilding in Public

I watched a spawn. From apes agents spawn until the agent actually answered a chat message, about sixty seconds passed. Most of that the setup script spent downloading bun and installing it into the agent's fresh home. Every time. Per agent.

That was built that way, and I had built it that way myself. Here's why I took it out again.

How it was before

At spawn an agent gets its own macOS user and its own home directory. The setup script then pulled its own bun runtime into that home — about 100MB, a good minute of work. After that the agent ran entirely on its own toolchain, without sharing anything with the host or other agents.

The assumption behind it: agents have to be isolated from each other, and isolation means the tooling isn't shared either. Each its own runtime. No shared binary an agent could change in a way that hits another. Isolation by default, down to the runtime.

That sounded right when I wrote it. It also wasn't wrong — just in the wrong place.

Why I took it out

On a single-user host, node, apes and the chat bridge are there anyway. They sit in the system, the user installed them, they don't change because an agent doesn't touch them. An own bun per agent duplicates a file that already exists, into a directory nobody else reads.

The actual dividing line between two agents on this machine isn't their runtime. It's the macOS uid and the privilege helper (escapes), which waves every privileged crossing through individually. That is the isolation boundary. Copying everything in front of it protects against nothing the uid doesn't already cover — it only costs spawn time and disk space.

I had built tooling isolation as if isolation were a stack of copies. But it's a boundary. And the boundary was elsewhere.

How it looks now

Instead of installing a runtime per agent, the setup captures where the tools sit on the host and bakes those paths into the agent's launchd plist:

# Capture the host's tool locations once, dedupe, bake into the plist PATH
agent_path=$(
  for bin in node openape-chat-bridge apes; do
    cmd=$(command -v "$bin" 2>/dev/null) || continue
    dirname "$cmd"
  done | awk '!seen[$0]++' | paste -sd: -
)

command -v for each of the three tools, dirname on the result, awk '!seen[$0]++' as dedupe (two tools often sit in the same directory), paste into one PATH string. That lands in the EnvironmentVariables of the plist, and the agent finds its tools where the host has them.

The spawn falls from about sixty seconds to about four. The four seconds aren't the tooling anyway, that's the rest of the setup. Per agent the 100MB fall away entirely.

What fell away

The per-agent bun installation. Completely.

The honest trade-off I don't want to hide: all agents now share one bridge version. If I update the bridge on the host, it changes for all of them at once. In a multi-tenant setup, where foreign agents run side by side, that would be the wrong decision — there you do want to keep the tooling versions apart. On my single-user host it's exactly what I want: one update, all current.

The client-side reflex would have been to leave the isolation in and sell it as a feature. It just did nothing.

The cut

isolation by default sounds like a safe default. It was none — it was an assumption from the time when one agent ran on the machine and isolated simply meant duplicates everything. As soon as the question gets concrete — isolation from what, against whom, at which boundary — the answer falls elsewhere: the boundary is the uid and the escapes helper, not the copied-in bun. What lies in front of that isn't security, but friction with a security coat of paint.

Defaults carry the world they were written in with them. The dangerous thing about by default isn't the default — it's that you stop asking what it was there for.