Background and the Push to Ditch VMs
We recently finished moving the camelAI agent off of virtual machines. The agent now runs inside Cloudflare Durable Objects, its filesystem lives in SQLite and R2, and it writes JavaScript instead of bash. Most teams run coding agents in a full Linux VM or container sandbox, and we used to as well.
We wanted off VMs because giving every user an always-on machine with attached disk was too expensive to scale. The hard part is that coding agents assume Linux. They are trained to reach for bash, and the harness we launched on required a full VM, so getting here took three redesigns. The tradeoff is that the agent can now only do things we've built an explicit method for, which sounds limiting but has been an exceptionally good trade-off.
I'm Miguel, CTO of camelAI. Our codebase recently went open source, so all designs and implementations in this post can be found in our repository. Here is the detailed evolution progression.
Step Zero: The VM Era and Self-Built Container Services
We launched on the Claude Code harness, which needs a full virtual machine to run. We tried several VM providers, none of them fit our persistence and performance requirements, and we ended up building our own container service. (That post is still up, but we no longer run any of that infrastructure.)
The container service worked, but it was heavy. An always-on VM for every user is expensive, and so is holding every user's files on fast attached disk. Scaling it means scaling real machines with real disks, which was going to be prohibitively expensive at the user counts we're aiming for. So instead of getting clever about VM orchestration, we started designing around not needing a VM at all.
Step One: Decoupling the Brain into Durable Objects
The Claude Code harness is inseparable from its VM, so the first move was building our own harness. We built it on pi, Mario Zechner's open-source coding agent. pi is a stack of libraries. The highest layer assumes a normal operating system, but the lower layers give you the agent primitives, like the agent loop and state management, without caring where they run. We didn't change any pi code. We imported those lower layers and built our own harness on top of them, running inside a Cloudflare Durable Object instead of a Linux environment.
A Durable Object is a small stateful compute instance that spins up on Cloudflare's edge, close to the user who created it. Each chat thread gets its own Durable Object, which brought latency down on its own compared to routing everything through a centralized VM host.
At this stage we kept the VMs, but the agent no longer lived inside one. It called into the VM remotely when it needed to run commands. Anthropic describes this same split for its managed agents, the brain separated from the hands, which gave us remarkable advantages:
- Zero-wait response: The agent starts responding before the VM is awake without waiting for machine boot.
- On-demand sleep: The VM can go back to sleep while the agent keeps working, or never wake up if no commands are needed.
- One brain to multiple hands: A single agent brain could operate several VMs simultaneously.
Remote Control Pipeline Separating Brain from Hands
-
Durable Object Processing
The agent brain initializes in milliseconds on edge DO nodes and streams responses.
-
Remote Command Dispatch
Sends remote control execution requests to remote VMs only when running terminal commands.
-
Immediate Sleep Upon Completion
The VM immediately enters sleep after returning execution results to save compute overhead.
We call those hands projects. Each project came with a VM for executing commands and a git repo created programmatically through Cloudflare Artifacts (git-compatible storage provisioned on the fly from Workers). The agent didn't really know it was running outside the VM—it still had bash and worked like any other coding agent.
The problem is that this fixed latency and nothing else. We still had a VM per user, so all cost and scaling bottlenecks remained.
Step Two: Removing VMs with SQLite + R2 Filesystem
The next version kept the same project structure but dropped the VM behind it. Each project is now backed by a filesystem inside a Durable Object, with R2 behind it for larger files.
We didn't invent this. Cloudflare's agents team built Shell (an experimental filesystem and execution runtime for Workers), and we reused their code heavily. The mechanics are simple: A Durable Object's storage is a SQLite database with a 10 GB cap, and each row has a maximum size. Small files live directly in SQLite rows; files over roughly 1.5 MB get written to R2, and the SQLite row just holds a pointer. To the agent it looks like a normal filesystem, but underneath it's a database and object storage hybrid—persistence is stored data rather than infrastructure we have to keep alive.
Version history still runs through Artifacts, so every project keeps a git history without us hosting a git server.
Step Three: Replacing Bash with a JavaScript Sandbox
Removing bash felt drastic. Coding agents are trained to reach for bash, and bash is why everyone runs them in VMs in the first place. It was also a problem beyond cost: An agent with bash and network access needs credentials to do anything useful, and our attempts at authenticated proxy URLs were getting hacky and hard to enforce securely.
So we removed it. Instead of bash, the agent writes JavaScript, executed through Code Mode and Cloudflare's dynamic Worker loaders. Each execution runs in a fresh V8 isolate that boots in milliseconds and uses a few megabytes of memory. The sandbox comes pre-loaded with user data connections and explicit methods for platform capabilities, ensuring credentials never enter the sandbox—the agent calls connection methods, and authentication happens on our backend.
Looking at what agents actually use bash for, losing it costs less than expected. Most of it is file operations, which the agent has native tools for (read, write, edit, plus custom grep and glob implementations). That covers 80% of needs. The remaining 20% special cases became explicit methods:
- Deployment Flow: wrangler deploy through a proxy became a fully controlled deploy_project method. Knowing exact deployment timing allows us to hook it and open a live preview automatically, whereas before we had to guess by sniffing proxied wrangler traffic.
- App Building and Notebook Execution: Building user apps and running Python notebooks became distinct APIs, both backed by short-lived containers.
We kept containers for those two jobs because they genuinely need Linux. User apps use Vite, Tailwind, and React Router, and adding dependencies requires bun install. Building inside a Worker was considered, but Workers have a 128 MB memory limit and fraction of a CPU, causing builds to stall or OOM. Instead, a build spins up a container via Cloudflare Sandbox SDK, copies the project in, runs the job, returns the result, and shuts the container down immediately. Notebook runs work the same way. We still use full Linux, but only for the seconds actually requiring it.
The honest downside is anticipating what the agent needs. With bash it could figure things out independently; now, missing capabilities must be added manually. In practice, this constraint benefited the product by forcing us to design first-class paths rather than letting the agent improvise.
New Architecture Benefits and Summary
Our current stack consists of Durable Objects for the agent and filesystem, R2 for large files, Artifacts for git history, pi as the harness, and Code Mode with dynamic Workers for execution. It deploys like any Cloudflare app with zero external container infrastructure to manage.
Dynamic Workers bill per execution rather than uptime. Thousands of executions cost what a few minutes of container time used to. Everything runs on the edge near users for minimal latency, leaving scaling to Cloudflare's infrastructure.
From the user's perspective, they build and deploy full-stack apps to live URLs, and the agent reads, writes, greps, and deploys without friction. Nothing degraded—it only got faster.