4.9 KiB
claudebox
Run Claude Code inside a bubblewrap sandbox with an allowlisted environment, explicit filesystem mounts, and a minimal PATH.
SSH keys, GPG/age secrets, cloud tokens, and Tailscale state stay completely invisible to the AI agent. If a secret is accessible inside the sandbox, it's a bug.
Quick start
nix run git+https://git.toph.so/toph/claudebox
Or add to your flake:
{
inputs.claudebox.url = "git+https://git.toph.so/toph/claudebox";
}
Then add inputs.claudebox.packages.${system}.default to your environment.systemPackages or home-manager packages.
What it does
- Starts Claude Code inside a bwrap namespace with
--clearenv - Only allowlisted env vars enter the sandbox (HOME, PATH, TERM, EDITOR, LANG, ANTHROPIC_API_KEY if set)
- Mounts CWD read-write, Nix store read-only, everything else is tmpfs
- Provides
nix shelland comma (, <tool>) so Claude can install tools on demand - Injects a SANDBOX.md so Claude knows it's sandboxed and how to get tools
- Pre-configures git identity and safe.directory from host
Flags
| Flag | Description |
|---|---|
--yes, -y |
Skip the env audit and launch immediately |
--dry-run |
Print the bwrap command without executing |
--check |
Verify prerequisites and exit |
--shell |
Drop into a bash shell instead of Claude Code |
--gc |
Remove stale per-project instance dirs and exit |
--with-ssh |
Forward $SSH_AUTH_SOCK into the sandbox (requires running ssh-agent) |
--ssh-key <path> |
Mount a private key file read-only into the sandbox ~/.ssh/ (repeatable) |
-- |
Pass remaining args to Claude Code |
Env vars
Env files (preferred) — define vars without polluting your shell:
~/.claudebox/env — global, loaded on every launch:
ANTHROPIC_API_KEY=sk-ant-...
MY_GLOBAL_VAR=value
<project>/.claudebox.env — per-project, loaded when present:
DATABASE_URL=postgres://localhost/myapp
SOME_PROJECT_VAR=value
Add .claudebox.env to your .gitignore if it contains secrets.
Pass-through — inject host vars already set in your shell:
CLAUDEBOX_EXTRA_ENV=MY_VAR,OTHER_VAR claudebox
All injected vars appear in the [+] section of the env audit.
SSH
SSH is opt-in. By default no keys or agent socket cross the sandbox boundary, which means git push/pull over SSH remotes won't work. Two mechanisms are available — pick whichever matches your workflow.
--with-ssh (agent forwarding)
Forwards $SSH_AUTH_SOCK into the sandbox so any keys loaded in your ssh-agent are usable inside. Your private key files are never mounted; only the agent socket is.
Start an agent before launching claudebox. The agent dies with the shell that started it, so don't expect it to survive across terminals.
Bash:
eval "$(ssh-agent)"
ssh-add ~/.ssh/id_ed25519
claudebox --with-ssh
Fish:
eval (ssh-agent -c)
ssh-add ~/.ssh/id_ed25519
claudebox --with-ssh
If --with-ssh is passed but no agent is running, claudebox warns and continues without forwarding.
--ssh-key <path> (explicit key files)
Mounts a specific private key (and matching .pub, if present) read-only into the sandbox at ~/.ssh/<basename>. Repeatable — pass it multiple times for multiple keys.
claudebox --ssh-key ~/.ssh/id_ed25519
claudebox --ssh-key ~/.ssh/id_work --ssh-key ~/.ssh/id_personal
Prefer this when you don't have an agent running, or when you want to scope exactly which keys the sandbox can use regardless of what's loaded in the agent.
known_hosts
When either flag is active, ~/.ssh/known_hosts is mounted read-only (if it exists) so SSH host verification works without prompting.
Both flags can be combined.
How it works
~/.claudebox/ # persistent config dir (host)
├── SANDBOX.md # managed by claudebox, overwritten each launch
├── history.jsonl # conversation history
├── .credentials.json # Claude Code credentials (if present)
└── projects/
└── <16-char-hex>/ # per-project instance dir (keyed by canonical git root)
└── project-root # records the canonical path for this instance
Inside the sandbox:
~/.claude → bind-mounted from host (plugins, skills, hooks, MCP all visible)
~/.claude/projects → bind-mounted from ~/.claudebox/projects/<hash>/ (per-project isolation)
~/.claude/history.jsonl → bind-mounted from ~/.claudebox/history.jsonl
~/.claude/SANDBOX.md → bind-mounted from ~/.claudebox/SANDBOX.md
Each project gets an isolated ~/.claude/projects/ directory inside the sandbox, so conversation history and project state are separated per repo. Git worktrees share the same instance dir as their main worktree.
Requirements
- NixOS or Nix with flakes enabled
- User namespaces (enabled by default on NixOS)
License
MIT