Devcontainer Sandboxing skill for Claude Code

Skip prompts only inside a boundary you have tested. Claude matches the isolation to the risk: the built-in Bash sandbox for fewer prompts on a laptop, a dev container with a non-root user, persisted config and a default-deny egress firewall for teams, or a disposable container for unattended bypass runs. Every boundary is proven with escape tests, credentials stay scoped, and bypass mode is used only where the tests passed.

  • namedevcontainer-sandboxing
  • size197 lines · ≈2,300 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Runs Claude Code inside isolation boundaries chosen for the risk, from the built-in Bash sandbox (Seatbelt or bubblewrap with filesystem and network allowlists) to dev containers with the Claude Code feature, a non-root user, persisted config volume, and a default-deny egress firewall, to disposable containers or VMs for unattended --dangerously-skip-permissions runs. Covers managed settings in the image, credential hygiene, and escape tests. Use when the user wants Claude to run without prompts safely, asks for a devcontainer, sandbox, or network lockdown, or works with untrusted code.

Try asking

  • Set up a devcontainer so Claude can run with --dangerously-skip-permissions safely
  • Turn on the Bash sandbox and lock network access to npm and GitHub only
  • I need to let Claude work on an untrusted repo — what's the safest setup?

Use it when

  • You want Claude to run long tasks without permission prompts
  • A team wants one isolated, reproducible environment for Claude Code
  • Network egress or filesystem reach must be restricted
  • You are working with untrusted code or repositories

Skip it when

  • You only need allow and deny rules for specific tools or commands — use permissions-policy instead
  • You are hardening production container images rather than agent environments — use container-security instead

How to check the result

Escape tests pass as expected: an arbitrary site is unreachable, the Claude API is reachable, host secrets are absent, the user is non-root, and writes outside the workspace fail.

The common mistake it prevents

Mounting ~/.ssh, cloud credentials or the Docker socket into the container for convenience, which hands a bypass-mode agent everything the boundary was meant to protect.

~/.claude/skills/devcontainer-sandboxing/SKILL.md

--- name: devcontainer-sandboxing description: Runs Claude Code inside isolation boundaries chosen for the risk, from the built-in Bash sandbox (Seatbelt or bubblewrap with filesystem… license: MIT ---

Isolation is done when you have written down what the agent can reach (files, network, credentials), proved each boundary with a command that should fail and does, and matched the permission mode to that boundary. Skipping permissions is acceptable only inside a boundary you have tested; never on a host with your SSH keys and cloud credentials.

Pick the boundary

NeedBoundaryPrompts
Fewer prompts on your laptop, trusted repoBuilt-in Bash sandbox (/sandbox)Auto-allow sandboxed commands
Same isolated toolchain for the whole teamDev container + Claude Code featureNormal or auto mode
Unattended long runs, --dangerously-skip-permissionsDev container or Docker with egress firewall, no host secretsBypass, inside the box only
Untrusted or unknown code, security researchDisposable VM or cloud sandbox, fresh per taskBypass inside; destroy after
Org-enforced policyManaged settings (server-managed or MDM), not repo filesPer policy

Key limits: the built-in sandbox covers shell commands (Bash, PowerShell, Monitor) and their child processes only; Claude’s file tools, MCP servers, and hooks run outside it. A dev container confines everything Claude Code does, but anything mounted (the workspace, ~/.claude credentials) and anything the network allows is reachable.

Option A — built-in Bash sandbox

/sandbox            # panel: enable, mode (auto-allow vs regular prompts), overrides, deps

macOS uses Seatbelt; Linux and WSL2 need bubblewrap and socat (sudo apt-get install bubblewrap socat). On Ubuntu 24.04+, AppArmor may block bwrap’s user namespaces (sysctl kernel.apparmor_restrict_unprivileged_userns returns 1); the sandbox docs give the AppArmor profile fix. Native Windows runs unsandboxed: use WSL2.

{
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false,
    "filesystem": {
      "allowWrite": ["/tmp/build"],
      "denyRead": ["~/.ssh", "~/.aws", "~/**/.env"]
    },
    "network": { "allowedDomains": ["registry.npmjs.org", "github.com", "*.githubusercontent.com"] },
    "excludedCommands": ["docker compose *"]
  }
}
  • Default writable: working directory, temp dir, --add-dir directories. Grant extra write paths with filesystem.allowWrite rather than excluding a tool.
  • allowUnsandboxedCommands: false stops the “retry outside the sandbox” escape hatch.
  • failIfUnavailable: true makes Claude Code exit instead of silently running unsandboxed when dependencies are missing.
  • excludedCommands runs matches with full access; keep patterns narrow, because a tool that executes a file in the repo (compose files, Makefiles) lets Claude write then run.
  • Project settings cannot set filesystem.disabled; user, managed, or --settings can.

Probe: ask Claude to run touch ~/sandbox-probe — it must fail with “Operation not permitted” (macOS) or “Read-only file system” (Linux). Decline any offer to retry unsandboxed; delete the file if it was created.

Option B — dev container

