[BUG] ENV_SCRUB=1 sandbox leaves persistent 0-byte placeholder files in cwd AND $HOME (.netrc, .bashrc, .gitconfig) — still in v2.1.211
Description
With CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1, every Claude Code run leaves behind persistent 0-byte placeholder files — 18 entries in the working directory and, additionally, credential/shell-config file names in $HOME (.netrc, .bashrc, .bash_profile, .bash_aliases, .profile, .zshrc, .gitconfig). They are not cleaned up on exit.
This extends #46165 (closed as not planned) and #17258 (closed as duplicate) with:
- Still reproduces on v2.1.211 (latest as of 2026-07-16), CLI headless mode, no IDE.
- A deterministic A/B: the env var alone flips the behavior (matrix below), confirming the community diagnosis in #46165.
- A previously unreported, larger
$HOMEfile set including.netrcand shell rc files — a 0-byte.netrcappearing in root's home is alarming in security scans and estate audits. - The files persist after the process exits (an earlier comment on #46165 observed create+delete per Bash invocation; on our hosts they leak permanently).
Environment
- Claude Code v2.1.211, native binary, CLI only (
claude -p), no IDE - Ubuntu 22.04, kernel 5.15, running as root on a VPS (
$HOME=/root) CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1set in~/.claude/settings.jsonenvblock (deliberately — we rely on subprocess env scrubbing for credential hygiene)
Reproduction (scratch HOME + scratch cwd, fully isolated)
mkdir -p /tmp/probe/home /tmp/probe/cwd && cd /tmp/probe/cwd
HOME=/tmp/probe/home CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 claude -p "say ok" --model haiku
ls -A /tmp/probe/cwd /tmp/probe/home
A/B matrix (v2.1.211, identical scratch env, prompt completes successfully in all cases)
| CLAUDE_CODE_SUBPROCESS_ENV_SCRUB | files created in cwd | files created in $HOME |
|---|---|---|
| 1 | 18 (list below), persist after exit | 12+ incl. .netrc, .bashrc, .gitconfig |
| 0 | none | none |
| unset | none | none |
cwd set (all 0 bytes): .env, .env.local, .env.development, .env.development.local, .env.test, .env.test.local, .env.production, .env.production.local, .gitmodules, .npmrc, .yarnrc, .yarnrc.yml, bunfig.toml, package.json, package-lock.json, pnpm-lock.yaml, yarn.lock, plus empty node_modules/.bin/.
additional $HOME set (all 0 bytes): .netrc, .bashrc, .bash_profile, .bash_aliases, .profile, .zshrc, .gitconfig, .bunfig.toml, .npmrc, .yarnrc, .yarnrc.yml.
The set is recognizably the sandbox's sensitive-file deny list — the placeholders appear to be created as mask/mount targets when the real files don't exist, and are then left behind.
Real-world impact we hit
- The 0-byte scaffold set was accidentally committed into a project repo (an April boundary commit picked them up) and confused dead-code audits for months.
- A batch landed in a data directory that happened to be a session cwd, and was repeatedly flagged by estate/hygiene sweeps.
- Deleting them is futile — the next session recreates them within minutes on a busy host.
Suggested fix
Create the mask placeholders under a private location (e.g. $CLAUDE_CONFIG_DIR/sandbox-masks/ or the session tmpdir) and bind-mount from there, instead of materializing them in the user's cwd/$HOME; or track which placeholders the sandbox itself created and unlink them on session exit. Either way, ENV_SCRUB=1 (a security-positive setting) should not have filesystem-pollution side effects that push users toward turning it off.
3 Comments
Reproduced on 2.1.233 (Linux): with
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1and bubblewrap installed, a singleclaude -prun left the 0-byte files you listed in both cwd and$HOME, and they persist after exit; without the variable, no files are created.This is intended behavior of the sandbox that variable turns on, though we agree it's confusing and looks alarming. The sandbox write-protects sensitive paths (
.netrc, shell rc files, git/npm config, lockfiles,.env*) for every sandboxed command. When such a path doesn't exist, it's pre-created as an empty regular file so protected tools that read those paths (git, npm, bash) keep working — otherwise they'd hit an unreadable device-file stand-in. The files are empty placeholders, never populated with data, and the working-directory ones are added to.git/info/excludeso they won't be committed.Two things we're considering to make this less surprising: documenting the placeholder files in the env var docs, and cleaning up placeholders Claude Code itself created when the session exits. Leaving this open to track that.
🤖 Generated with Claude Code
Same issue in 2.1.233 inside a rootless Docker container gw0/docker-claude-code with sandboxing enabled. Everything works fine without sandboxing (
sandbox.enable=falseandCLAUDE_CODE_SUBPROCESS_ENV_SCRUB=0).It seems the empy files appear only when Claude Code starts. Workaround is to remove auto-generated empty files and dirs in project dir 2s after start like in https://github.com/gw0/docker-claude-code/blob/main/scripts/entrypoint.sh#L88. Sandboxed commands still see
/dev/nulloverlays, but the host dir does not include empty files and dirs anymore.Confirming on claude-code v2.1.246 in a Linux devcontainer where
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1is set (via a managed-settings.json floor, as the recommended subprocess-scrub defense).CLAUDE_CODE_SUBPROCESS_ENV_SCRUBis the definitive trigger — controlled test:env -u CLAUDE_CODE_SUBPROCESS_ENV_SCRUB claude→ 0 placeholder files.node_modules/.binon every session start (and, per @NexusOne23'sinotifywait, recreated on every Bash tool call).sandbox.enabled, removing the managed floor entirely, orsandbox.excludedCommands. Removing the env var is the only thing that stops it at the source.Extra data point: in our setup the scrub also appears to strip legitimate app secrets (e.g.
AZURE_CLIENT_SECRET) from intended subprocesses — so the same feature behind this bug can silently break tooling that relies on inherited env. A documented opt-out, or a fix that protects\$HOMEdotfiles without scaffolding the project cwd, would resolve both. (Related, now locked: #46165.)