Network-jailed Docker environment for running the Ralph iteration loop autonomously. The sandbox provides process isolation and network restrictions while sharing the repo filesystem with the host via bind mount.
Host (interactive) Docker (autonomous)
────────────────── ───────────────────
1. /ralph Phase 1 → prd.json
2. /ralph Phase 2 → .claude/ralph-prompt.md
3. pnpm install
4. git checkout -b feat/...
5. docker compose up ───────────────→ 6. .claude/ralph.sh --force
7. Claude iterates (headless)
- reads prd.json ←── bind mount
- writes code ←── bind mount
- commits to .git ←── bind mount
- updates prd.json ←── bind mount
8. ralph.sh exits
9. git log (sees commits) ←────────── (same filesystem)
10. git push origin <branch>
11. gh pr create
Three zones:
- Creative (host) — spec authoring, prd.json conversion, prompt crafting. This is where you think.
- Execution (Docker) — ralph.sh iterates autonomously. This is where it works.
- Coordination (host) — push, PR, review. This is where you ship.
The compose file intentionally does not pin container_name. If you need multiple .ai-dev sandboxes at once, start each one with its own Compose project name:
docker compose -p ai-dev-auth -f .ai-dev/docker-compose.yml up -d
docker compose -p ai-dev-billing -f .ai-dev/docker-compose.yml up -dUse the same -p <name> value for follow-up commands like exec, logs, and down:
docker compose -p ai-dev-auth -f .ai-dev/docker-compose.yml exec sandbox bash
docker compose -p ai-dev-auth -f .ai-dev/docker-compose.yml down| Scenario | Recommended | Why |
|---|---|---|
| Short feature (1-5 stories), developer present | Host | Simpler — Ralph Phase 3 runs directly |
| Long-running feature, run overnight or AFK | Docker | Network jail, memory limits, detachable terminal |
| CI/CD integration | Docker | Reproducible, isolated |
No .ai-dev/ infrastructure in repo |
Host | Docker is optional |
The /ralph and /ship skills support Docker execution via flags:
/ralph path/to/SPEC.md --docker
/ship path/to/SPEC.md --ralph-docker
Ralph auto-discovers the compose file in the repo (searches for a docker-compose.yml defining a sandbox service). To skip discovery, pass the path explicitly:
/ralph path/to/SPEC.md --docker .ai-dev/docker-compose.yml
When passed, Ralph runs ralph.sh inside the Docker sandbox instead of on the host. Phases 1-2 (Convert, Prepare) still run on the host — only Phase 3 (Execute) moves into the container.
- Docker and Docker Compose
- The
/ralphskill installed (for Phase 1-2 on host) ANTHROPIC_API_KEYset in your environment
cd .ai-dev
cp .env.example .env
# Edit .env — add your ANTHROPIC_API_KEY
docker compose buildThere are three ways to use the Docker sandbox, depending on where you are in the workflow.
Use when you want t 8000 he interactive spec/prd work on host, then hand off execution to Docker for autonomous iteration.
1. Prepare on host (interactive Claude Code session):
# In your repo root, start Claude Code and invoke /ralph with your spec:
# /ralph path/to/SPEC.md
#
# Ralph Phase 1 converts the spec → prd.json
# Ralph Phase 2 crafts the prompt → .claude/ralph-prompt.md
# → .claude/ralph.sh (execution script)
#
# Tell Ralph to stop after Phase 2 — you'll handle execution via Docker.
# Create feature branch
git checkout -b feat/my-feature
# Install dependencies (container shares this via bind mount)
pnpm install
# Commit artifacts so the container starts clean
git add prd.json .claude/ralph-prompt.md .claude/ralph.sh
git commit -m "chore: add Ralph artifacts for my-feature"2. Start the sandbox and execute:
cd .ai-dev
docker compose up -d
docker compose exec sandbox bash
# Inside the container — you are now at /workspace
.claude/ralph.sh --max-iterations 20 --max-turns 100 --forceYou can detach (Ctrl+P, Ctrl+Q) and check back later.
3. Review and ship on host:
# Changes are already in your repo (bind mount)
git log --oneline
git diff HEAD~5
# Push and create PR
git push origin feat/my-feature
gh pr create --title "feat: my-feature" --body "..."4. Clean up:
cd .ai-dev
docker compose downUse when prd.json and .claude/ralph-prompt.md already exist from a previous session — you just need to resume or re-run execution.
cd .ai-dev
docker compose up -d
docker compose exec sandbox bash
# Inside the container:
.claude/ralph.sh --forceUse when you want the full Ralph workflow inside the container. Host plugins (including /ralph) are available inside Docker via the plugin mount.
cd .ai-dev
docker compose up -d
docker compose exec sandbox bash
# Inside the container — start Claude Code and use /ralph normally:
claude
# Then: /ralph path/to/SPEC.mdNote: Interactive spec work is less convenient inside Docker (no browser tools, no macOS computer use). Pattern A is preferred for most workflows.
| Domain | Access | Purpose |
|---|---|---|
*.anthropic.com |
Full | Claude API calls |
*.claude.com |
Full | Claude Code authentication |
registry.npmjs.org |
Full | pnpm install |
*.inkeep.com |
Full | Organization services |
api.github.com |
Full | GitHub API (PR, reviews, CI) |
github.com/inkeep/* |
Path-restricted | Git push/pull (org repos only) |
*.githubusercontent.com/inkeep/* |
Path-restricted | GitHub raw content |
| Everything else | Blocked |
| Path | Container access | Notes |
|---|---|---|
/workspace/ |
Read-write | Bind mount of repo root — same files, same .git |
/home/agent/.claude/ |
Read-write | Docker volume — persists Claude auth across restarts |
/host-plugins/ |
Read-only | Host's ~/.claude/plugins/ — copied to container on startup by entrypoint |
| Everything else | Container-only | Lost when container is removed |
- Access host home directory (
~/.ssh/,~/.gitconfig/, etc.) - Access other repos or projects on the host
- Run processes on the host
- Reach arbitrary internet domains (network jail)
Edit squid.conf:
# Add a new domain
acl my_domain dstdomain .example.com
http_access allow my_domainThen restart the proxy: docker compose restart proxy
If you want the old strict model (no package installation inside container), comment out the npm ACL in squid.conf:
# acl npm_registry dstdomain registry.npmjs.org
# http_access allow npm_registryThe container will use node_modules/ from the host's bind mount. Run pnpm install on the host before starting Docker.
Edit docker-compose.yml:
deploy:
resources:
limits:
memory: 16G # Default: 14GTo enable git push and PR creation from inside the container:
- Set
GITHUB_TOKENin.env - The
ghCLI and git are pre-installed in the container - The GitHub API is already in the allowlist
Note: gh CLI reads GITHUB_TOKEN from the environment and works out of the box. For git push to authenticate, you would also need a git credential helper configured — this is not set up by default. A future enhancement could add git config --global credential.helper '!f() { echo "password=$GITHUB_TOKEN"; }; f' to entrypoint.sh.
This changes the trust model — the container can now push code and create PRs on your behalf.
The Ralph skill places ralph.sh at .claude/ralph.sh during Phase 2 (alongside .claude/ralph-prompt.md). The container accesses it via the bind mount at /workspace/.claude/ralph.sh.
The canonical source is in the Ralph skill (scripts/ralph.sh). Each Phase 2 run copies a fresh version — no manual sync needed.
Run .claude/ralph.sh --help for CLI options.
┌─────────────────────────────────────────────────────────┐
│ Host Machine │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Docker Internal Network │ │
│ │ │ │
│ │ ┌────────────┐ ┌──────────────────┐ │ │
│ │ │ Squid │ ◄────── │ Claude Sandbox │ │ │
│ │ │ Proxy │ │ │ │ │
│ │ │ (SSL bump) │ │ /workspace ─────┼──────┼──┼──► ../repo
│ │ └─────┬──────┘ └──────────────────┘ │ │
│ │ │ │ │
│ └────────┼───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ External Net │ ──► Only allowed domains │
│ └────────────────┘ │
└──────────────────────────────────────────────────────────┘
The container runs entrypoint.sh on startup, which:
- Copies host plugins — If
~/.claude/plugins/is mounted at/host-plugins/, copies them into the container's~/.claude/plugins/so skills and hooks are available inside Docker. - Configures sandbox — Sets
enableWeakerNestedSandbox: truein Claude Code's settings. Claude Code's bubblewrap sandbox cannot run in unprivileged Docker; this flag tells it to use a weaker sandbox and rely on the Docker container + Squid proxy as the security boundary.
The sandbox uses a custom Dockerfile (Dockerfile) instead of the official docker/sandbox-templates:claude-code image. This gives us:
| Tool | Version | Why custom |
|---|---|---|
| Node.js | 22 | Repo requires >=22 (official image has 20) |
| pnpm | 10.10.0 | Repo package manager (not in official image) |
| gh | Latest | GitHub CLI for optional PR workflow |
| jq | Latest | JSON processing for ralph.sh |
| Claude Code | Latest | Installed via official installer |
To update the image after changes: docker compose build --no-cache
docker compose ps # Check if proxy is running
docker compose logs proxy # Check for errors
docker compose restart # Restart everythingdocker compose exec proxy tail -f /var/log/squid/access.logLook for TCP_DENIED entries.
The Claude data volume persists auth. If auth breaks:
docker compose down
docker volume rm ai-dev_claude-data
docker compose up -d
# Re-authenticate inside the containerRun /ralph Phase 1-2 on the host first. The prompt must exist at .claude/ralph-prompt.md before starting Docker execution.
Run pnpm install on the host before starting Docker. The container accesses node_modules/ via the bind mount.
If a story requires a NEW dependency, the container can install it (npm registry is in the allowlist). But run pnpm install on the host afterward to ensure the lockfile is consistent.
The current sandbox is execution-only — ralph.sh iterates inside Docker, host handles everything else (spec, push, PR, review). A future upgrade could enable full autonomous operation inside the container:
| Item | What it enables | Trigger to revisit |
|---|---|---|
| Git credential helper in entrypoint | git push from inside the container |
Want to push/PR from Docker instead of host |
CLAUDE_SPECS_DIR=/workspace/.claude/specs |
/spec output persists via bind mount |
Want to run /spec inside the container |
gh auth config (GH_TOKEN export) |
gh pr create, gh api from inside |
Want full /ship review loop inside Docker |
Git URL HTTPS rewrite (insteadOf ssh) |
Prevents SSH attempts through the proxy | If tools default to SSH and silently fail |
| Health checks in docker-compose | Proxy readiness before sandbox starts | Intermittent startup failures |
| Convenience start script | Pre-flight checks (token set, plugins exist) | Team onboarding friction |
See ~/.claude/specs/docker-sandbox-upgrade/SPEC.md for the full analysis that drove these decisions.
- SSL inspection: The proxy performs MITM on HTTPS traffic for URL path filtering. All traffic is decrypted by the proxy.
- Network isolation: The sandbox has no direct internet access — all traffic goes through the proxy.
- Bind mount: The container has read-write access to the entire repo directory, including
.git/and.envfiles. - npm registry: Enabled by default.
pnpm installinside the container triggers postinstall scripts from downloaded packages. These scripts run with the same filesystem access as Ralph (the repo directory). See squid.conf to disable if this is a concern.