# sandbox Run [Claude Code](https://docs.anthropic.com/en/docs/claude-code) inside a Docker container so it only has access to the directory you explicitly mount in — not your whole filesystem. ## Why? Claude Code runs shell commands, edits files, and installs packages. Running it in a container means: - It can only see the project directory you mount in (not `~/.ssh`, `~/.gnupg`, etc.) - Sensitive files (`.env`, `secret.json`, etc.) are automatically hidden via `/dev/null` mounts - Sensitive directories (`~/.ssh`, `~/.gnupg`) under the workspace are hidden via tmpfs overlays - Your host system stays untouched — packages Claude installs stay in the container ## Quick start ```bash # 1. Build the image ./build.sh # 2. (Optional) Set up environment variables cp .env.sample .env # Edit .env if you want to pre-configure API keys # 3. Run Claude Code on a project directory python3 claude.py ``` This drops you into Claude Code with your current working directory mounted at `/workspace` inside the container. ## How it works ### `claude.py` — the launcher The Python wrapper does three things: 1. **Scans your project** for sensitive files (`.env`, `.envrc`, `secret.json`, `secrets.json`) and generates a Docker Compose override that mounts `/dev/null` over each one 2. **Hides sensitive directories** (`~/.ssh`, `~/.gnupg`) that fall under the mounted workspace using tmpfs overlays 3. **Runs `docker compose run`** with your current directory mounted at `/workspace` ### `entrypoint.sh` — user mapping The entrypoint creates a non-root `claude` user inside the container whose UID/GID matches your host user. This means files Claude creates or edits have the right ownership on your host filesystem. ### `docker-compose.yml` — persistence A named Docker volume (`claude_userhome`) persists Claude's settings, auth tokens, and conversation history across runs. ## Usage ### Set up a shell alias Add this to your `~/.bashrc` or `~/.zshrc` for convenience: ```bash alias claude='python3 /path/to/sandbox/claude.py' ``` Then from any project directory: ```bash cd ~/projects/my-app claude # launches Claude Code with my-app mounted claude . # same thing claude --help # pass any flags through to Claude Code ``` ### Authentication You have two options: 1. **Interactive login** — run `claude login` inside the container. Credentials persist in the `claude_userhome` volume. 2. **API key** — copy `.env.sample` to `.env` and set `ANTHROPIC_API_KEY`. ### Passing extra arguments Any arguments to `claude.py` are forwarded to the Claude Code CLI: ```bash python3 claude.py --help python3 claude.py --model sonnet python3 claude.py -p "explain this codebase" ``` ## What's in the image The Dockerfile installs: - **Core**: git, Node.js, Python 3, ripgrep, fd, gh (GitHub CLI), vim - **Build tools**: cmake, build-essential, pkg-config - **Rust**: rustup + cargo (with musl target) - **Playwright**: Chromium + browser automation deps - **Hugo**: static site generator - **Terraform**: infrastructure as code - **abra**: [Co-op Cloud](https://coopcloud.tech) CLI - **Caddy**: web server / reverse proxy - **Tailscale**: mesh VPN The tools beyond core are included because this project (a Co-op Cloud recipe maintainer) uses them. If you're adapting this for your own use, remove or comment out what you don't need in the Dockerfile to speed up the build, and add your own tools: ```dockerfile # Example: add Go RUN curl -fsSL https://go.dev/dl/go1.22.0.linux-amd64.tar.gz | tar -xz -C /usr/local ``` Then rebuild with `./build.sh`. ## Sensitive file protection The launcher automatically hides files matching these names anywhere in your project tree: - `.env` - `.envrc` - `secret.json` - `secrets.json` And hides these directories if they exist under the mounted workspace: - `~/.ssh` - `~/.gnupg` To add more, edit the `SENSITIVE_NAMES` set or `SENSITIVE_DIRS` list in `claude.py`. ## Running the tests The test suite verifies that sensitive files are properly hidden inside the container: ```bash python3 tests/test_env_hiding.py ``` Requires the `sandbox` image to be built first. ## Troubleshooting **"Permission denied" on mounted files** — make sure `HOST_UID` / `HOST_GID` are passed correctly. The `claude.py` launcher handles this automatically; if using `docker compose` directly, set them: ```bash HOST_UID=$(id -u) HOST_GID=$(id -g) docker compose run --rm claude ``` **Claude Code not found** — rebuild the image (`./build.sh`). The install script fetches the latest version at build time. **Persisted state issues** — to reset Claude's saved state, remove the Docker volume: ```bash docker volume rm sandbox_claude_userhome ```