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:
- What it can see: only the worktrees it's working on.
- What it can change: nothing outside those worktrees, and no system files.
- Where it can send data: not your computer, not your local network, and optionally only an allowlist of domains.
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
-
Install Lima.
On macOS, run
brew install lima. On Linux, install Lima and QEMU from your package manager, for exampleapt install limaorpacman -S lima. -
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.
-
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.
-
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
/loginin Claude Code. The login stays in the VM until you delete it. -
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 athost.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.254metadata 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
localhostin the VM shows up on your computer'slocalhost, 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:
- Web traffic goes through a proxy inside the VM (tinyproxy) that only lets allowlisted domains through. Anything else gets a 403 response that tells the agent where the allowlist lives.
- Everything else (databases, SSH, other ports) is dropped unless a custom rule allows that protocol, port, and destination.
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:
- Always installed: Node.js 22, Git, and curl.
- Selected by default: Claude Code, Codex, ripgrep, fd, jq, tree, unzip, yq, and Python 3.
- Optional: OpenCode, Antigravity CLI, GitHub CLI, Playwright with Chromium, and containerd with nerdctl.
To install something later, use apt from a root shell, or create the VM with
Restrict sudo off.
Working with the VM
- VM menu: a cube icon in the title bar (or More → VM) starts and stops the VM, copies shell and root-shell commands, and opens VM and network settings.
-
Terminals: the cube button next to
+in the tab bar opens a shell inside the VM.+itself opens a shell on your computer unless you change that in settings. -
Baton CLI and previews: the
batoncommand works inside the VM, and dev servers open in Baton's browser preview through port forwarding. - Starting over: Delete VM in the Danger zone keeps your workspaces but wipes everything inside the VM, including agent logins, chat history, and installed tools. Workspaces that ran in the VM fall back to your computer.
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 toKEY=valueassignments that tell the agent how to reach Baton:BATON_API_URL,BATON_TERMINAL_ID,BATON_NOTIFY_HOST(when you set a notify host), andCOLORTERM=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 hoston Linux (it defaults to localhost). Usehost.docker.internalwith 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.
-
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 runwsl --terminate <distro>. -
Mount only Baton's folders. Mount
%USERPROFILE%\.baton\worktreesand%USERPROFILE%\.baton\reposread/write withmount -t drvfsor/etc/fstab. -
Add a custom method. Wrap command
wsl -d agents -- bash -lc "{{env_prefix}}{{command}}", shell commandwsl -d agents, and an empty notify host. Baton only listens on Windows' localhost, so turn on mirrored networking (networkingMode=mirroredunder[wsl2]in%USERPROFILE%\.wslconfig, Windows 11 22H2 or later) to let the distro reach it there. - 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:
- New Workspace dialog: the environment pill lists Local, your sandbox methods, and your SSH hosts. Baton remembers your choice for the next workspace, and a newly added sandbox becomes the choice. Agent tabs you add later run where their workspace runs.
-
Shell into sandbox for new terminals: off by default, in the
Terminals card under Settings → Sandboxes. When it's off,
+opens a shell on your computer and the cube button opens one in the sandbox. Turn it on to swap them. - Workspaces agents launch: a workspace an agent starts from inside the sandbox stays in the sandbox, unless you set a different default under Settings → Integrations & MCP → Launch defaults.
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.
Free to download for macOS, Windows, and Linux.
Questions, or a setup we should support? Get in touch.