Make extra paths writable#

By default the sandbox makes only $PWD writable. Sibling projects under /workspaces/ are read-only. This recipe covers the two ways to widen that scope.

Restore the broad /workspaces bind#

To make every sibling devcontainer project writable again, set CLAUDE_SANDBOX_WORKSPACE_ROOT in your devcontainer’s remoteEnv:

// .devcontainer/devcontainer.json → remoteEnv
"CLAUDE_SANDBOX_WORKSPACE_ROOT": "/workspaces"

Restart (or rebuild) the devcontainer for the change to take effect.

Add specific writable paths#

For extra writable paths without widening to all of /workspaces, add allow-write lines to the sandbox config. Edit it in the clone at .devcontainer/claude-sandbox.conf:

# .devcontainer/claude-sandbox.conf  (installed to /etc/claude-sandbox.conf)
allow-write = /cache
allow-write = /workspaces/sibling-project

One absolute path per line. Blank lines and # comments are ignored; non-existent paths are skipped.

The same conf also carries the network-jail keys egress-jail and allow-ip (the on-by-default lateral-movement isolation). Those are covered in Configure the network egress jail; for the full key reference see Configuration.

A path may be a directory, a file, or a unix socket.

Reach a container-engine socket#

Warning

A container-engine socket is a sandbox escape. Whoever holds the engine socket can start a container that bind-mounts any path the engine’s account can read — your $HOME, your ssh keys, the sandbox conf itself — and run arbitrary code there, outside every jail this project builds. Binding the socket in hands the agent exactly that power: the sandbox can restrict which paths the socket file is reachable at, but it cannot constrain what the engine on the far side will do when asked.

Do not expose your host’s podman/docker socket — including one already mounted into your devcontainer. If the agent genuinely needs a container engine, give it a dedicated, disposable one that holds nothing you care about (a rootless engine under a throwaway account, or an engine inside the devcontainer itself).

The same reasoning applies to any socket, not just container engines: a socket is an API, and binding it into the sandbox grants the agent everything that API can do with your privileges. Before exposing one, think through what is listening on the other end and what you are giving the agent.

The shape that stays inside the warning above is an engine that exists only for this devcontainer: a rootless engine started inside the container, under the container user, holding no images, volumes or mounts you care about — e.g. podman system service --time=0 &. Its socket lands under the container’s own $XDG_RUNTIME_DIR — typically /run/user/<uid>/podman/podman.sock or /run/user/<uid>/docker.sock. The sandbox masks /run/user with a tmpfs, because that directory is also where ssh-agent, gpg-agent, dbus and keyring sockets live.

allow-write re-exposes a single path through that mask, so name the socket itself rather than lifting the mask off the whole directory:

# The dedicated in-container engine socket, and nothing else under /run/user.
allow-write = /run/user/1000/podman/podman.sock

Use the uid of the account running the engine (id -u). Since the conf is host-global, a hardcoded uid is fine — it describes one machine.

What must not go on that line is the path where your devcontainer mounts the host’s engine socket — a common devcontainer.json pattern mounts the host’s podman.sock or /var/run/docker.sock into the container so builds can reuse the host engine. That mount is exactly what the warning above is about: it is your host’s engine, and no allow-write scoping makes it safe to hand to the agent.

Making the socket reachable is only half the job: the sandbox scrubs the environment, so DOCKER_HOST does not survive into the session — forward it with pass-env so the client finds the socket.

Prefer the socket path over the enclosing directory: allow-write = /run/user/1000 would work, but it unmasks the agent and keyring sockets next to it, handing a compromised session your ssh credentials.

The engine socket is a unix socket, not a network connection, so the egress jail does not stand in its way.

Applying the change#

install.sh copies the clone’s .devcontainer/claude-sandbox.conf to the host-global /etc/claude-sandbox.conf, which the shadow reads at launch. After editing the conf, either:

  • re-run ./install, or

  • rebuild the devcontainer (postCreate re-stamps the conf).

Why the conf lives in /etc, not the workspace#

The config is read from /etc/claude-sandbox.conf rather than from the rw-bound workspace so that a compromised in-session Claude cannot rewrite it to widen the next launch’s binds. The clone’s .devcontainer/claude-sandbox.conf is the editable source; /etc is the authoritative copy the shadow trusts. See the threat model for why the workspace itself is not a trusted location.