# claudebox Run [Claude Code](https://docs.anthropic.com/en/docs/claude-code) inside a [bubblewrap](https://github.com/containers/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 ```bash nix run git+https://git.toph.so/toph/claudebox ``` Or add to your flake: ```nix { 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 shell` and [comma](https://github.com/nix-community/comma) (`, `) 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 ` | 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: ```bash ANTHROPIC_API_KEY=sk-ant-... MY_GLOBAL_VAR=value ``` `/.claudebox.env` — per-project, loaded when present: ```bash 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: ```bash 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: ```bash eval "$(ssh-agent)" ssh-add ~/.ssh/id_ed25519 claudebox --with-ssh ``` Fish: ```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 ` (explicit key files) Mounts a specific private key (and matching `.pub`, if present) read-only into the sandbox at `~/.ssh/`. Repeatable — pass it multiple times for multiple keys. ```bash 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// (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