↑ All Lab Code story sandvault Mar 31, 2026 → May 3, 2026 5 weeks

Good Fences make Good Agents: Sandvault (part 1 of N)

A simple solution to working with agents that cannot be trusted to run 'ls'.

rm -rf /

I asked Claude what execution protections were in place. Claude tested by running rm -rf / and reported back cheerily that the command had failed. I killed off everything and went quickly from WTF to absolutely-fucking-not.

Good fences make good Agents, or so the saying goes.

The first time you see something like this, or an agent break containment, it activates something. (At least if you are an engineer with ahem a certain amount of experience.)

The fence-building industrial complex

As a result of whatever that impulse is, I am now aware of something like 12 containment projects from people I know, and the wider count is bigger. Apparently it's a thing, building fences that turn into gated communities for agents we cannot trust to run ls.

I found sandvault through a post by Mike McQuaid. agentsh, Stockyard, navaris, and a few more from people I trust are at the bottom of this post.

TLDR: Enter Sandvault

Sandvault is from Patrick Wyatt, whose claims to fame include games where you have to build a lot of containment.

Sandvault logo

install bash
 1brew install webcoyote/tap/sandvault 2sv build 3sv claude      # or sv codex, sv shell
One brew install. Each agent runs as a dedicated macOS user behind sandbox-exec.

Handing a task off with /sv

/sv is a Claude Code skill that packages the current task as a briefing file in the shared workspace, opens a new terminal, runs sv-clone to spin up a sandboxed Claude (or Codex) with a per-repo SSH deploy key whose write scope is exactly the one repo, and hands the briefing to the sandboxed agent as its first instruction. The worker runs, commits, and pushes its branch back through that single deploy key. The host session never has to leave the conversation.

This was my commit BTW, and it's been very useful.

/sv from a host Claude session text
 1/sv         hand a task off to a sandboxed Claude (or Codex) 2/sv status  read the report channel from the sandboxed worker 3/sv pull    pull the result branch back through the deploy key
The per-repo deploy key is the only credential that crosses the boundary.

