Skip to content

Fix/macos per session tmpdir#90

Merged
tito merged 3 commits into
mainfrom
fix/macos-per-session-tmpdir
May 20, 2026
Merged

Fix/macos per session tmpdir#90
tito merged 3 commits into
mainfrom
fix/macos-per-session-tmpdir

Conversation

@nikitalokhmachev-ai
Copy link
Copy Markdown
Contributor

fix(macos): per-session TMPDIR and Claude Code bash working-dir access

Closes #11

Problem

On macOS, sandboxed processes were silently failing to write to /tmp because
the static path greywall injected (TMPDIR=/tmp/greywall) was never actually
created, so processes fell back to /private/tmp — which the Seatbelt policy
denied.

For the claude profile specifically this caused two distinct failures:

  1. Session directory blocked — Claude Code hardcodes
    /private/tmp/claude-{uid}/{encoded-cwd} as its bash session directory,
    ignoring $TMPDIR entirely. Any bash tool call resulted in:

    EPERM: operation not permitted, mkdir '/private/tmp/claude-501/...'
    
  2. Working-dir tracker blocked — Claude Code also creates
    /tmp/claude-{pid}-cwd to persist the working directory between commands.
    The PID suffix changes every run, so a UID-specific allow rule wasn't enough:

    zsh: operation not permitted: /tmp/claude-2522-cwd
    

Fix

Per-session TMPDIR (manager.go, macos.go, utils.go, dangerous.go)

Instead of injecting a static TMPDIR path that may not exist, Manager.Initialize()
now calls os.MkdirTemp("", "greywall-") on macOS. This creates a real directory
inside the system's $TMPDIR (/var/folders/.../T/greywall-XXXXXX), which is
already covered by the existing getTmpdirParent() write rule. The path is:

  • added explicitly to the Seatbelt allow file-write* rules
  • set as TMPDIR=... in the sandboxed process environment
  • cleaned up via os.RemoveAll in Manager.Cleanup()

The old static /tmp/greywall paths are removed from GetDefaultWritePaths().

Claude Code glob allowlist (profiles/agents/claude.go)

Added glob patterns to the claude/claude-code profile's darwin allowlist:

/tmp/claude-*       → covers /tmp/claude-{pid}-cwd (working-dir tracker)
/tmp/claude-*/**    → covers contents of /tmp/claude-{uid}/ (session dir)

expandMacOSTmpPaths in macos.go automatically mirrors these to
/private/tmp/claude-* and /private/tmp/claude-*/**, so both the canonical
path and the symlink target are covered.

Testing

  • All existing unit and integration tests pass (make test)
  • TestMacOS_SeatbeltAllowsTmpGreywall replaced with
    TestMacOS_SeatbeltAllowsPerSessionTmpDir (tests $TMPDIR write via the
    actual sandbox, not a static path)
  • Manually verified with greywall --profile claude -- claude --dangerously-skip-permissions:
    bash tool calls (echo hello) now complete cleanly with no permission errors

Replace the static /tmp/greywall TMPDIR with a unique per-session
directory created by os.MkdirTemp at sandbox init time. The dynamic
path is added to the Seatbelt write allowlist and injected as TMPDIR
into the sandboxed environment, then removed on Cleanup().

This fixes bash commands being blocked under Claude Code on macOS:
the shell was falling back to /private/tmp when TMPDIR pointed at the
non-existent /tmp/greywall, and the sandbox denied those writes.
The per-session dir lives under the system TMPDIR (/var/folders/...),
which is already covered by the getTmpdirParent() write rule.

Closes #11
Claude Code constructs /private/tmp/claude-{uid}/{cwd} for its bash
tool's working directory, ignoring $TMPDIR. Add that UID-scoped path
to the darwin read/write allowlists in the built-in claude profile so
bash commands (echo, ls, etc.) are no longer blocked by EPERM.
…rofile

Claude Code uses two distinct temp path patterns on macOS:
- /private/tmp/claude-{uid}/{encoded-cwd}  (session directory, UID-based)
- /tmp/claude-{pid}-cwd                    (working-dir tracker, PID-based)

The previous UID-specific paths didn't cover the PID-based tracker, causing
`zsh: operation not permitted: /tmp/claude-{pid}-cwd` on every bash command.

Replace with glob patterns /tmp/claude-* and /tmp/claude-*/** which cover
both. expandMacOSTmpPaths mirrors these to /private/tmp/ automatically.
@nikitalokhmachev-ai nikitalokhmachev-ai force-pushed the fix/macos-per-session-tmpdir branch from b981d52 to 3bf14a7 Compare April 24, 2026 18:41
@tito tito merged commit 38b1d84 into main May 20, 2026
4 checks passed
@tito tito deleted the fix/macos-per-session-tmpdir branch May 20, 2026 22:21
tito pushed a commit that referenced this pull request May 20, 2026
* fix(macos): per-session TMPDIR for sandboxed processes

Replace the static /tmp/greywall TMPDIR with a unique per-session
directory created by os.MkdirTemp at sandbox init time. The dynamic
path is added to the Seatbelt write allowlist and injected as TMPDIR
into the sandboxed environment, then removed on Cleanup().

This fixes bash commands being blocked under Claude Code on macOS:
the shell was falling back to /private/tmp when TMPDIR pointed at the
non-existent /tmp/greywall, and the sandbox denied those writes.
The per-session dir lives under the system TMPDIR (/var/folders/...),
which is already covered by the getTmpdirParent() write rule.

Closes #11

* fix(macos): allow Claude Code bash working dir in Seatbelt profile

Claude Code constructs /private/tmp/claude-{uid}/{cwd} for its bash
tool's working directory, ignoring $TMPDIR. Add that UID-scoped path
to the darwin read/write allowlists in the built-in claude profile so
bash commands (echo, ls, etc.) are no longer blocked by EPERM.

* fix(macos): use glob patterns for Claude Code tmp paths in Seatbelt
profile

Claude Code uses two distinct temp path patterns on macOS:
- /private/tmp/claude-{uid}/{encoded-cwd}  (session directory,
UID-based)
- /tmp/claude-{pid}-cwd                    (working-dir tracker,
PID-based)

The previous UID-specific paths didn't cover the PID-based tracker,
causing
`zsh: operation not permitted: /tmp/claude-{pid}-cwd` on every bash
command.

Replace with glob patterns /tmp/claude-* and /tmp/claude-*/** which
cover
both. expandMacOSTmpPaths mirrors these to /private/tmp/ automatically.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Have a default local TMPDIR

2 participants