Skip to content
Go back

Where Should Your Coding Agent Run?

By SumGuy 13 min read
Where Should Your Coding Agent Run?
Contents

The Question Nobody Asks About Their Coding Agent

Every conversation about Claude Code, Aider, Cline, or whatever agent you’ve bolted onto your editor this month turns into a model bake-off. Sonnet versus Opus versus Qwen3-Coder, who writes cleaner diffs, who hallucinates fewer imports. That’s the wrong argument. The model picks the words. What actually determines how bad your day gets when it picks the wrong words is where the agent’s hands are: what filesystem it can write to, what processes it can spawn, and what’s sitting within reach when it decides rm -rf is a reasonable way to “clean up the build directory.”

I’ve looked at a lot of setups people use to run these things, and almost nobody thinks about this until after the incident. So let’s do it now, before you’re the one explaining to your team why the agent pushed a force-push to main at 2 AM because it had your real git credentials and a theory.

There are four real options: your machine directly, a container on your machine, a throwaway VM, and (the genuinely new one) an isolate running at the edge inside Cloudflare’s Durable Objects. Each one trades capability for safety in a different place, and one of them is a much worse trade than its marketing suggests.

Option 1: Your Laptop, Bare Metal, Full YOLO

This is what most people actually do, and I’m not going to pretend otherwise. You open a terminal in your project directory, run claude or aider, and let it loose. Full POSIX shell, your real SSH keys sitting in ~/.ssh, your AWS credentials in ~/.aws/credentials, your kubeconfig pointed at a cluster with real customers on it.

The upside is real: zero setup, zero latency, the agent can run your actual test suite, compile your actual Rust crate, hit your actual local Postgres. The feedback loop is as tight as it gets.

The downside is also real, and it’s not hypothetical. An agent that can run npm install can also run curl | sh from a typosquatted package, and it will do so with your user’s full permissions. A single bad tool call doesn’t touch a scoped sandbox, it touches your dotfiles, your credentials, your browser’s saved sessions if it goes looking. This is the equivalent of handing a new hire the keys to the server room on day one because training takes too long. It works right up until it doesn’t, and then it works spectacularly badly.

If you do this (and plenty of you will keep doing this), at least don’t also leave cloud credentials with production write access lying around in plaintext env vars in that same shell.

Option 2: A Docker Container on Your Own Machine

This is the sane default for almost everyone, and it’s cheap enough that there’s no excuse not to. Bind-mount the repo into a container, give the agent a shell inside it, and now a bad rm -rf deletes a throwaway filesystem instead of your home directory.

Here’s a Dockerfile that runs as a non-root user instead of the default root, which matters more than people think:

Dockerfile
FROM node:22-bookworm-slim
# The official node images already ship a non-root "node" user at uid 1000.
# Adding your own there fails the build, so just use the one that's there.
USER node
WORKDIR /workspace

And here’s the docker run invocation that actually locks it down, not just wraps it in a container for cosmetic reasons:

Terminal window
docker run --rm -it \
--network none \
--read-only \
--tmpfs /tmp:rw,nosuid,size=128m \
--tmpfs /home/node:rw,nosuid,size=256m,uid=1000,gid=1000,mode=0700 \
--cap-drop ALL \
--security-opt no-new-privileges \
--pids-limit 256 \
--memory 2g \
-u 1000:1000 \
-v "$(pwd)":/workspace:rw \
agent-sandbox:latest

Walk through what each flag buys you: --network none means the agent can’t reach anything outside the container, no LAN, no internet, no exfiltrating your .env file to a webhook. --read-only means the image’s own filesystem is frozen, so the only writable paths are the tmpfs mounts and the bind mount you explicitly gave it. --cap-drop ALL and --security-opt no-new-privileges close off the kernel capabilities a container escape would normally reach for. -u 1000:1000 means even inside the container, it isn’t root. None of this is exotic, it’s just flags most people never bother setting because the happy path works without them.

The --tmpfs /home/node line is the one people leave out, and then spend an hour confused. --read-only freezes the home directory along with everything else, so the first thing your agent runs writes to ~/.npm (or ~/.cache, or ~/.local) and dies with a read-only filesystem error that has nothing to do with your actual code. The uid=1000,gid=1000,mode=0700 part earns its keep too: a tmpfs mount arrives owned by root at mode 755, so adding the mount without those options just swaps your read-only error for a permission-denied error. Docker special-cases /tmp to mode 1777, which is why that one works with no help. Same reason to skip noexec on /tmp here: plenty of toolchains extract a helper binary to a temp directory and execute it, esbuild and node-gyp among them, so noexec buys you a little hardening and a lot of confusing build failures.

The Docker Socket Is a Skeleton Key, Never Hand It Over

If you’ve ever mounted /var/run/docker.sock into a container so the agent could “manage its own containers” or run Docker-in-Docker for a build step, undo that right now. A container with access to the host’s Docker socket can spin up a new container with --privileged and a bind mount of /, which is a fully generic root-on-host escape. It doesn’t matter how locked down the rest of your flags are. Handing an agent the Docker socket is like giving your teenager the car keys, the garage remote, and a note explaining you’ll be out of town all weekend. The isolation you set up two paragraphs ago stops mattering entirely.

Cutting the Network When the Agent Doesn’t Need It

--network none above is the correct default for anything that’s pure code editing and local test runs. Where it gets awkward is npm install or pip install, which need to reach a registry. The honest answer is: don’t try to thread that needle inside the container’s own network stack. Docker’s default bridge network still routes outbound to the internet, so a “custom bridge network” alone buys you nothing against exfiltration, it just changes the subnet. If the agent genuinely needs package installs, do that as a separate, human-triggered step outside the agent’s turn (docker run --rm -u "$(id -u):$(id -g)" -e npm_config_cache=/tmp/.npm -v "$(pwd)":/workspace -w /workspace node:22-bookworm-slim npm ci), then hand the agent the already-populated node_modules on a network-isolated re-run. Keep the two phases separate instead of trusting the container to police its own egress.

Option 3: A Throwaway VM or Cloud Sandbox

Step up a layer and you get a real kernel boundary instead of a shared-kernel container boundary. Spin up a cheap VPS, or a Firecracker microVM if you want the fast-boot version of the same idea, run the agent unattended overnight, and terraform destroy it in the morning regardless of what happened.

This is the right call for genuinely unattended runs, the kind where you kick off a multi-hour refactor before bed and don’t want to babysit it. A container escape is rare but not impossible; a VM escape is rare enough that most of us will never see one in a home lab lifetime. The isolation is closer to “actually isolated” than “isolated as long as the kernel behaves.”

The cost is latency and money. Boot time is seconds to a couple of minutes depending on image size, not the near-instant docker run you get locally. And unlike a stopped container, a stopped VM you forgot to tear down is still costing you something every hour, even doing nothing. This is your forklift option: total overkill for moving one couch, exactly right when you’ve got a whole warehouse of pallets to shift unattended while you sleep.

Option 4: An Edge Isolate, Running an Agent Inside a Cloudflare Worker

This is the interesting one, and it’s newer than the other three by years. I’ve seen a published writeup describing a port of the portable core of the Pi coding agent (packaged as @earendil-works/pi-agent-core and @earendil-works/pi-ai) to run entirely inside Cloudflare Workers, with one Durable Object per session owning the conversation transcript and a virtual filesystem persisted in that Durable Object’s own SQLite storage. Instead of spawning a real shell to run npm install or vite build, the setup bundles the generated app in-memory and previews it by dynamically loading it into a short-lived Worker.

What It Actually Wins

Durable Objects hibernate when idle and keep their state in that per-object SQLite store, so a session that isn’t actively being used costs you nothing while it waits. There’s no VM to boot and no container to warm up, the whole thing is reachable the moment you open a URL, from a phone, from a Chromebook, from whatever’s in front of you. And because each Durable Object is single-threaded by design, you get a strict guarantee that two prompts can’t race against the same session’s state the way two shells on the same bind mount absolutely can.

What It Actually Loses

“Zero idle cost” sounds like the killer feature until you notice it’s solving a problem you didn’t have. Nobody’s monthly agent bill is dominated by an idle VM sitting around between sessions, containers stop for free and VMs cost cents an hour. The real cost of running a coding agent is model tokens, and tokens cost exactly the same no matter which of these four options is holding the harness. Stored transcript and filesystem state in that SQLite store still gets billed. “Runs at zero cost while idle” is true and mostly beside the point.

