[BUG] macOS: enabling sandbox in user settings.json breaks Bash entirely (session-env mkdir EPERM); identical config via --settings works
Summary
On macOS, enabling the Bash sandbox via user-scope ~/.claude/settings.json makes the Bash tool fail on every command in new sessions. The tool cannot create its per-session working directory:
EPERM: operation not permitted, mkdir '<config-dir>/session-env/<uuid>'
The identical sandbox config loaded via --settings <file> (CLI scope) works correctly — Bash runs, and credential/filesystem denies are enforced as expected. So the sandbox itself works; the defect is specific to enabling it from the auto-loaded user settings.json.
Net effect: the documented "add sandbox to your settings.json" setup path is unusable on macOS — it takes the Bash tool fully offline instead of sandboxing it.
Environment
- Claude Code 2.1.218
- macOS 15.7.7, Apple Silicon (arm64)
- Native install (
~/.local/share/claude/versions/2.1.218) - Default config dir
~/.claude,CLAUDE_CONFIG_DIRunset
Root cause (as far as I can see)
The Bash tool creates its session working dir at join(configDir, "session-env", <uuid>) — hardcoded under the config directory. When the sandbox is enabled, its filesystem layer write-protects the config directory (this is the intended settings-self-protection behavior). The two collide: the mkdir under <config-dir>/session-env is denied by the very sandbox the settings file just turned on, so no Bash command can initialize.
This does not happen with --settings — evidently the sandbox's write-protection is scoped differently, or applied at a different point in startup, when the config comes from the CLI flag rather than the user settings file.
Repro (minimal)
- macOS, default config dir
~/.claude. - Add to
~/.claude/settings.json:
``json``
"sandbox": { "enabled": true, "autoAllowBashIfSandboxed": true }
- Start a new session, run any Bash command, e.g.
echo hi.
Expected: command runs sandboxed (auto-allowed).
Actual: every Bash command fails before executing:
EPERM: operation not permitted, mkdir '/Users/<user>/.claude/session-env/<uuid>'
Reproduced 5×: minimal config through full config (credentials/network/excludedCommands), and from two different working directories. 100% failure.
Control that isolates it to user-settings scope
Same config, but placed in an external file and passed via CLI:
# ~/pai-sandbox.json contains the identical {"sandbox": {...}} block
claude -p 'echo probe-ok; wc -c < ~/.claude/.env; wc -c < ~/.zshrc' \
--settings ~/pai-sandbox.json
Result — works correctly:
probe-ok✅ (Bash initializes;session-envmkdir succeeds)~/.claude/.env→operation not permitted🔒 (credential deny enforced at OS level)~/.zshrc→ byte count ✅ (ordinary read unaffected)
Location is not the trigger — scope is. Copying that same file inside the config dir (~/.claude/pai-sandbox-INSIDE.json) and loading it via --settings also works (probe-ok). Only the auto-loaded user settings.json path triggers the failure.
Impact
- The primary documented setup (
sandboxinsettings.json) is a hard break on macOS: it disables Bash entirely rather than sandboxing it, with an error that reads like a system fault rather than a config problem. - The
--settingsworkaround exists but is awkward for a setting meant to be always-on (requires wrapping everyclaudeinvocation).
Suggested fix directions
- Exempt
<config-dir>/session-env(and any other runtime dirs the tool must create —shell-snapshots, etc.) from the sandbox's config-dir write-protection, or create the session-env dir before the sandbox profile is applied. - Make the two code paths (user-settings vs
--settings) apply the same write-protection scope, so behavior is consistent regardless of where the sandbox config comes from.