ralph --sandbox docker runs the Claude Code CLI inside an isolated Docker
container instead of directly on your machine (Issue #74, first slice of the
Phase 6.0 sandbox epic #49).
Ralph's orchestration stays on the host; only Claude's execution is containerized:
HOST CONTAINER (persistent)
ralph_loop.sh ── docker exec ──────────▶ claude -p "..." (per iteration)
├─ rate limiting / circuit breaker │
├─ response analysis / exit detection ▼
├─ status.json (ralph-monitor) /workspace ◀── bind mount (rw) ── project dir
└─ cleanup on exit / Ctrl+C
- One persistent container is started before the loop (
docker run -d ... sleep infinity) and reused by every iteration viadocker exec— no per-loop startup cost, and Claude's in-container session state survives across iterations (session continuity works). - The project directory is bind-mounted read-write at
/workspace, so file changes land on the host immediately. No file synchronization layer is needed (cloud-sandbox sync is Issue #76). ralph-monitorworks unchanged —status.jsonis written host-side and gains asandboxfield (provider,container_id,status).- No silent fallback: if sandbox setup fails (no Docker, missing image, bad config), Ralph exits with an error rather than running Claude on the host you asked it to protect.
Docker must be installed and the daemon running. Pull the official image (published to GHCR on release tags — Issue #298) or build it yourself:
# Pull the official image and give it the default name
docker pull ghcr.io/frankbria/ralph-sandbox:latest
docker tag ghcr.io/frankbria/ralph-sandbox:latest ralph-sandbox:latest
# Or build locally from a source checkout
docker build -t ralph-sandbox .
# Or from a global install (install.sh copies the Dockerfile to ~/.ralph)
docker build -t ralph-sandbox ~/.ralphReleases are tagged ghcr.io/frankbria/ralph-sandbox:<version> and :latest,
built multi-arch (linux/amd64 + linux/arm64) and smoke-tested (claude --version as a non-root user) before publishing. You can also point
--sandbox-image ghcr.io/frankbria/ralph-sandbox:latest at it directly.
The default image is node:20-slim plus git, jq, python3, and the Claude Code
CLI, with the base image's non-root node user as the default. At runtime the
container is started with --user "$(id -u):$(id -g)", so everything Claude
writes to the bind-mounted workspace keeps your host ownership and the seeded
0600 credential files stay readable.
| Flag | Default | Description |
|---|---|---|
--sandbox docker |
(off) | Enable Docker sandbox execution |
--sandbox-image IMAGE |
ralph-sandbox:latest |
Container image (must have claude on PATH) |
--sandbox-memory SIZE |
4g |
Memory limit (docker run --memory format) |
--sandbox-cpus NUM |
2 |
CPU limit (decimals allowed, e.g. 1.5) |
--sandbox-network MODE |
bridge |
none, bridge, or host |
The e2b cloud provider is also available — see
E2B_SANDBOX.md. daytona and cloudflare are not
planned (issues #79, #80) and are rejected with a clear error. The sub-flags
require their provider (either via --sandbox or SANDBOX_PROVIDER in
.ralphrc).
.ralphrc equivalents (CLI flags override):
SANDBOX_PROVIDER="docker"
SANDBOX_DOCKER_IMAGE="ralph-sandbox:latest"
SANDBOX_DOCKER_MEMORY="4g"
SANDBOX_DOCKER_CPUS="2"
SANDBOX_DOCKER_NETWORK="bridge"Environment variables of the same names take precedence over .ralphrc, and
--monitor (tmux) forwards all sandbox flags to the loop pane.
Handled by setup_docker_credentials() in lib/sandbox_docker.sh, in order:
ANTHROPIC_API_KEYset — written to a0600env-file in a per-run runtime directory under/tmp(deliberately outside the bind-mounted project, so the sandboxed process cannot read it as a workspace file and it can never be swept into a commit) and passed viadocker run --env-file. The value is never logged; the file is deleted on cleanup. (docker secretis not used — it requires Swarm mode and does not work with plaindocker run.)- Host
~/.claude/.credentials.jsonexists — copied into a container-scoped directory in the same runtime dir, mounted as the container'sHOME. The container can refresh its own session state there without ever touching the host's real~/.claude. Removed on cleanup. - Neither — a warning is logged and the loop continues (useful for custom images with authentication baked in).
bridge(default) — container can reach the Claude API; normal isolation from the host network.host— shares the host network namespace; less isolation, occasionally needed for localhost services.none— full network isolation. This blocks the Claude API, so it only makes sense for images that route through a proxy or have offline tooling; the help text and docs call this out.
- Startup:
init_docker_sandbox(config validation, daemon check, image presence check with build/pull guidance) →setup_docker_credentials→start_sandbox_container. Any failure aborts the run. - Per iteration: the built Claude command array is wrapped as
docker exec -i -w /workspace <container> claude .... Both--live(stream-json) and background modes work through the wrapper. - Timeout (exit 124): the host-side timeout kills only the
docker execclient; the container is restarted (docker restart -t 5) to reap the orphaned in-container process before the next iteration. - Exit: the container is stopped and removed, and credential artifacts are deleted — on graceful completion, circuit-breaker halt, errors, and SIGINT/SIGTERM. Cleanup is idempotent.
- State:
.ralph/.docker_sandbox_state(JSON, atomic temp+mvwrites) tracks image, limits, container id, and status.
Any image with the Claude CLI on PATH works:
FROM ralph-sandbox:latest
USER root
RUN pip3 install --break-system-packages numpy pandas
USER nodedocker build -t my-ml-sandbox .
ralph --sandbox docker --sandbox-image my-ml-sandbox| Symptom | Fix |
|---|---|
Docker daemon is not reachable |
Start the Docker service (sudo systemctl start docker, or Docker Desktop) |
Sandbox image 'ralph-sandbox:latest' not found |
docker pull ghcr.io/frankbria/ralph-sandbox:latest && docker tag ghcr.io/frankbria/ralph-sandbox:latest ralph-sandbox:latest — or build: docker build -t ralph-sandbox ~/.ralph |
| Claude auth errors inside the container | Export ANTHROPIC_API_KEY, or log in on the host first so ~/.claude/.credentials.json exists |
Loop hangs then times out with --sandbox-network none |
Expected — none blocks the Claude API; use bridge |
Orphaned container after a hard kill (kill -9) |
docker ps --filter name=ralph-sandbox then docker rm -f <id>; normal exits and Ctrl+C clean up automatically |