The much bigger loss: there’s no POSIX shell in there. No npm install against arbitrary registries, no running your real Jest or pytest suite, no compiling C or Rust, no custom Webpack or Vite plugin doing something clever. The published version is scoped to a pinned React-plus-Worker template, which is a fine sandbox for “generate a small app from scratch” and useless for “fix this bug in my existing 40,000-line monorepo.” An agent that can’t execute your actual build and test pipeline can’t tell you whether it broke anything, it can only tell you the code looks plausible. Those are very different guarantees, and “looks plausible” is exactly the failure mode that gets you paged.

The Security Trade-offs Nobody’s Highlighting

This is the part that deserves way more attention than it’s getting. As described in that writeup, this is explicitly a single-user prototype: session identifiers are UUIDs that isolate one session’s storage from another’s, but they provide no actual authorization check, so anyone who has or guesses a session URL has full access to it. Tool calls execute automatically with no human approval gate in the loop. And the Worker holds an API token scoped with Workers Scripts Edit permission so it can publish live Workers under the account owner’s name, meaning the agent’s own tool calls can push new code straight to production infrastructure.

Stack those three facts and you get an internet-reachable endpoint that runs unattended tool calls and can deploy to prod on its own authority. That’s not a footnote, that’s the headline. Cloudflare Access in front of it is the only thing standing between “cool demo” and “someone else’s agent session is now your Worker fleet.” One more egress detail: the preview sandbox for generated apps keeps outbound fetch() available even while stripping other bindings, which is a live path out for anything the generated code decides to phone home to.

And practically: this requires a paid Cloudflare plan and enrollment in a beta program, so most of you reading this can’t spin up your own copy today even if you wanted to. File it under “architecture worth understanding,” not “thing you deploy this weekend.”

The Comparison Table

HostRuns your real test suiteBlast radius if the agent is wrongIdle costSetup effortWorks from your phone
Laptop, bare metalYesYour real filesystem, SSH keys, cloud credentialsNone, it’s your machineNoneNo
Docker container, own machineYes, full shellScoped to the bind mount and whatever flags you skippedNone once stoppedLow, one DockerfileNot without a tunnel
Throwaway VM / cloud sandboxYes, full kernelScoped to the VM, gone on destroyCents per hour while runningMedium, image plus provisioningSort of, via an SSH client
Cloudflare Worker + Durable ObjectNo, no shell at allScoped to the Worker and DO storage, but internet-reachable and can self-deployZero while hibernatedHigh, beta access, paid plan, pinned templateYes, it’s just a URL

So Where Should Yours Actually Run

If you’re doing normal day-to-day work on a repo you care about, run the agent in a container on your own machine with the network cut and the socket nowhere near it. It’s cheap, it keeps your real shell and test suite available, and it turns “agent deletes something important” into “agent deletes something in a directory you can rm -rf and forget about.” That’s the default for almost everyone reading this.

Reach for a throwaway VM when you’re letting the agent run unattended for hours, the kind of job where you don’t want to be the one approving every step at 1 AM. The extra boot latency and the few cents an hour are a fair price for an actual kernel boundary.

Running an agent directly on your laptop with full shell and real credentials is fine only if you’ve genuinely accepted that a bad tool call touches your actual life, not a sandboxed copy of it. Some of you will keep doing this anyway. At least know what you’re accepting.

The edge isolate is a legitimately interesting piece of architecture, hibernating state, single-threaded session isolation, reachable from a phone with no local toolchain. It’s also the wrong tool the moment your project has a real test suite, a real build step, or any dependency your agent needs to install from scratch. Use it for demos and greenfield toy apps. Don’t mistake “no idle cost” for “no cost,” and don’t point it at anything with a Workers Scripts Edit token unless there’s an auth layer sitting in front of it.

Pick based on what the agent actually needs to touch, not on which option sounded coolest in a blog post. This one included.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Mise Deep Dive: Tasks, Tools, Env Vars
Next Post
Linux PSI: The Pressure Metrics Load Average Wishes It Were

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts