Guide · Sandboxing

Sandbox AI coding agents

Let Claude Code, Codex, Antigravity CLI, and OpenCode work without permission prompts, inside a VM that only sees your Baton worktrees and only reaches the network you allow.

macOS Lima VM

A managed Debian VM on Apple's Virtualization framework, with network controls built in. Safehouse is a lighter option that limits file access only.

Linux Lima VM Experimental

The same managed VM, running on QEMU. Or bring your own wrapper, such as Docker, Podman, or bubblewrap.

Windows Custom wrapper

Lima isn't available on Windows. Run agents in a dedicated WSL2 distro or a container through a custom wrapper.

On this page
  1. Why sandbox an agent
  2. Compare sandbox methods
  3. Lima VM
  4. Network sandboxing
  5. Outbound allowlist
  6. Root access
  7. Resources and software
  8. Safehouse
  9. Custom wrappers
  10. WSL2 on Windows
  11. Choosing where a workspace runs
  12. FAQ

Why sandbox an agent

Agents get the most done when they don't stop to ask before every command. That's what Claude Code's --dangerously-skip-permissions and the equivalent modes in other agents are for. The catch: the agent then runs any command as you. It can delete files, read ~/.ssh and cloud credentials, and send data anywhere. A prompt injection hidden in a README, an issue, or a web page can steer it there.

A sandbox limits the damage to three things you control:

A sandbox contains the agent, not the code it writes. The agent edits your real worktrees. When you open, build, or run that code on your computer (an npm install, a test run, a script), it runs as you, outside the sandbox. Review changes before you run them on the host.

Compare sandbox methods

Baton manages the Lima VM for you, offers Safehouse as a ready-made template, and runs any other tool as a custom wrapper. Agents' own permission systems are a reasonable baseline when you're watching closely.

Feature Lima VM
Recommended
Safehouse Custom wrapper Agent's own
Isolation Separate kernel (VM) Same kernel, macOS sandbox profile Depends on the tool Same kernel, enforced by the agent
Network controls Block host and LAN; domain allowlist — You set them up Varies by agent
Safe without permission prompts ✓ Files only Depends on the tool ✗ skipping prompts turns it off
Notifications and MCP One click per agent Uses your host setup You configure them Uses your host setup
Platforms macOS, Linux (experimental) macOS All All

Agents' own sandboxes. Claude Code's permission rules, Codex's sandbox modes, and Antigravity CLI's approval prompts all restrict the agent on your host, and the details differ per agent, so check its docs. They work well for supervised sessions, but any "skip permissions" mode switches them off. That's the gap a VM fills.

Lima VM

Baton creates and manages a Debian 13 virtual machine with Lima. It uses Apple's Virtualization framework on macOS and QEMU on Linux, where support is still experimental. One VM, baton-vm, serves every sandboxed workspace. Agents inside it see only Baton's worktrees, can't become root, and reach only the networks the firewall allows.

Set up the VM

  1. Install Lima.

    On macOS, run brew install lima. On Linux, install Lima and QEMU from your package manager, for example apt install lima or pacman -S lima.

  2. Create the VM.

    Open Settings → Sandboxes, expand Lima VM, and review resources, security, and software. Click Create and start VM. The first run downloads a Debian image of about 300 MB and installs the tools you picked.

  3. Enable agent integrations.

    On the VM created screen, click Enable next to Notifications and MCP Server for each agent you use. These install Baton's hooks inside the VM so finished turns and input requests show up in Baton, and agents can launch workspaces. You can change them later under Settings → Integrations & MCP on the Lima VM tab.

  4. Sign in to your agents inside the VM.

    Logins on your computer aren't copied into the VM. Open a sandboxed workspace and sign in once, for example with /login in Claude Code. The login stays in the VM until you delete it.

  5. Start a workspace.

    Once the VM exists, new workspaces start in it. To run one on your computer instead, switch the environment in the New Workspace dialog. See Choosing where a workspace runs.

By default the VM starts when Baton opens and stops when Baton quits, so it's ready when you are and not using memory otherwise. Both switches are in the VM Status card on the Lima VM settings page.

What the agent can see

Access Paths
Read and write ~/.baton/worktrees, ~/.baton/repos, and ~/.baton/vm-logs
Read only ~/.baton/settings, ~/.baton/skills, and ~/.baton/vm-shared (the Baton CLI and helpers)
Not visible Everything else, including your home folder, ~/.ssh, cloud credentials, and your Baton license

To share another folder, add it under Filesystem access on the Lima VM settings page. It appears at the same path inside the VM, is read-only unless you choose read/write, and takes effect after a VM restart.

Default settings

The defaults keep the agent off your computer and local network while leaving the internet open. Turn on outbound blocking when you want an allowlist.

Setting Default What it does
Restrict sudo On No root for the agent. Set when the VM is created.
Block host machine On The VM can't reach services on your computer, apart from exemptions.
Block local networks On No access to your LAN, router, or cloud metadata endpoints.
Port forwarding On Dev servers in the VM open on your computer's localhost.
Block outbound traffic Off When on, only allowlisted domains and your custom rules are reachable.
Restrict user DNS Off When on, the agent can't make DNS lookups. Needs outbound blocking and Block host machine.
SSH agent forwarding Off When on, the agent can use your ssh-agent keys, for example to push.
Start with Baton / stop on quit On The VM runs only while Baton is open.

SSH agent forwarding lives in the Security & permissions card and needs a VM restart. When it's on, private keys stay on your computer, but the agent can ask your ssh-agent to sign with any key it holds while the VM is running. Avoid it if those keys can force-push to repositories you care about.

Network sandboxing

Open it from the Network sandboxing card on the Lima VM settings page, or from the VM menu in the title bar. The Network policy tab has the switches. The Firewall tab shows what was blocked and holds your allowed domains, host exemptions, and custom rules. The firewall runs inside the VM, and changes apply right away without restarting terminals.

Block host machine
Inside the VM, your computer is host.lima.internal (192.168.5.2). With this on, the agent can reach only DNS, DHCP, Baton's own port for notifications and MCP, and any ports you add under Firewall → Host machine exemptions. To let the agent use a database or API running on your computer, add its port there. A Postgres on 5432 is then reachable at host.lima.internal:5432. You can also remove Baton's own exemption, but notifications and MCP stop working.
Block local networks
Drops traffic to private IPv4 ranges (RFC 1918), link-local addresses, and IPv6 unique-local addresses. That covers your router, NAS, and other devices on your network. On a cloud machine it also covers the 169.254.169.254 metadata endpoint, which could otherwise hand out the machine's credentials. Lima's own subnet stays open so the VM keeps working.
Port forwarding
Anything the agent serves on localhost in the VM shows up on your computer's localhost, so dev servers and browser previews just work. Turn it off for strict containment; the setting applies after a VM restart.

Outbound allowlist

With the defaults, the agent can still reach any site on the internet. Turn on Block outbound traffic to deny everything the agent sends out unless you allowed it:

How domains match. Entries match on hostname, not on URL paths. api.example.com matches that host exactly. *.example.com matches any subdomain at any depth, but not example.com itself, so add both if you need both.

Allow what got blocked. The Firewall tab lists recent proxy and firewall blocks. Use Allow on a row to add the exact host, the domain with www, or a wildcard. It's quicker than guessing ahead of time which hosts an agent will need.

Default allowlist

Baton fills in this list the first time you turn on outbound blocking. Edit it on the Firewall tab; Reset to defaults brings it back.

  • AI providers and sign-in: *.anthropic.com, claude.ai, *.claude.ai, claude.com, *.claude.com, *.openai.com, chatgpt.com, *.chatgpt.com, generativelanguage.googleapis.com, accounts.google.com, oauth2.googleapis.com
  • Package registries: npm (registry.npmjs.org, *.npmjs.org), PyPI (pypi.org, files.pythonhosted.org), Cargo (crates.io, static.crates.io), Go (proxy.golang.org, sum.golang.org), RubyGems (rubygems.org, index.rubygems.org), Maven (repo.maven.apache.org, repo1.maven.org), NuGet (api.nuget.org, *.nuget.org)
  • Containers: registry-1.docker.io, auth.docker.io, production.cloudflare.docker.com, ghcr.io
  • GitHub: github.com, *.github.com, *.githubusercontent.com
  • Debian packages: deb.debian.org, security.debian.org
Git over SSH and DNS with outbound blocking on

Git over SSH is blocked. Port 22 isn't on the allowlist. Push over HTTPS (github.com is allowed by default), or add a custom rule for TCP port 22 to your Git host.

DNS stays open by default, because custom rules can name hosts. A determined agent could hide small amounts of data in DNS lookups. To close that, turn on Restrict user DNS, which also needs Block host machine. Web traffic keeps working because the proxy resolves names itself, but custom rules then need IP addresses instead of hostnames.

Hardening, not a guarantee. Hostname filtering can be bypassed when an attacker's site shares a CDN with an allowed domain. The firewall also lives inside the VM, so it only holds while the agent can't become root. Keep Restrict sudo on. For stricter needs, put a filtering proxy you control in front of the VM, or turn network access off for tasks that don't need it.

Root access

Restrict sudo is on by default. It removes passwordless sudo from the agent's user, and that user has no password, so the agent can't install system packages, edit system files, or turn off the firewall. Baton also removes the setuid bit from the user-namespace tools, so the agent can't pose as another user to get past the firewall rules.

Restrict sudo is applied when the VM is created. To change it, delete the VM and create it again. You can always get in as root yourself: Root shell in the VM menu copies an ssh command that logs you in as root, for one-off installs or fixes.

Resources and software

The VM starts with 4 CPUs, 8 GB of memory, and a 20 GB disk. Each running agent takes a few hundred megabytes, and builds, tests, and dev servers in the VM share the same pool, so give it more if you run many agents at once. Change resources in the VM Status card and restart the VM to apply them. Disk can grow later but can't shrink.

Software is chosen when you create the VM:

To install something later, use apt from a root shell, or create the VM with Restrict sudo off.

Working with the VM

Safehouse macOS

Agent Safehouse runs a command inside a macOS sandbox profile that limits which folders it can read and write. It shares your computer's kernel and network, so it's lighter than a VM but offers no network controls. Install Safehouse, then add it from Settings → Sandboxes → Sandbox methods → + Safehouse. The template Baton fills in is:

safehouse --add-dirs=~/.baton/repos --add-dirs-ro=~/.baton/bin/baton:~/.baton/port -- {{command}}

The agent runs with your normal environment, so your existing notification and MCP integrations keep working.

Custom wrappers

Anything that can run a command for you can be a sandbox: Docker, Podman, a dedicated WSL2 distro, bubblewrap, firejail, systemd-nspawn, or your own script. Add one from Settings → Sandboxes → Sandbox methods → + Custom. Baton then runs every agent command in that workspace through it.

Wrap command required
A shell template that Baton runs for each agent command. {{command}} is replaced by the agent command. {{env_prefix}} is optional and expands to KEY=value assignments that tell the agent how to reach Baton: BATON_API_URL, BATON_TERMINAL_ID, BATON_NOTIFY_HOST (when you set a notify host), and COLORTERM=truecolor. Use it with containers and VMs, which start with a fresh environment. Same-kernel wrappers inherit your environment and don't need it.
Shell command optional
Opens an interactive shell inside the sandbox, for example wsl -d agents. It powers the sandbox shell button in the terminal tab bar and also accepts {{env_prefix}}.
Notify host optional
The hostname the sandbox uses to reach Baton on your computer. Baton only listens on your computer's localhost, so the name must lead there. Leave it empty for same-kernel wrappers and for sandboxes that share your computer's network, like docker run --network host on Linux (it defaults to localhost). Use host.docker.internal with Docker Desktop. A plain host IP, like Docker's bridge gateway on Linux, doesn't reach Baton.