- [ ] 1. devcontainer.json with the Claude Code feature and a non-root remoteUser
- [ ] 2. Persist ~/.claude and set CLAUDE_CONFIG_DIR so login survives rebuilds
- [ ] 3. No host secret mounts; pass short-lived tokens via env/Codespaces secrets
- [ ] 4. Egress firewall (default deny + allowlist) if running without prompts
- [ ] 5. Managed settings baked into the image if policy is needed
- [ ] 6. Run the escape tests below; then choose the permission mode
{
  "name": "app-dev",
  "build": { "dockerfile": "Dockerfile" },
  "features": {
    "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}
  },
  "remoteUser": "node",
  "runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"],
  "mounts": [
    "source=claude-config-${devcontainerId},target=/home/node/.claude,type=volume"
  ],
  "containerEnv": {
    "CLAUDE_CONFIG_DIR": "/home/node/.claude",
    "DISABLE_AUTOUPDATER": "1"
  },
  "postStartCommand": "sudo /usr/local/bin/init-firewall.sh"
}
  • CLAUDE_CONFIG_DIR matters: auth and per-project trust live in .claude.json, which otherwise sits outside ~/.claude and is lost on rebuild.
  • ${devcontainerId} in the volume name isolates state per project.
  • The feature tag pins the install script, not the CLI. For a pinned CLI version, install @anthropic-ai/[email protected] in the Dockerfile and keep DISABLE_AUTOUPDATER=1.
  • NET_ADMIN/NET_RAW are needed only for an in-container firewall.
  • Bake org policy into the image: COPY managed-settings.json /etc/claude-code/managed-settings.json. Anyone with repo write access can edit the Dockerfile, so for non-bypassable policy use server-managed settings or MDM.
  • If the browser login callback never reaches the container, paste the code shown in the browser at the terminal prompt.

Egress firewall (sketch)

Adapt from the reference init-firewall.sh in the anthropics/claude-code repository. The shape:

#!/usr/bin/env bash
set -euo pipefail
ALLOWED=(api.anthropic.com registry.npmjs.org github.com api.github.com objects.githubusercontent.com)
iptables -F OUTPUT; ipset destroy allowed 2>/dev/null || true
ipset create allowed hash:net
for d in "${ALLOWED[@]}"; do
  for ip in $(dig +short A "$d" | grep -E '^[0-9.]+$'); do ipset add -exist allowed "$ip"; done
done
HOST_NET=$(ip route | awk '/default/ {print $3}' | sed 's/\.[0-9]*$/.0\/24/')  # editor port forwarding
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -d "$HOST_NET" -j ACCEPT
iptables -A OUTPUT -m set --match-set allowed dst -j ACCEPT
iptables -P OUTPUT DROP
ip6tables -P OUTPUT DROP 2>/dev/null || true
curl -sS --max-time 5 https://example.com >/dev/null && { echo "FIREWALL OPEN"; exit 1; } || echo "egress locked"
  • Get the authoritative list of Claude Code endpoints (inference, auth, optional telemetry) from the network access requirements page; the list above is illustrative.
  • DNS answers rotate for CDNs; resolve at start and re-run the script if a host starts failing. GitHub publishes ranges at https://api.github.com/meta.
  • Allowing DNS (port 53) to anywhere permits DNS tunneling; for high-assurance setups restrict DNS to the container resolver.
  • Add only registries and hosts the task needs. Every allowed domain is an exfiltration path for anything readable in the container.

Option C — disposable container or VM for unattended runs

docker run --rm -it --user 1000:1000 \
  -v "$PWD":/workspace -w /workspace \
  -e ANTHROPIC_API_KEY="$SCOPED_KEY" \
  --memory 4g --pids-limit 512 \
  my-claude-image claude --dangerously-skip-permissions -p "$TASK" --max-turns 40
  • The CLI refuses --dangerously-skip-permissions as root; use a non-root user.
  • Mount a fresh clone, not your working copy, when the task is risky: bypass mode can rewrite or delete anything in the mounted workspace, and it appears on the host.
  • Use a scoped, revocable API key or claude setup-token output, never your personal cloud credentials. Rotate after runs on untrusted code.
  • For fewer prompts without full bypass, auto mode (a classifier reviews actions) is the middle ground. Disable bypass org-wide with "permissions": {"disableBypassPermissionsMode": "disable"} in managed settings.

Escape tests (run all, record results)

TestExpect
curl -sS --max-time 5 https://example.comFails
curl -sS https://api.anthropic.com (any HTTP status)Connects
ls ~/.ssh ~/.aws 2>&1Absent or not readable
touch /etc/probe or outside workspacePermission denied
whoamiNon-root user
env | grep -iE 'token|secret|key'Only the intended scoped key
docker ps (if socket not intended)Not available

Gotchas

  • Mounting /var/run/docker.sock into the container gives root on the host. Never for agent containers.
  • Bind-mounting ~/.ssh or ~/.aws “just for git” defeats the boundary; use a repository-scoped deploy key or token.
  • Prompt injection from repo content is the main threat in bypass mode: the boundary is what limits the blast radius, not the model’s judgment.
  • A firewall script that fails halfway leaves the container wide open; set -euo pipefail and a final probe that exits non-zero when egress is open.
  • Hooks and MCP servers run outside the Bash sandbox (Option A); review them like code.

Output

**Boundary:** dev container (feature claude-code:1.0, user node), egress allowlist of 6 domains
**Mode:** --dangerously-skip-permissions inside container only; bypass disabled on hosts via managed settings
**Credentials:** scoped API key via Codespaces secret; no host mounts besides /workspace and config volume
**Escape tests:** 7/7 as expected (example.com blocked, api.anthropic.com reachable, ~/.ssh absent)
**Residual risk:** workspace is writable and mirrored to host; allowed registries could receive data

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc