Pass environment variables in#

The sandbox scrubs the environment (--clearenv) and re-exports only what Claude itself needs — TERM, LANG, VIRTUAL_ENV, the UV_* vars and a few others. Nothing else survives, including variables the surrounding devcontainer set via containerEnv, runArgs -e=… or remoteEnv.

That is deliberate: the sandbox builds its environment rather than inheriting one, so a variable reaches the session only if something says it should. This recipe is how you say so.

Warning

Every variable you forward is disclosed to the agent — and to every tool, test and script the session runs, and potentially to the model provider in context. Before adding a name to pass-env, check what your shell actually holds under it: tokens, API keys, cloud credentials and connection strings with embedded passwords are all one env away once forwarded. Forward the minimum, prefer pointer variables (DOCKER_HOST, a path, a hostname) over secret-bearing ones, and never forward a variable whose value you would not paste into the chat.

If a variable points at a unix socket, forwarding the name is the small half of the decision — see the warning in Make extra paths writable before binding the socket itself in.

Symptoms#

The environment is empty rather than wrong, so the failure usually shows up as something further downstream:

$ env | grep DOCKER_HOST      # inside the sandbox
                              # (nothing)

A test suite that reads its configuration from the environment tends to fail in a way that doesn’t mention the environment at all — a fixture that skips its yield, a client that falls back to a default socket path. If a command works in your devcontainer terminal but not under claude, check the environment first.

Forward a variable#

Name it with pass-env in the sandbox config. Edit the clone at .devcontainer/claude-sandbox.conf:

# .devcontainer/claude-sandbox.conf  (installed to /etc/claude-sandbox.conf)
pass-env = DOCKER_HOST
pass-env = MY_SERVICES_PATH, MY_FIXTURE_DIR

Comma- or space-separate the names, and/or repeat the key. Then re-run ./install, or rebuild the devcontainer (postCreate re-stamps the conf).

These are names, not assignments. The value is read from the environment claude is launched with, so pass-env forwards what your shell already has and cannot invent a value of its own. A variable that is unset at launch is skipped.

Forward one for a single session#

CLAUDE_SANDBOX_PASS_ENV does the same thing without touching the conf, which is handy for a one-off:

$ CLAUDE_SANDBOX_PASS_ENV=DOCKER_HOST claude

What you cannot forward#

The variables the sandbox sets itself — PATH, HOME, USER, IS_SANDBOX, GIT_CONFIG_GLOBAL, GIT_CONFIG_SYSTEM — and the loader/shell startup hooks (LD_*, BASH_ENV, ENV, SHELLOPTS, BASHOPTS, IFS) are ignored. The sandbox’s own value wins.

These aren’t arbitrary: forwarding PATH would undo the shadow’s PATH discipline that makes plain claude resolve to the sandbox, IS_SANDBOX would trip the recursion guard into skipping the jail, and LD_PRELOAD runs code of someone else’s choosing in every process the session spawns. See the configuration reference for the full list.

Why not just inherit the container’s environment?#

Because the environment is an input to the sandbox, not a detail of it. Inheriting wholesale would carry in whatever the surrounding container happens to hold — cloud credentials, tokens injected by a CI runner, a LD_PRELOAD set three layers up — and the sandbox would have no say in it. The allowlist keeps the set of things that cross the boundary small enough to read, which is the same principle as the bind-mount allowlist in the threat model.

The cost is this page: variables your project needs have to be named once.