[BUG] macOS: enabling sandbox in user settings.json breaks Bash entirely (session-env mkdir EPERM); identical config via --settings works

Status Open
Reported on v2.1.218
Maintainer reply None cached
Activity 0 comments · opened Jul 23, 2026

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_DIR unset

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)

  1. macOS, default config dir ~/.claude.
  2. Add to ~/.claude/settings.json:

``json
"sandbox": { "enabled": true, "autoAllowBashIfSandboxed": true }
``

  1. 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-env mkdir succeeds)
  • ~/.claude/.envoperation 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 (sandbox in settings.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 --settings workaround exists but is awkward for a setting meant to be always-on (requires wrapping every claude invocation).

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.

View original on GitHub ↗