DEV Community

Cover image for The Agent Runtime Got Heavy: Rethinking Sandboxes with Codex App Server
Takafumi Endo
Takafumi Endo

Posted on

The Agent Runtime Got Heavy: Rethinking Sandboxes with Codex App Server

Once an AI agent moves past being a chat box — reading and writing files, installing dependencies, running tests, watching them fail and retrying — the model alone stops being a product. The agent needs a place to work:

  • a filesystem
  • a shell and process execution
  • language runtimes and libraries
  • dev tools like Git
  • long-running processes
  • execution logs and diffs
  • permissions and an approval UI
  • state that survives between sessions

On the web, the usual way to provide this is to spin up an isolated container or microVM per user. AWS, Cloudflare, and Vercel all offer good services for exactly this. But should every agent product own a cloud sandbox?

I wrote before, in The Wrapper Got Heavy, that the thing we used to dismiss as a thin "wrapper" has become a heavy runtime — a sandbox plus an agent loop plus state gravity. This post is something like the sequel. If the runtime got heavy, the next question is where you run it — in the cloud, or on the user's own machine.

Lately I've been recommending a different shape more often. For a single-user agent, I first consider running Codex or Claude Code inside a desktop app. This isn't just "local is cheaper." Moving execution onto the user's PC changes more than infra — it changes how you design file access, state, latency, and the permission UI. Treat what follows as a working thesis, not a final verdict.

TL;DR

  • For single-user AI agents, a desktop app can be a more natural runtime than a cloud sandbox.
  • You can use the user's existing environment and compute instead of provisioning a second one in the cloud, and Codex App Server lets you reuse a serious agent loop rather than rebuilding it.
  • But Codex isn't an SDK — treat it as an embedded runtime that needs version pinning and compatibility management.

Cloud sandboxes have already come a long way

Let me say it up front: cloud sandboxes aren't bad. For public services that must safely run code from many untrusted users, a strong isolation boundary in the cloud is essential.

AWS AgentCore Runtime assigns a dedicated microVM per session. Each session has its own kernel, memory, and filesystem, and on completion the microVM is destroyed and its memory sanitized. The underlying Firecracker is a KVM-based lightweight microVM with under 5 MiB of overhead per VM (AWS's Tools writeup).

Cloudflare Sandbox SDK exposes command execution, file operations, background processes, and port forwarding as an API. You do need to understand its lifecycle: by default it stops after 10 minutes of inactivity and starts fresh next time, so ordinary local disk state is lost — you push durable state out to something like R2.

Vercel Sandbox gives you an isolated Linux microVM (Firecracker) with streaming, file management, network policy, and snapshots in the SDK. Persistent sandboxes are the default: on stop the filesystem is snapshotted, and it's restored on resume.

The hard part isn't "starting a VM"

When you build a sandbox in the cloud, the hard part isn't the boot API. In a real product you also have to solve:

Area What you have to design
Lifecycle start, sleep, resume, destroy, timeout
State persistence, snapshots, caching
Transport WebSocket, SSE, reconnect, output streaming
Security network limits, secrets, privilege escalation
Placement region, cold start, session affinity
Ops logs, metrics, audit, crash recovery
Cost CPU, memory, storage, egress, idle time
UX approvals, progress, diffs, interruption, retries

For example, Cloudflare Containers can take a while to start from a stopped state, and you have to account for routing follow-up requests to the right instance — and for it possibly restarting in a different location. Even AgentCore sessions require carrying a session ID so requests reach the same microVM; lose it and you hit a cold start (microVM stickiness).

None of this is a flaw of the cloud. It's the complexity required to let many untrusted users share one fleet of servers. The question is whether a single-user desktop product actually needs that complexity.

On the desktop, the user's PC is already a finished runtime

A developer's machine already has the working repo, Git history and credentials, language runtimes and dependency caches, Docker and local databases, SSH config, an IDE, and plenty of storage, memory, and sometimes a GPU.

The cloud approach has to recreate all of that in a separate environment, keep it in sync, and persist/restore it as needed (clone, install deps, inject secrets, hand results back). Granted, there are options like Vercel Sandbox where persistence is the default, or Cloudflare's R2-based backup/restore — but the shape of "stand up a separate environment and sync it" remains. On the desktop, you just launch the agent in that directory. No network file sync, no upload to remote storage, no image build required. CPU, memory, and storage aren't metered and billed by a cloud provider to the app vendor — they're resources the user already owns.

One important caveat: even though the Codex or Claude Code process runs locally, LLM inference does not run on a local GPU. File and command operations are local; the model conversation goes over the network. A local GPU can help with workloads the agent kicks off — builds, ML, image processing — but for a single-user agent that's rarely the main draw. The bigger, more mundane wins are fast operations over large local files (parsing, indexing, searching, embedding) on the local SSD, and the user's existing toolchain working as-is.

So what a desktop setup lets the provider avoid, or greatly reduce, is the ongoing cost of sandbox CPU/memory/disk, durable storage, egress, orchestration, and monitoring infra. In return you take on distribution, updates, per-OS support, and the security design of local execution. So this isn't "local is always cheaper" — it's that the cost of compute and the burden of complexity shift from the provider to the user's machine. Even so, not having to run a per-user execution platform is a big deal, I think.

PTY brings interactive local execution into the app

When you embed a tool like Claude Code or Codex CLI in an app, calling child_process.exec() isn't always enough. These CLIs do interactive things: streaming progress, waiting for input, adapting to terminal width, ANSI control sequences, signals like Ctrl+C, and keeping long-lived processes alive.

That's where you need a PTY (pseudo-terminal). Microsoft's node-pty uses forkpty on macOS/Linux and ConPTY on Windows to make a child process believe it's attached to a real terminal. For display you can use xterm.js (the same one VS Code's integrated terminal uses). The overall shape looks roughly like this:

