Sanitized single-commit public mirror of recipe-maintainer. - Removed test-ssh/.testenv (live creds); added test-ssh/.testenv.example placeholders. - Removed plans/ and planned-updates/ (deployment-planning docs) so no client/ deployment domains appear in the public repo. - All other secret stores were already gitignored. - docs.coopcloud.tech retained as a submodule (public upstream).
sandbox
Run 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/nullmounts - 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
# 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:
- Scans your project for sensitive files (
.env,.envrc,secret.json,secrets.json) and generates a Docker Compose override that mounts/dev/nullover each one - Hides sensitive directories (
~/.ssh,~/.gnupg) that fall under the mounted workspace using tmpfs overlays - Runs
docker compose runwith 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:
alias claude='python3 /path/to/sandbox/claude.py'
Then from any project directory:
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:
- Interactive login — run
claude logininside the container. Credentials persist in theclaude_userhomevolume. - API key — copy
.env.sampleto.envand setANTHROPIC_API_KEY.
Passing extra arguments
Any arguments to claude.py are forwarded to the Claude Code CLI:
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 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:
# 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.envrcsecret.jsonsecrets.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:
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:
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:
docker volume rm sandbox_claude_userhome