Put {{env_prefix}} where a shell will read it. Tools that pass arguments straight to a program, like docker exec, need a bash -lc around it:

# Docker: mount ~/.baton/worktrees and ~/.baton/repos at the same paths in the container
docker exec -i agents bash -lc "{{env_prefix}}{{command}}"

# A dedicated WSL2 distro
wsl -d agents -- bash -lc "{{env_prefix}}{{command}}"

Mount the worktrees at the same paths. The sandbox needs read/write access to ~/.baton/worktrees and ~/.baton/repos, at the paths they have on your computer.

Notifications and MCP. Baton's one-click integrations cover your computer, the Lima VM, and SSH hosts. Inside your own container or VM, set up the agent's hooks and MCP yourself. Settings → Integrations & MCP → Other Agent CLIs shows the commands, and the sandbox must be able to reach Baton's port through the notify host. MCP is served at http://<notify-host>:<port>/api/v1/mcp.

Sketch: a dedicated WSL2 distro on Windows

A separate WSL2 distro gives VM-level isolation on Windows. We haven't verified this setup end to end, so treat these steps as a starting point.

  1. Create a distro just for agents. In its /etc/wsl.conf, turn off Windows drive automounting ([automount] enabled = false) and Windows interop ([interop] enabled = false), then run wsl --terminate <distro>.
  2. Mount only Baton's folders. Mount %USERPROFILE%\.baton\worktrees and %USERPROFILE%\.baton\repos read/write with mount -t drvfs or /etc/fstab.
  3. Add a custom method. Wrap command wsl -d agents -- bash -lc "{{env_prefix}}{{command}}", shell command wsl -d agents, and an empty notify host. Baton only listens on Windows' localhost, so turn on mirrored networking (networkingMode=mirrored under [wsl2] in %USERPROFILE%\.wslconfig, Windows 11 22H2 or later) to let the distro reach it there.
  4. Set up notifications and MCP inside the distro, as described above.

Running agents in WSL2 already, or trying to? We'd like to make it a built-in option. Tell us what works, and what doesn't.

Choosing where a workspace runs

Adding a sandbox method doesn't move anything into it. You choose per workspace:

FAQ

Does the VM slow agents down?

Agents spend most of their time waiting on the model, so you'll rarely notice. File access through the shared folders is a bit slower than on your disk, which can show up in large builds or node_modules installs. Give the VM more CPUs and memory if you run several agents at once.

Can I still open the worktree in my editor?

Yes. The worktrees are ordinary folders on your computer, shared into the VM. Edit them in any editor, and review and commit from Baton as usual.

How do I let the agent use a database on my computer?

Add its port under Firewall → Host machine exemptions, then connect to host.lima.internal:<port> from inside the VM. For a database elsewhere on your network or the internet with outbound blocking on, add a custom rule for its protocol, port, and address.

Can agents push to GitHub from the VM?

Over HTTPS, yes, once the agent has credentials, for example from gh auth login inside the VM. Over SSH, turn on SSH agent forwarding, and if outbound blocking is on, add a custom rule for TCP port 22. Many people prefer to push from Baton's Git toolbar instead, after reviewing the diff.

Is a sandbox enough to run agents unattended?

It makes the worst case much smaller, but it's defense in depth, not a guarantee. The agent can still damage the worktrees it's working in, push if you give it credentials, and talk to any site on your allowlist. Keep backups pushed, review diffs, and give agents only the access a task needs.

Try the Lima sandbox

Free to download for macOS, Windows, and Linux.

Download Baton

Questions, or a setup we should support? Get in touch.