Blog

How to sandbox an AI coding agent on macOS

Every sandbox answers the same question: if this agent does the worst thing it could do, what breaks? They differ in how much of your working setup you give up to get that answer, and they agree on one blind spot.

On macOS there are four practical ways to sandbox a coding agent, in rising order of isolation and cost. Seatbelt (sandbox-exec) confines a process with a kernel policy and no container at all, which is what Claude Code and Codex use for their own built-in sandboxes. A devcontainer puts the agent in a container, so the blast radius is a rebuild. Docker Sandboxes give each agent a disposable microVM with its own filesystem and network stack. A full VM such as Lima isolates most completely and costs the most in setup and speed. None of the four stops a credential the agent is allowed to use from being sent somewhere it should not go, because containing a process is not the same as containing a key.

The short version

Pick by how much of your working setup you are willing to rebuild inside the box.

  • Want isolation with no container and no setup: Seatbelt, via the sandbox your agent already ships.
  • Want a clean, reproducible environment your team shares: a devcontainer.
  • Want each agent to get a disposable machine and you already run Docker: Docker Sandboxes.
  • Want the strongest boundary and do not mind the cost: a Linux VM on macOS, such as Lima.
  • Want the agent to keep working on your real machine, with your real tools, and the keys out of its reach: that is a different mechanism, and it is the last section.

What a sandbox is actually for

A sandbox answers one question: if this agent does the worst thing it could do, what breaks. It is a blast radius control. It is not a prediction that the agent will misbehave, and the useful cases are almost never malice.

The ordinary case is thoroughness. An agent asked to clean up a repository runs a delete that is correct inside the repository and wrong one directory up. An agent debugging a build reads the whole home directory looking for a config. A dependency's postinstall hook runs during a routine install and does something nobody reviewed. None of that requires a bad actor, and all of it is contained by a sandbox.

That is worth being precise about, because it decides how much cost is reasonable. If you are defending against a hostile binary, you want a VM. If you are containing an enthusiastic agent, a kernel policy on the shell is enough and costs you almost nothing.

Seatbelt: the macOS primitive

macOS ships a sandbox in the kernel. The command is /usr/bin/sandbox-exec, the policy language is SBPL, and the profile is applied when the process starts and inherited by every descendant. Python, node, a build script, a postinstall hook and an agent's own tool calls are all bound by whatever the shell was started under.

Two properties make it the right primitive for this job. It needs no entitlement, no system extension and no daemon, so nothing has to be installed and nothing runs privileged. And it cannot be lifted from inside: a process under a profile that asks for a wider one gets sandbox_apply: Operation not permitted from the kernel. That is what separates a boundary from an honour system.

It has two real limits. Apple's man page has called sandbox-exec deprecated for years while every sandbox on the platform continues to use it, including the ones Claude Code and Codex ship. And profiles do not nest: once a process is under a profile that contains any deny rule at all, starting a second sandbox inside it fails outright. That second limit matters more than it sounds, and the next section is where it bites.

The sandbox your agent already ships

Claude Code and Codex both ship a sandbox, and if you have not turned it on, that is the cheapest thing on this page. Codex exposes sandbox modes and a read-directory allowance. Claude Code applies a profile to the work it runs.

Because profiles do not nest, this decides something about everything else you might layer on. If you wrap the whole terminal in your own Seatbelt profile, the agent's own sandbox stops working inside it, and you have traded a boundary the vendor maintains for one you wrote. Wrapping the pane is the intuitive move and it is the wrong one.

The pattern that works is to confine narrowly and let the agent keep its own sandbox: bound the folders, or bound the one process that received a credential, and leave the shell the agent starts in unprofiled.

Containers and VMs

A devcontainer moves the agent into a container, and the blast radius becomes a rebuild. The cost is that the environment inside the container is now a thing you maintain: your shell config, your tools, your credentials and your editor integration all have to exist twice, and the ones you forget are the ones the agent will ask for.

Docker Sandboxes, which Docker shipped in 2026, give each agent a disposable microVM with its own filesystem, its own Docker daemon and an isolated network stack. It is the most complete answer in the container family and it has the same tax. Lima, a CNCF project, runs Linux VMs on macOS and isolates further still.

The honest trade across all three is the same. Isolation is proportional to how much of your machine the agent no longer has, and your machine is where your work is. That is a real trade and for some workloads it is obviously right, particularly anything running unattended against code you did not write. It is a poor fit for the case most people are actually in, which is an agent working on their own repository with their own tools.

Where isolation stops being the answer

Be precise about what these do and do not claim, because the good ones claim more than you might expect. Docker Sandboxes lists credentials alongside filesystem and network as something it isolates, and that is a real feature: it decides which credentials exist inside the box at all, which is worth having and is more than a devcontainer gives you by default.

What no boundary on this page does is stop a credential the agent is PERMITTED to use from going somewhere it should not. If the agent is allowed to deploy, the deploy key is inside the box with it by design, and one permitted destination is enough: a value can leave inside an ordinary request aimed at the very host it belongs to, and nothing here reads a payload. Scoping which keys are in the room is a different problem from what happens to the one you deliberately put there.

That second problem is answered by the value never being in the agent's hands at all: the command that needs the credential receives it, the agent receives the result, and the key is not in the prompt, the command line or the transcript to be sent anywhere. That is a substitution mechanism rather than an isolation one, and the two compose fine. Use both.

One footnote that catches people, and it is documented rather than theoretical: a git worktree is not credential isolation either. Anthropic's own worktree documentation describes a .worktreeinclude file whose purpose is to copy gitignored files such as .env and .env.local into every worktree Claude Code creates, because a fresh checkout would not otherwise have them. The isolation is of edits, deliberately, and the credentials are copied in on purpose.

Where Forkbench sits

Forkbench is a Mac app for running coding agents, and it uses the macOS primitive rather than a container, in three narrow places.

A Thread can be locked to a set of folders. Every shell in that Thread is started under a generated Seatbelt profile that denies reads and writes under /Users, /Volumes and /Network apart from the folders you allowed, and every process the shell spawns inherits it. File metadata stays readable, so stat, path resolution and shell completion still work, but a denied folder cannot be listed or opened. System paths and Homebrew stay readable, so builds still run. The network is untouched by this lock, because it is a lock on folders and says nothing about the wire. It applies as a shell starts, so locking a Thread offers to restart its terminals, and a running shell cannot be sealed after the fact.

When a command uses a key from the Vault, the child process that receives it is started under a second profile that denies outbound sockets except loopback. That makes the local proxy the only road out, so a program that ignores the proxy variables has nowhere to dial, and the key goes to the one destination it was bound to. The honest ceiling: this is containment and evidence, not isolation. One allowed destination is still a destination, and nothing here inspects a payload.

And a single pane can be cut off from the Thread it sits in, so its agent gets no board, no notebooks and no Vault, while the shell, the files and the git panel carry on as normal. That switch is live in both directions, and it is a scoping tool for the human rather than a jail for a malicious binary.

None of that is what makes the agent safe to run flat out. What does is that the key it needs is used by the command and never handed to the agent, which is why the folder lock can stay narrow and the shell can stay yours.

Related: What is sealed, what is not, and where the boundary stops

Frequently asked

Keep reading