Use the prebuilt container image (no devcontainer)#
Run fully sandboxed Claude Code on any Linux host with rootless podman
(or docker) — no devcontainer, no VS Code, no root access on the host.
The published image ships the whole sandbox pre-installed: the claude
shadow, the relocated real binary, the integrity
guard, and the network egress
jail.
Image: ghcr.io/gilesknap/claude-sandbox:latest (amd64 + arm64), built
by CI from the same install.sh the devcontainer runs — plus a weekly
rebuild so the baked-in Claude tracks upstream releases. The in-image
auto-updater is deliberately disabled (that is part of the integrity
guard), so updating means pulling a newer image, not letting a running
container update itself.
Prerequisites#
rootless podman (or docker). On shared or centrally managed machines this may need IT to provision subuid/subgid ranges once per user — the same requirement as any rootless container use.
/dev/net/tunon the host (present on stock Linux). The egress jail is fail-closed without it.Unprivileged user namespaces enabled — the default on RHEL 8/9 and most distros. Ubuntu 24.04 hosts restrict them via AppArmor; the container entrypoint probes and refuses with instructions rather than running unsandboxed.
Quick start#
Fetch the launcher and put it on your PATH:
curl -fsSLO https://raw.githubusercontent.com/gilesknap/claude-sandbox/main/container/claude-container
chmod +x claude-container
The launcher runs unsandboxed on your host, so give it the scrutiny
that deserves: it is ~200 lines of plain bash — read it before you run
it. For fixed provenance, replace main in the URL with a release tag
or commit SHA (any ref that contains container/claude-container) and
re-fetch the same pinned ref when you update:
curl -fsSLO https://raw.githubusercontent.com/gilesknap/claude-sandbox/<tag-or-commit>/container/claude-container
You don’t have to watch this repo for launcher fixes: each published
image carries a label naming the launcher version it was built and
tested with, and on every run the launcher compares itself against your
locally pulled image (claude-container --version prints your copy’s
version). When your copy is older it prints a curl command pinned to
the exact revision the image was built from; it never updates itself —
the launcher runs unsandboxed, so replacing it stays a deliberate,
reviewable act.
Then, from any project directory:
cd ~/src/my-project
claude-container
The first run pulls the image, creates a container named after the
project directory, and starts sandboxed claude with the project
mounted read-write. Later runs restart the same container. Everything
you know from the devcontainer applies inside: /verify-sandbox runs
the live battery, the egress jail is on by default, and plain claude
can only ever resolve to the shadow.
One named container per project#
The launcher deliberately creates a persistent named container per
project directory rather than a throwaway --rm container:
gh/glab logins made inside it (see below) live for the container’s lifetime — the same container-scoped credential model as a devcontainer, without re-pasting a PAT on every launch. Credentials are never mounted from the host.
claude-container --recreateremoves and recreates it (do this after pulling a newer image, or to change create-time settings). Forge logins must then be re-done — that ceremony is the deliberate cost of keeping PAT blast radius small.Arguments after the options are passed to
claudewhen the container is created; a plain restart reuses them. If the container is already running (a session is active),claude-containeropens an additional sandboxed session in it instead, and fresh arguments do apply on that path.
Authenticate to forges#
Inside the container, the clone is baked at /opt/claude-sandbox, so
the usual recipes work:
just --justfile /opt/claude-sandbox/justfile gh-auth
just --justfile /opt/claude-sandbox/justfile glab-auth gitlab.example.com
See Authenticate with forges for the recommended PAT scopes.
Persist login and memory#
The launcher mounts ~/.config/terminal-config (override:
CLAUDE_SANDBOX_SHARED_CONFIG) at /user-terminal-config, and the
entrypoint symlinks ~/.claude and ~/.claude.json into it — the same
convention devcontainers use, so a host that runs both shares one Claude
login, memory, and settings. You log in to Claude once, not once per
container.
Configure the sandbox#
Per-session (create-time) settings are environment variables, passed through automatically when the container is created:
CLAUDE_SANDBOX_NO_FORGE=1 claude-container # no forge creds inside
CLAUDE_SANDBOX_EGRESS_JAIL=0 claude-container # jail off (not recommended)
They are frozen into the container at create time — --recreate to
change them.
Durable settings go in ~/.config/claude-sandbox.conf (override:
CLAUDE_SANDBOX_CONF), written in the normal
claude-sandbox.conf format. When the file
exists the launcher mounts it read-only over
/etc/claude-sandbox.conf — the canonical path the shadow reads. The
usual rule that the conf must live outside the sandbox’s writable set
still holds: inside the container it is at /etc and read-only, so a
compromised session cannot widen its own binds for the next launch.
# ~/.config/claude-sandbox.conf
allow-ip = 172.23.1.3 # keep this IOC reachable past the blackhole
To make extra folders writable, --mount binds them into the container
and adds a matching allow-write entry for the sandbox:
claude-container --mount ~/src/shared-lib
EPICS / lab-device hosts#
--host-net creates the container with --network=host (Channel Access
broadcast for non-Claude shells). Claude itself stays inside the egress
jail either way — the jail is container-network-mode-agnostic — so
device access for Claude is still granted per-IP with allow-ip.
Update#
podman pull ghcr.io/gilesknap/claude-sandbox:latest
claude-container --recreate
If you launch with CLAUDE_SANDBOX_ENGINE=docker, pull with docker
instead — and set the variable on the --recreate run too (the launcher
reads it on every invocation; it is not remembered).
Limitations#
Your toolchain isn’t in the image. The base is the DLS ubuntu-devcontainer (git, build-essential, uv, gh/glab, just…), not your site’s module system or cross-compilers. Claude can read, edit, build what the image supports, and commit; site-specific builds may still happen outside the container.
Claude’s version is the image’s. By design (disabled updater); pull +
--recreateto update.Rootless podman is the supported engine.
CLAUDE_SANDBOX_ENGINE=dockerexists, but under rootful docker the egress jail’s pasta attach is denied (Couldn't open user namespace ... Permission denied— differing namespace/ptrace semantics), soclaudefail-closes at launch. Rootless docker is untested. If you must use such an engine, the jail can be disabled per session (CLAUDE_SANDBOX_EGRESS_JAIL=0) — a weaker posture; prefer rootless podman.Linux only — the sandbox is built on Linux namespaces. macOS with
podman machineruns the Linux image in a VM and should work, but is untested.