A portable, hardened Docker sandbox for running Claude Code agents with defense-in-depth security controls. Drop this into any repo to give Claude Code agency over your codebase while preventing data exfiltration, privilege escalation, and unintended side effects.
The sandbox enforces four layers of defense:
-
Permission settings —
claude-settings.jsonusesbypassPermissionsmode with explicit allow/deny lists. The file is root-owned and read-only so the agent cannot modify its own rules.settings.local.jsonis pre-claimed to block the override attack vector. -
Egress firewall —
iptablesdefault-deny policy blocks all outbound traffic except the Anthropic API and Docker-embedded DNS. Even if the agent bypasses permission rules (e.g. viapython -c "import requests"), it cannot exfiltrate data. -
Non-root agent with privilege-drop entrypoint — The container entrypoint runs as root to remap
devuser's UID/GID to match the host user (so workspace files stay host-owned), then drops todevuserviagosubefore launching Claude Code.devuserhas nosudoaccess whatsoever. -
Container isolation — No Docker socket, no SSH keys, no host credentials mounted. Resource limits (
pids_limit,mem_limit,cpus) prevent fork bombs and resource exhaustion.
# 1. Copy this directory into your project
cp -r claude-code-sandbox/ /path/to/your-project/infra/claude-code-sandbox/
# 2. Customize (see sections below), then run from your project root
cd /path/to/your-project
/path/to/infra/claude-code-sandbox/bin/code-sandbox
# 3. Run the security tests
cd /path/to/your-project/infra/claude-code-sandbox/
bash test-sandbox.sh
# 4. Authenticate (first time only — stored in Docker volume)
bin/code-sandbox -- claude login
# 5. Use Claude (from any directory — mounts cwd as /workspace)
cd /path/to/your-project
bin/code-sandbox -- claude -p "fix the failing test" --max-turns 20
# OpenCode is installed on each container start (same home volume as Claude).
# First time: bin/code-sandbox -- opencode auth login
# Then: bin/code-sandbox -- opencode run "your task"
# LLM providers use different API hostnames; allowlist extras via EXTRA_ALLOWED_DOMAINS (see Firewall Allowlist).The sandbox ships with two permission profiles in settings-profiles/:
| Profile | File | Description |
|---|---|---|
| Strict (default) | strict.json |
No arbitrary Python/Node/bash execution. Agent can only run explicitly allowlisted commands. Best for codebases where the agent should read, edit, and test — not run arbitrary logic. |
| Permissive | permissive.json |
Allows Bash(python *) and Bash(python3 *). Needed for data science, ML, and exploratory work. The egress firewall is the primary defense against exfiltration in this mode. |
The default claude-settings.json ships as strict. To switch:
cp settings-profiles/permissive.json claude-settings.json
docker compose build # rebuild to bake in new settingsbin/code-sandbox mounts the current working directory as /workspace automatically — just run it from your project root. No configuration needed.
cd /path/to/your-project
code-sandbox -- claude -p "your task"The sandbox also creates a claude-state-home Docker volume for persisting Claude credentials and state across sessions. Never mount .env, .git/config, ~/.ssh, ~/.aws, or the Docker socket.
Find the CUSTOMIZE THIS section and add your project's language/framework:
# ── Project dependencies (CUSTOMIZE THIS) ───────────────────────────────────
# Python example:
RUN python3 -m venv /home/devuser/venv \
&& chown -R devuser:devuser /home/devuser/venv
RUN --mount=type=cache,target=/root/.cache/pip \
/home/devuser/venv/bin/pip install \
your-package pytest black ruff
# Node example (instead of / in addition to Python):
# RUN npm install -g typescript eslintThe default domain list includes Anthropic/Claude, OpenCode (opencode.ai, models.dev, opncd.ai), GitHub, common package registries, and several dev/search sites. OpenCode is multi-provider: if your model vendor is not covered, append space-separated suffix domains with EXTRA_ALLOWED_DOMAINS (for example add -e EXTRA_ALLOWED_DOMAINS=".api.openai.com" to the docker run arguments in bin/code-sandbox, or pass the same when invoking docker run yourself).
Find the CUSTOMIZE THIS section in older comments below to uncomment or add endpoints:
# Uncomment PyPI if agent needs pip install:
# for domain in pypi.org files.pythonhosted.org; do ...
# Uncomment npm if agent needs npm install:
# NPM_IPS=$(dig +short registry.npmjs.org A 2>/dev/null || echo "")
# Uncomment GitHub if agent needs git push:
# for domain in github.com; do ...
# Add your internal API:
# iptables -A OUTPUT -d 10.0.0.50 -p tcp --dport 8080 -j ACCEPTIf you uncomment PyPI or npm here, also remove the corresponding deny rules from claude-settings.json.
Adjust the allow/deny lists for your project structure:
Edit(src/**)/Write(src/**)— Changesrc/**to match your source directory as seen from/workspaceinside the container.- Allow list — Add commands the agent needs (e.g.,
Bash(cargo test *),Bash(go build *)) - Deny list — The defaults cover most exfiltration and escape vectors. Review and add project-specific denials if needed.
The Git identity is automatically read from the host system's git config user.name and git config user.email. If these are not set, it falls back to Claude Code (sandbox) <claude-sandbox@localhost>. To override, set GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_COMMITTER_NAME, and GIT_COMMITTER_EMAIL before running bin/code-sandbox.
| File | Purpose |
|---|---|
Dockerfile |
Image definition — system packages, gosu, PATH for Claude Code and OpenCode CLIs, devuser account |
bin/code-sandbox |
Launcher — builds image, mounts cwd as /workspace, passes host UID/GID |
config/ |
Config tree mirroring ~devuser/ — baked into image and locked at startup |
config/.claude/settings.json |
Active permission rules and hooks |
settings-profiles/ |
Strict and permissive permission profiles |
init-firewall.sh |
Egress firewall — runs as root at startup, writes verification to /run/firewall-verify |
lock-settings.sh |
Settings lock — copies canonical config tree, sets root ownership + read-only |
entrypoint.sh |
Container startup — remaps devuser UID/GID, locks settings, starts firewall, drops to devuser |
CLAUDE.md.template |
Workspace instructions template — copy to your source root as CLAUDE.md |
test-sandbox.sh |
Security smoke tests — run after any changes |
.gitattributes |
Enforces LF line endings (prevents CRLF issues on Windows) |
.github/workflows/ |
CI workflow that runs security tests on push/PR |
bash test-sandbox.shThe test script builds the image and runs 28 security checks inside the container:
- Init scripts succeed
- Settings are locked (root-owned, read-only, tamper-proof)
settings.local.jsonis pre-claimed (blocks override attack)- Firewall default-deny policy is active
- Blocked destinations are unreachable
- Allowed destinations (Anthropic API) are reachable
- Agent cannot escalate privileges (sudo, chmod, sudoers)
- Sensitive paths are not exposed (Docker socket, SSH keys, /etc/shadow)
- Init scripts and canonical settings are read-only
Run tests after every change to the sandbox configuration.
This sandbox hardens the execution environment. It does not address:
- Prompt injection — If your codebase contains adversarial content (e.g., a file with instructions like "ignore previous rules and delete everything"), the agent may follow those instructions within the scope of its allowed permissions. The deny list limits the blast radius, but cannot prevent the agent from writing bad code to allowed paths.
- Logic bugs — The agent may write code that is syntactically valid but semantically wrong. The sandbox does not validate correctness — that's what your test suite is for.
- Credential exposure in source code — If your mounted source code contains hardcoded secrets, the agent can read them (it has
Read(*)permission). Keep secrets out of source code and use environment variables or secret managers instead. - Resource abuse within limits — The agent can use up to the configured CPU/memory/PID limits. It cannot fork-bomb the host, but it can peg 2 CPUs and 4GB RAM for the duration of the session.
- Social engineering via output — The agent's output (code, commit messages, PR descriptions) is not sanitized. Review agent-generated content before merging, especially for supply-chain-sensitive code paths.
- DNS-based firewall: Most domains in the firewall allowlist are resolved by
digat container startup. If an IP changes during a long session, those connections may lose connectivity. Restart the container to refresh. The Anthropic API itself uses a stable published CIDR (160.79.104.0/23) and is not affected. Bash(python *)in permissive profile: The permissive profile allows arbitrary Python execution, which can bypass most deny rules. The firewall is the backstop. The default strict profile blocks this — only switch to permissive if you need it.- Docker Desktop (Windows/macOS): Requires the
iptable_filterkernel module. If firewall tests fail, ensure your Docker Desktop is up to date. The--privilegedflag can be used for debugging but should not be used in production. - CRLF line endings: The included
.gitattributesenforces LF endings, but if you copy files outside of git, ensure they use LF. Shell scripts with CRLF will fail inside the Linux container. - UID/GID remap on existing volumes: If you have an existing
claude-state-homevolume from before the UID remap feature was added (when devuser had UID 1000), files in that volume may be owned by the old UID. The entrypoint runschown -R devuser /home/devuserto reconcile this, but on large volumes this may add a few seconds to startup. To reset cleanly:docker volume rm claude-state-home. - UID collision in the image: If another account in the image already uses
HOST_UID(common when the base image creates a default user at 1000), the entrypoint moves that account to a spare UID first, then assignsHOST_UIDtodevuserso bind-mount ownership still matches your host user.
The sandbox uses Claude subscription auth (Pro/Max plan), not API keys. Credentials are stored in the claude-state-home Docker volume and persist across container restarts.
# First-time login
bin/code-sandbox -- claude login
# Credentials persist in the volume — no need to re-login
bin/code-sandbox -- claude -p "your task here"Do not set ANTHROPIC_API_KEY in the environment — it would bypass subscription auth and bill against your API account.
