claudebox/README.md

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 shell and 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