The two properties that make this worth the setup: the host's context is spent on judgement, not on watching the worker type. And the worker is bounded twice. Once by sandbox-exec (it can't reach your home directory or any other repo on disk). Once by the deploy key (it can't push to any GitHub repo besides the one you cloned). Two independent fences. You can leave the worker alone.

How the sandbox boundary actually works

sv claude and the agent runs as sandvault-jesse, sees only what sandvault-jesse can see. If it does something stupid the radius is bounded by one user account. The model is concrete:

what the sandbox can touch text
 1writable:  /Users/Shared/sv-$USER     -- shared with you and sandvault-$USER 2writable:  /Users/sandvault-$USER     -- sandvault's own home directory 3readable:  /usr, /bin, /etc, /opt     -- system directories 4no access: /Users/*                   -- every other user, including yours 5no access: /Volumes/*                 -- mounted, remote, and network drives
From the sandvault README. The boundary is enforced by macOS user permissions plus a sandbox-exec profile, not a configurable allowlist you have to maintain.

Cross-boundary work goes through the shared workspace at /Users/Shared/sv-$USER/. When you sv-clone a repo, sandvault adds a sandvault remote on your host-side repo pointing at the in-sandbox clone, so git fetch sandvault pulls commits the sandboxed agent made. No new protocols, no daemons, just git remotes.

First-run reality

The strict-isolation model has one immediate consequence worth knowing: the sandbox user is a fresh macOS user with no credentials, so on first run you have to log into things inside the sandbox. Run sv claude and Claude Code asks you to authenticate. Run sv codex and Codex shows you an OAuth URL. Same for gh, npm, anything else the agent will need. It is the equivalent of setting up a new laptop on the first try. After that, the sandbox user keeps its own session state, so subsequent runs come up signed in.

A few features that turned out to matter once I was past first-run:

Dotfiles sync. Drop your shell config into /Users/Shared/sv-$USER/user/ (e.g. .zshrc, .gitconfig, .config/), and sandvault copies it into the sandbox user's home on each build. I keep a different-coloured prompt in there so I always know when I'm looking at a sandbox shell. Mike McQuaid's dotfiles have a script/sync worth borrowing.

--native-install (-N). By default sandvault uses your host's Homebrew to install Claude / Codex / OpenCode / Gemini. With -N, the AI tools install inside the sandbox using each tool's own installer (curl ... | bash, npm install -g). Useful if you want the sandbox to be self-contained, or if you don't want sandvault touching host Homebrew at all. Set export SANDVAULT_ARGS="--native-install" to make it the default.

--browser for headless Chrome. sv --browser claude starts Chrome on the host with a dynamic CDP port and exposes the endpoint inside the sandbox as $SV_BROWSER_ENDPOINT. Playwright and Puppeteer connect with one line: chromium.connectOverCDP(process.env.SV_BROWSER_ENDPOINT). The Chrome instance runs in an isolated user-data directory so your real Chrome profile is untouched.

--ios for iOS Simulator. Same shape: Simulator.app runs on the host (it's a GUI), an HTTP bridge runs on localhost, the bridge endpoint is exposed inside the sandbox as $SV_IOS_SIMULATOR_ENDPOINT. The bridge translates calls into xcrun simctl plus iosef. Add --ios-gui to watch the simulator window while an agent works it.

Nested-sandbox carve-outs. macOS doesn't support recursive sandboxes, so swift and xcodebuild (which already sandbox themselves) break inside sandvault unless you tell them not to. Sandvault sets SV_SESSION_ID in the environment; you check for it in your build scripts and pass --disable-sandbox / SWIFTPM_DISABLE_SANDBOX=1 / etc. The README has the exact incantations.

Maintenance. sv b -r rebuilds the sandbox and re-applies ACLs (useful if files got copied in with wrong ownership). sv uninstall removes the sandvault user, the sandbox profile, and the shared-workspace ACL config, but does not delete /Users/Shared/sv-$USER. Your work stays put.

Running several at once

For running multiple sandboxed workers at once, point a tmux-session manager at the sv commands. Jesse Vincent has been working on exactly this with claude-session-driver ("launch, control, and monitor other Claude Code sessions as workers via tmux"). Each driven session can be an sv claude or sv codex invocation, so the sandboxing comes along for free. A Claude supervisor can dispatch to sv codex (or Codex to sv claude) to keep the workers on a different token budget.

A few other fences my friends with trust issues are building...

Sandvault isn't the only one I'd trust on my machine. Full writeup coming; until then, public links only:

agentsh — Eran Sandler (@erans). Execution-layer security: policy-enforced bash-compatible shell. Point your harness at agentsh instead of /bin/bash and it intercepts syscalls against a deterministic policy. Ships a local DLP proxy for secret detection. Stricter quarantine than sandvault. Eran also writes about why he built it.

navaris — Eran Sandler. A sandbox control plane for managing isolated execution environments across multiple backends (LXC / Firecracker), with copy-on-write fork support. Signature feature is that you can peek into a running sandbox without breaking it.

StockyardJesse Vincent. Firecracker VM farm + ZFS. 1.9-second container start on a NUC. ZFS copy-on-write snapshots from pre-tool-use hooks for state rollback. Tailscale auth, in-browser console. Ephemeral cattle VMs.

agent-safehouse — by Eugene. Single self-contained shell script that wraps any agent invocation with sandbox-exec. Claude Code, Codex, Gemini CLI, Cursor Agent, Cline, Aider.

Full writeup is the next post in the series unless I get distracted by something else.

My own contributions to this project

Seven contributions landed upstream: four new features and three bug fixes.

New features

2970e9a Add agentsview export from sandbox sessions jesserobbins · May 3, 2026 · +1760 −0

Mirrors sandbox-side session JSONLs into the host's agentsview via read-only symlinks. Adds an opt-in prompt during sv setup, a contamination pre-flight that refuses to run if the mirror paths point at non-sandbox directories, and a vendored TOML config writer so it works on system Python 3.9.

e565225 Add /sv Claude Code skill for handing tasks off to sandvault jesserobbins · Apr 24, 2026 · +92 −0

The /sv Claude Code skill. Hand a task off from a host Claude session to a sandboxed Claude or Codex with three commands. Detailed up above.

1d62630 Add per-repo SSH deploy keys for cloned repositories jesserobbins · Apr 22, 2026 · +113 −2

Fresh ED25519 key per cloned repo. origin rewritten to SSH, core.sshCommand pinned to that key with IdentitiesOnly=yes, public key uploaded via gh scoped to that one repo. The sandbox can push without seeing any other repo's credentials.

49ae69f Add --fix-permissions flag, umask detection, and permission hardening jesserobbins · Mar 31, 2026 · +276 −16

A restrictive umask makes sv build exit 0 with a broken environment. Adds an opt-in --fix-permissions flag, warns on every invocation when umask is restrictive, hardens /var/sandvault/, and adds six tests for the umask/permissions behavior.

Bug fixes

66a1233 Fix SSH mode when Remote Login is set to "All users" jesserobbins · Mar 31, 2026 · +20 −2

Sandvault was checking for the com.apple.access_ssh group as the SSH-enabled signal, which does not exist when Remote Login is set to "All users". Falls back to checking whether sshd is actually running, and adds an SSH smoke test at build time.

f1959b1 Fix git object permissions with --no-hardlinks jesserobbins · Apr 21, 2026 · +1 −1

One-character fix. git clone --local hardlinks loose objects, which inherit mode 0400 from the source, which makes commit history unreadable to the sandvault user. Switching to --no-hardlinks forces a copy with the calling user's umask.

d983d62 Fix sandvault user not added to sandvault group jesserobbins · Apr 20, 2026 · +5 −0

Five lines. PrimaryGroupID alone does not register the user in dseditgroup membership checks, so ACL inheritance worked but membership-based ACL checks did not. Found by a sandboxed Claude during its own permission tests.