┌────────────────────────────────────────────┐
│ Desktop UI  (Chat / Diff / Approval / Term) │
└──────────────────┬─────────────────────────┘
                   │ restricted IPC
┌──────────────────▼─────────────────────────┐
│ Privileged Host Process                    │
│  Project / Permission / PTY / Audit        │
│  Codex App Server Client                    │
└──────────────┬─────────────────┬───────────┘
               │ stdio JSONL     │ PTY
        ┌──────▼──────┐   ┌──────▼──────────┐
        │ codex       │   │ shell / tools   │
        │ app-server  │   │ tests / servers │
        └──────┬──────┘   └─────────────────┘
               │
        OS-level sandbox → Local filesystem
Enter fullscreen mode Exit fullscreen mode

Note that talking to Codex App Server itself does not require a PTY. App Server connects over ordinary stdio (JSONL); the PTY is for user-facing terminals and interactive child processes (shells, dev servers, progress-drawing CLIs). App Server does have its own PTY-backed command-execution API, but that's separate from the connection transport.

Codex is well suited to embedding in a desktop app

The specific reason I reach for Codex is Codex App Server. It's a local server process for driving Codex's agent machinery from a custom client — not just a prompt-sending API. It handles thread creation/resume/fork/persistence, turn start/interrupt/mid-turn input, approval requests for commands and file changes, streaming of execution events and diffs, config, ChatGPT login, and integration with MCP and Skills.

App Server holds threads as a long-running child process and connects over bidirectional JSON-RPC. A single input produces many events, and when approval is needed the server asks the client and pauses the turn until it answers. The integration pattern is clean: bundle the per-OS Codex binary, pin a verified version, launch it as a child process, and exchange JSONL over stdin/stdout. Codex's own desktop app and VS Code extension use the same shape.

