Run a coding agent β Claude Code or Codex β on a repo in its own
"skip-permissions" mode, while the macOS sandbox, not the agent, enforces what it
can touch. A wrong rm -rf
, a stray git reset --hard
, a pip install
into your system: the sandbox turns these from incidents into errors.
s claude # run Claude Code, confined to the current repo (setup below)
It's one short, auditable shell script around macOS sandbox-exec
(the Seatbelt sandbox). The agent gets read-write access to your working copy and the bare minimum to run itself and your tests; everything else is denied by default and opened only when you ask.
Native, in placeβ the agent edits your real working copy with your real macOS toolchain. No Linux guest, no copied files, no syncing changes back.Lightweightβ a sandboxed process, not a machine: no VM to boot, no image, no daemon; nothing to install beyond one script.** OS-enforced**β the kernel, not the agent, decides what it can touch, so you can let it run unattended in skip-permissions mode.** Auditable**β read the one script before you trust it;--print
shows exactly what any invocation grants, andDESIGN.mdexplains how it works.
In scope: the agent runs the wrong command β rm -rf
in the wrong place,
git reset --hard
, clobbering files outside the task, installing junk system-wide. sandfence turns these from incidents into errors.
Out of scope: a malicious agent, prompt injection, or a poisoned dependency
actively trying to escape or exfiltrate. sandbox-exec
shares your kernel and user
account β it's a guardrail, not a containment boundary. The network is open and your
working copy is readable, so code the agent runs (npm install
, a build hook) can read secrets in your repo and send them out. For untrusted code, use a VM or a separate user account.
Requires macOS on Apple Silicon. Clone it, then make a short s
wrapper so daily
use is just s claude
:
git clone https://github.com/sheremetyev/sandfence ~/.config/sandfence
cat > ~/.local/bin/s <<'EOF'
#!/bin/sh
exec ~/.config/sandfence/sandfence.sh --rust --node --python "$@"
EOF
chmod +x ~/.local/bin/s
s
is yours to tune: keep only the presets you use, and add -r DIR
/ -w DIR
for paths you reach for often. Each preset and grant is a real widening of the sandbox.
Auth (once). Each agent keeps its own token in a file the sandbox grants β never the login Keychain, never your shell environment.
Claude Codeβ runs claude
, then/login
; paste the printed URL into your browser (the sandbox can't open one). It writes~/.claude/.credentials.json
and refreshes it from then on.Codexβ runcodex login
once (anywhere); its token lives in~/.codex/auth.json
, which the sandbox reads.
Read-write |
the current directory β your working copy |
Read-only |
its own .git / .jj β you drive version control outside the sandbox; the agent can't commit or rewrite history |
Denied |
the rest of $HOME β ~/.ssh , ~/.aws , gh /glab tokens, the login Keychain, ~/.gitconfig credentials, other repos |
Widen it explicitly: ** -r PATH** /
add a directory or file, and the
-w PATH
presets grant build caches read-write while keeping registry tokens and PATH-plant vectors denied.
--rust
/ --node
/ --python
shows exactly what an invocation grants;
--print
DESIGN.mdexplains why each grant is there.
macOS on Apple Silicon, default toolchains (rustup
,nvm
- stock
npm
, Applepython3
). Homebrew, pyenv, pnpm, and yarn aren't auto-detected β grant them with-r
/-w
.β only the CLI is deprecated; the Seatbelt engine under it still powers the macOS App Sandbox and Chrome's renderer sandbox, so it isn't going away. A future macOS could change the CLI.sandbox-exec
is deprecated by Apple (2017) but fully functional/tmp
and/var/folders
are read-write wholesale β don't expect repo isolation there.- Under
jj,.jj
is read-only; run read-only commands asjj --ignore-working-copy β¦
.
./test.sh ~/sandfence-tests
runs real commands inside the sandbox and asserts each
allow/deny β so you verify what's granted, not just read it. Use a real dir (not /tmp
),
and run it in a plain shell (sandbox-exec
can't nest).
MIT β see LICENSE.