Sandboxed development environments for Claude Code. Each sandbox runs a Claude container with your workspace mounted, and exposes project commands as MCP tools that run on the host.
Host Machine Claude Container
(debian + Claude CLI)
MCP Server
cbox_build ──sh -c──> calls via MCP tool
cbox_test ──sh -c──> calls via MCP tool
run_command (git, gh) calls via MCP tool
/workspace (mount)
docker.sock (mount)
- Claude container — Fixed Debian image with Claude Code CLI.
cbox chatconnects here. - MCP server — Runs on the host, exposes named commands (
cbox_build,cbox_test, etc.) and whitelisted host commands (git,gh) as MCP tools. Claude calls these tools from inside the container. - Workspace — A git worktree mounted into the container. Each branch gets its own isolated worktree.
go install github.com/richvanbergen/cbox/cmd/cbox@latestOr build from source:
git clone https://github.com/richvanbergen/cbox.git
cd cbox
go build -o bin/cbox ./cmd/cboxRequires Docker.
cbox supports shell completion for bash, zsh, and fish. Completions include commands, flags, git branches, and config-defined commands.
# Load for current session
source <(cbox completion bash)
# Install permanently (Linux)
cbox completion bash > /etc/bash_completion.d/cbox
# Install permanently (macOS with Homebrew)
cbox completion bash > $(brew --prefix)/etc/bash_completion.d/cbox# Enable completion (add to ~/.zshrc if not already enabled)
autoload -U compinit; compinit
# Install completion
cbox completion zsh > "${fpath[1]}/_cbox"# Load for current session
cbox completion fish | source
# Install permanently
cbox completion fish > ~/.config/fish/completions/cbox.fishcd your-project
# Create config
cbox init
# Edit cbox.toml to set your build/test commands
# Start sandbox on a new branch
cbox up feat-my-feature
# Start Claude
cbox chat feat-my-feature
# Run a one-shot prompt
cbox chat feat-my-feature -p "refactor the auth module"
# Shell into the Claude container (for debugging)
cbox shell feat-my-feature
# Stop container (keeps worktree)
cbox down feat-my-feature
# Full cleanup (removes worktree and branch)
cbox clean feat-my-featureCreate cbox.toml in your project root (cbox init generates a starter config):
env = ["ANTHROPIC_API_KEY"] # read from host environment
host_commands = ["git", "gh"]
[commands]
build = "npm run build"
test = "npm test"| Field | Description |
|---|---|
commands |
Named commands exposed as cbox_<name> MCP tools (run on the host via sh -c) |
env |
Environment variable names to pass from host into the Claude container |
env_file |
Path to an env file |
browser |
Enable Chrome bridge for browser-aware Claude sessions |
host_commands |
Commands Claude can run on the host via the run_command MCP tool (e.g. git, gh) |
copy_files |
Files or directories to copy from the main project into each new worktree |
ports |
Ports to expose from the container to the host (Docker -p syntax) |
dockerfile |
Path to custom Dockerfile (see cbox eject) |
open |
Shell command to run before chat (use $Dir for worktree path, e.g., code $Dir) |
editor |
Editor command for editing flow descriptions (e.g., vim, code --wait) |
serve |
Serve process config — see Serve |
Creates a default cbox.toml in the current directory with placeholder build and test commands, and git/gh as default host commands.
Creates a git worktree, builds the Claude image, creates a Docker network, and starts the Claude container. If commands or host_commands are configured, starts an MCP server on the host. Idempotent — re-running replaces the existing container.
Stops the container, MCP server, and removes the network. Preserves the worktree so you can cbox up again.
Launches Claude Code interactively in the Claude container.
Runs a one-shot Claude prompt in the Claude container (headless, JSON output).
Flags:
--open [command]— Run a command before starting chat (usesopenconfig if no command specified; use$Dirfor worktree path)
Opens a bash shell in the Claude container. Useful for debugging.
Runs the open command configured in cbox.toml for the specified sandbox without starting a chat session. Useful for opening your editor or browser to the worktree.
Flags:
--open <command>— Override the config and run a custom command (use$Dirfor worktree path)
Runs a named command from the commands section of cbox.toml directly on the host machine (not via MCP). Useful for local development workflows.
Example:
# With this config:
# [commands]
# test = "go test ./..."
cbox run test # runs 'go test ./...' on the hostCopies the embedded Dockerfile into your project as Dockerfile.cbox and updates cbox.toml to reference it. Use this when you need to customize the container image (e.g., to install runtimes like Node.js or Python).
After ejecting, you can freely edit Dockerfile.cbox. Existing sandboxes need rebuilding with cbox up --rebuild <branch>.
Lists all tracked sandboxes and their status.
Shows details about a specific sandbox (container name, network, worktree path).
Stops the container, removes the network, deletes the worktree, and removes the branch.
Generates shell completion scripts. See Shell Completion for installation instructions.
Each entry in commands becomes a dedicated MCP tool named cbox_<name>. When Claude calls the tool, the MCP server on the host runs sh -c '<expression>' in the worktree directory.
For example, with this config:
[commands]
test = "npm test"
build = "npm run build"Claude sees two MCP tools: cbox_test and cbox_build. Calling cbox_test runs sh -c 'npm test' on the host in the worktree directory.
Claude inside the container doesn't have access to host tools like git or gh. The host_commands config whitelists commands that Claude can run on the host machine via the run_command MCP tool.
When host_commands or commands are configured, cbox up starts an MCP server on the host. Claude Code in the container connects to it automatically via .mcp.json. A system-level CLAUDE.md is also injected to tell Claude how to use the available tools.
host_commands = ["git", "gh"]With this config, Claude can run git status, gh pr create, etc. on the host via the run_command tool. Commands not in the whitelist are rejected.
Path arguments containing /workspace/... are automatically translated to the host worktree path, and paths outside the worktree are rejected.
When cbox up creates a new worktree, you can automatically copy files or directories from your main project directory into it using the copy_files configuration:
copy_files = [".env", "node_modules", "vendor"]This is useful for:
- Environment files (
.env,.env.local) that shouldn't be in git - Dependencies (
node_modules,vendor) to avoid reinstalling on every branch - Build artifacts or cached files that are .gitignored
The worktree itself is mounted into the container via Docker volume at /workspace, so changes are immediately visible on both the host and container—no copying between host and container occurs.
However, when creating a new worktree with git worktree add, git only checks out tracked files. The copy_files setting lets you supplement each new worktree with additional files from your main project:
- Worktree creation — git creates the worktree with tracked files
- File copying — cbox copies each pattern from the main project to the worktree
- Container mount — the worktree is bind-mounted to
/workspacein the container
- Patterns are relative to the project root
- Missing files are silently skipped (so optional entries like
.envdon't cause errors) - Both files and directories are supported
- For directories, the entire tree is recursively copied
- File permissions are preserved
copy_files = [
".env", # environment secrets
"node_modules", # pre-installed dependencies
".next", # Next.js build cache
]With this config, each new worktree will have these files copied from your main project directory, even though they're not in git.
By default, cbox uses a minimal Debian-based image with Claude CLI. If you need additional tools (Node.js, Python, Go, etc.) or system packages in the container, you can customize the Dockerfile:
# Eject the embedded Dockerfile
cbox ejectThis creates Dockerfile.cbox in your project and updates cbox.toml:
dockerfile = "Dockerfile.cbox"Example customization:
# Add Node.js runtime
FROM node:20-bookworm-slim AS node-base
FROM debian:bookworm-slim
COPY --from=node-base /usr/local/bin/node /usr/local/bin/
COPY --from=node-base /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm
# Rest of the original Dockerfile...After editing, rebuild existing sandboxes:
cbox up --rebuild <branch>By default, cbox containers have no ports mapped to the host. The ports config field maps container ports to the host using Docker's standard -p syntax:
ports = ["3000", "8080:80", "127.0.0.1:3000:3000"]| Format | Meaning |
|---|---|
"3000" |
Expose container port 3000 on the same host port |
"8080:80" |
Map host port 8080 to container port 80 |
"127.0.0.1:3000:3000" |
Bind to a specific host interface |
Changing ports requires restarting the sandbox (cbox down + cbox up).
The cbox container has the host Docker socket mounted and the Docker CLI installed, so Claude can run containers inside the sandbox. The ports field makes services started this way accessible from your host machine.
Without eject — Claude can use docker run directly inside the container. Expose the port so you can reach it from the host:
ports = ["3000"]
[commands]
build = "go build -o app ./cmd/server"Claude can then run the built binary via Docker:
docker run --rm -v /workspace:/workspace -w /workspace golang:1.23 ./appThe app listening on port 3000 inside the container is reachable at localhost:3000 on your machine.
With eject — If your app needs runtimes baked into the container, eject the Dockerfile and install them. The ports field works the same way:
cbox ejectAdd your runtime to Dockerfile.cbox:
FROM golang:1.23-bookworm AS go-base
FROM debian:bookworm-slim
COPY --from=go-base /usr/local/go /usr/local/go
ENV PATH="/usr/local/go/bin:${PATH}"
# Rest of the original Dockerfile...dockerfile = "Dockerfile.cbox"
ports = ["3000"]
[commands]
build = "go build -o app ./cmd/server"
run = "./app"Now Claude can build and run the app directly inside the container, and port 3000 is accessible on the host. Rebuild after ejecting:
cbox up --rebuild <branch>The open config field lets you automatically run a command when starting a chat session, useful for opening your editor or browser:
open = "code $Dir" # Open VS Code in the worktree directoryThe special variable $Dir is replaced with the worktree path at runtime.
Usage:
# Uses the configured open command automatically
cbox chat my-branch --open
# Or override with a custom command
cbox chat my-branch --open "cursor $Dir"
# Run open command without starting chat
cbox open my-branchCommon examples:
open = "code $Dir" # VS Code
open = "cursor $Dir" # Cursor
open = "open -a 'IntelliJ IDEA' $Dir" # IntelliJ (macOS)
open = "tmux new-session -s cbox -c $Dir" # tmux sessionWhen running multiple worktrees of the same app, port conflicts are inevitable (e.g., two branches both trying to bind to port 3000). The [serve] config section solves this by automatically allocating random ports and routing traffic through a shared Traefik reverse proxy using hostname-based routing.
Add a [serve] section to cbox.toml:
[serve]
command = "npm start --port $Port"cbox up(orcbox serve start) allocates a random port and substitutes$Portin the command- A shared Traefik reverse proxy container routes
http://<branch>.<project>.dev.localhostto the allocated port cbox down(orcbox serve stop) removes the route; Traefik stops automatically when no routes remain
Browser → feature-auth.myapp.dev.localhost:80
↓
Traefik container (shared across branches)
↓
App process on host (random port)
The .dev.localhost domain resolves to 127.0.0.1 per RFC 6761 — no /etc/hosts or DNS configuration needed.
| Variable | Description |
|---|---|
$Port |
Primary port — used for Traefik routing |
$Port2, $Port3, ... |
Additional auto-allocated ports for services that need their own ports |
Use extra port variables when your app has auxiliary services that bind to fixed ports (e.g., dev tools):
# Allocate a random port for the app AND for TanStack devtools
command = "DEVTOOLS_PORT=$Port2 npm run dev --host 0.0.0.0 --port $Port"If a service needs a specific fixed port, just hardcode it:
command = "DEVTOOLS_PORT=42069 npm run dev --host 0.0.0.0 --port $Port"[serve]
command = "npm start --port $Port" # required: shell command to run
# port = 3000 # optional: force a fixed primary port (skip random allocation)
# proxy_port = 80 # optional: override the Traefik listen portYour app must listen on 0.0.0.0, not 127.0.0.1, for Traefik (running in Docker) to reach it. Most dev servers default to localhost, so you'll typically need --host 0.0.0.0:
command = "npm run dev --host 0.0.0.0 --port $Port"# Start serve for an existing sandbox
cbox serve start <branch>
# Stop serve (removes Traefik route)
cbox serve stop <branch>Serve also starts/stops automatically with cbox up and cbox down.
Workflow orchestration for task-driven development. Creates an issue, spins up a sandbox, and provides the inner Claude with task context — the inner Claude drives the work, the flow system handles issue tracking.
# Add workflow config to cbox.toml (defaults use gh CLI)
cbox flow init# Create issue + sandbox + task context (opens editor for description if not provided)
cbox flow start
# Or provide description inline
cbox flow start -d "Add user authentication"
# Open interactive chat (refreshes task from issue)
cbox flow chat add-user-authentication
# Create PR when done
cbox flow pr add-user-authentication
# Merge and clean up
cbox flow merge add-user-authentication
# Or abandon the flow
cbox flow abandon add-user-authentication
# Check status
cbox flow status
cbox flow status add-user-authenticationRun fully autonomous — creates issue, starts sandbox, sends a headless prompt, and opens a PR:
cbox flow start --yolo "Add user authentication"-
flow start— Slugifies the description into a branch name, creates a GitHub issue, starts a sandbox with thecbox_reportMCP tool enabled, fetches issue content viagh issue view, writes a.cbox-taskfile into the worktree, and appends a task pointer to the container'sCLAUDE.md. -
flow chat— Refreshes.cbox-taskfrom the latest issue content, then opens an interactive Claude session. The inner Claude reads/workspace/.cbox-taskfor task details and callscbox_reportwhen done. -
flow pr/flow merge— Creates a PR (using done report as description if available), then merges and cleans up.
The workflow section of cbox.toml controls issue tracking commands.
You can also configure an editor at the top level for editing flow descriptions:
editor = "vim" # or "code --wait", "nano", etc.If not configured, defaults to $EDITOR environment variable, then falls back to a temporary file approach.
Workflow commands:
[workflow]
branch = "$Slug"
[workflow.issue]
create = "gh issue create --title \"$Title\" --body \"$Description\" | grep -o '[0-9]*$'"
view = "gh issue view \"$IssueID\" --json number,title,body,labels,state,url"
close = "gh issue close \"$IssueID\""
set_status = "gh issue edit \"$IssueID\" --add-label \"$Status\""
comment = "gh issue comment \"$IssueID\" --body \"$Body\""
[workflow.pr]
create = "gh pr create --title \"$Title\" --body \"$Description\""
view = "gh pr view \"$PRNumber\" --json number,state,title,url,mergedAt"
merge = "gh pr merge \"$PRNumber\" --merge"
[workflow.prompts]
yolo = "custom prompt for --yolo mode (optional)"All commands use shell variable substitution ($VarName). Values are passed as environment variables so they're safe from shell metacharacter injection. Replace with your issue tracker's CLI as needed.
Per sandbox, cbox creates:
- 1 container:
cbox-<project>-<branch>-claude - 1 bridge network:
cbox-<project>-<branch> - 1 image:
cbox-<project>:claude - 1 git worktree directory
- 1 MCP server process (if commands or host_commands are configured)
All are cleaned up by cbox clean.