The connection itself is light. You launch the bundled (version-pinned) Codex binary with the app-server subcommand as a child process and connect over standard I/O — stdio is the default transport, so you don't open a network port. Then you read the child's stdout line by line as JSONL: send an initialize request once per connection, and after you get its response, send the initialized notification (it's sent by the client, not returned by the server). Then call thread/start with the working directory (cwd), a sandbox mode (e.g. workspace-write), and an approval policy (e.g. on-request). That brings up one thread, and from there events — model output, diffs, approval requests — flow back on stdout.

In practice you add request-ID-to-Promise mapping, crash handling and restarts, event ordering, backpressure, and handling turns that are suspended for approval. That last set is where the "light" first impression ends — a restart racing an in-flight turn, or the initialization flow itself, is where the real work (and the surprise I hit below) lives. You can generate types matching your bundled binary with codex app-server generate-ts, which makes it easy to pin the binary/client combination. There's also a WebSocket transport, but it's officially experimental/unsupported; inside an app, keeping ports closed and using stdio is simpler and safer.

You get Codex's own local sandbox

Running the agent locally is not the same as letting the model do anything it wants with the user's privileges. Even locally, Codex combines an OS-enforced sandbox and approval policy:

  • macOS: Seatbelt
  • Linux / WSL2: bubblewrap (bwrap) + seccomp (falling back to a bundled helper that relies on user namespaces if bwrap isn't present)
  • Windows: a native sandbox using a dedicated low-privilege user, filesystem boundaries, and the firewall when run in PowerShell; the Linux sandbox when run in WSL2

In the standard workspace-write + on-request setup, going outside the working directory or reaching the internet requires approval. Sandbox and approvals are two separate dials: the former controls what's technically possible, the latter controls when to stop and ask. You don't have to build per-OS sandboxes from scratch — the execution side rides existing boundaries. App Server also returns approval events as structured data, so instead of making users type y at a raw log, you can build a product-native approval dialog.

Even so, embedding Codex isn't simple

Having written up the upsides, I should be honest that embedding it carries a real cost of keeping up.

When I recently upgraded Codex to the 0.144 line, an implementation that had been working stopped working, and recovery took real effort. I can't be sure the cause was only a specific change on Codex's side. Around the same time, users reported cases like app-server crashing right after launch in an environment bundling 0.144.0-alpha.4, and — after a desktop-app update — trouble with in-app browser control and with how running services were handled on restart. These are user reports, not a confirmed root cause or a documented spec change, but they point at the same thing: not just the agent's protocol but its updates and process lifecycle now need verifying.

This is exactly why the earlier "pin the binary version and generate schemas from it" matters. One distinction worth drawing: backward compatibility of the JSON-RPC protocol and compatibility of the whole runtime including process management are not the same thing. Even with a stable message contract, lifecycle-level incompatibilities can bite — binary replacement mid-update, a running turn racing a restart, stale runtimes lingering, missing helper binaries.

In our own case, the root cause wasn't the JSON-RPC protocol at all — it was the initialization flow. After initializing, App Server tried to connect to Remote Control, but our embedding context had no ChatGPT auth, so it retried forever and hung — presenting as won't start / freeze / a flood of errors. And the trap: setting -c features.remote_control=false did nothing — it's treated as an unknown key, silently a no-op. The switch that actually worked was an internal environment variable, CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED=1. The config surface and the runtime's real behavior had diverged, and the only working knob was undocumented and internal — which is precisely the kind of failure you don't get from an SDK, and do get from a runtime you embed.

What I took from this: don't treat Codex App Server like an ordinary SDK. Codex is a runtime you embed in your app. You want to pin, test, and be able to roll back the binary, schema, config, launch method, and process lifecycle as a single compatibility unit. In fact OpenAI itself describes bundling per-platform binaries and pinning to a tested version in its VS Code extension and desktop app. An upgrade is something to verify and then switch to, not something to follow unconditionally — that's my current operating stance.

Separate the UI process from the execution process

"Local" doesn't mean "safe." The agent's process sits close to the user's files, credentials, SSH config, and browser data — so the app's own privilege separation matters.

With Electron, don't let the renderer touch Node.js or the PTY directly. Enable the renderer sandbox and context isolation, and keep privileged operations in the main process or a narrow preload API. Expose to the renderer only vetted operations — start a thread, approve, interrupt, subscribe — and never a way to run arbitrary shell commands or read/write arbitrary paths. Manage the PTY and App Server on the privileged side. Tauri is the same idea: define per-command permissions and capabilities and minimize what the WebView is granted.

The UX advantages of the desktop shape

More than the infra savings, I think the UX difference is what really matters.

  • The files are already there. No waiting on upload or sync; you work directly against the current tree, including uncommitted diffs and local-only config.
  • You reproduce in the same environment. A problem that only happens on the user's machine can be investigated with the same OS, deps, and local services.
  • Big file operations are fast. Search and Git-history walks finish on the local SSD, without one HTTP call per operation.
  • Existing tools just work. The user's already-configured linters, test runners, Docker, Xcode, Android SDK, and so on are all available.
  • Approvals are easier to make meaningful. You can tie an approval for a write target, a network destination, or a command to the OS file picker and notifications.

The desktop shape has clear weaknesses too

It isn't always the right shape. Objections I'd raise myself:

  • If the machine stops, the agent stops. Sleep, power loss, and disconnects all hit it. Claude Code's Remote Control lets you steer from another device while execution stays local, but that's different from a cloud environment that keeps running while your machine is off. For long-running work you can combine SSH or a managed remote environment and split UI from execution.
  • Environment variance is large. Shell, paths, permissions, PTY, and package management differ per OS. "You can leverage the user's environment" and "you can't reproduce it" are two sides of the same coin.
  • Distribution and updates become a compatibility problem of their own. A cloud sandbox lets the provider update the runtime centrally, in one place. On the desktop you need a delivery pipeline that pins — and can roll back — the binary, schema, config, and launch method as one compatibility unit, and threads that unit through auto-update, not just development. (We track Codex versions with a weekly workflow and a lockfile-pinned version.)
  • You're close to secrets. A local agent can reach more sensitive data than a disposable cloud environment. Don't skip the working-directory boundary, network limits, approvals, and audit logs.
  • Not for running anonymous users' code. Playgrounds, online IDEs, coding tests, generated-UI previews and the like still need a cloud sandbox.

Which should you choose

Roughly, I'd think about it like this:

Requirement Better fit
A single user operating their own files Desktop
Leverage an existing local dev environment Desktop
Handle lots of local files fast Desktop
Investigate a problem specific to the user's machine Desktop
Keep running while the machine is off Cloud
Run anonymous users' code Cloud
Guarantee an identical environment for everyone Cloud
Update the runtime centrally, without per-client compatibility work Cloud
Use centrally managed secrets Cloud / managed remote env
Concentrate high-end GPU usage Cloud / dedicated remote env

In a real product, a hybrid is natural: everyday work on Desktop + Local Codex, long or heavy jobs on a remote environment or Cloud Sandbox, untrusted code in a Cloud Sandbox, review and approval in the Desktop UI.

There's another axis worth using: the client form factor. Even within one product, I'd lean the desktop app toward a serious runtime — the side that embeds the heavy local work environment — and lean the web browser toward lightweight features — pushing heavy execution to a cloud sandbox and keeping the browser to viewing, approvals, and light tasks. The point is to separate where the heavy runtime lives from where you keep only a light surface.

This continues the argument from The Wrapper Got Heavy. Now that the runtime is heavy, the design question isn't "which client calls the model" but "where does that heavy runtime live, and where does the state gather?" By state gravity I mean the pull that draws the runtime toward wherever the state — the repo, dependencies, auth, caches, running processes — already collects. Seen that way, heavy execution belongs where the hardware and state naturally gather — the desktop app — while the browser stays a light window onto it.

Wrapping up

A capable agent needs a runtime with files, a shell, libraries, processes, and state. Providing that on the web means a large substrate — microVMs or containers, durable storage, network controls, lifecycle management — and AWS, Cloudflare, and Vercel provide it powerfully.

For a single-user desktop app, though, the user's PC is already a high-performance, persistent agent environment. Launch Codex or Claude Code from a desktop app with PTY support and you reuse the existing files, tools, caches, and compute. And Codex App Server exposes threads, events, diffs, approvals, auth, and config over bidirectional JSON-RPC, with embedding into local apps and IDEs officially in scope. That Codex carries its own OS-level sandbox is another implementation win.

Before building a cloud sandbox, it's worth asking once: does this agent really need to run on the provider's servers? If it operates the user's own files and tools, the first candidate might not be a web app plus a microVM. Desktop × PTY × Codex is becoming a practical default for this class of agent — at least, that's my current view.

Service and Codex App Server specifics reference the official docs as of July 2026; this area moves fast, so check the latest before relying on any detail.

Top comments (0)