Add sandbox-git-write guard hook#10
Conversation
A PreToolUse/Bash hook (bin/claude-sandbox-guard) that denies sandboxed git-write and srb commands up front, telling Claude to retry with dangerouslyDisableSandbox=true instead of eating a cryptic EPERM and retrying. Both classes reliably fail under the command sandbox and are safe to run unsandboxed: - git writes hit .git/worktrees/.../index.lock and the hardcoded blocked paths (.claude/, .vscode/, .gitmodules) inside pt worktrees - sorbet (srb) wants to write its mdb / rubocop cache Read-only git (log/show/diff/status, worktree/stash/submodule list) is left sandboxed, so the common path is untouched. claudeconfig.sh now concatenates hooks.<event> arrays across base + active role. The previous deep merge replaced same-event arrays, which would silently drop a base-level PreToolUse hook whenever the active role also defined PreToolUse (work.jsonc does). Behaviour is locked down by a bats suite (test/): hand-written edge cases (env prefixes, git -C, compound chains, read-form list commands, already-unsandboxed calls) plus a data-driven sweep over 90 real commands pulled from the session corpus. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Capture the verified rationale in the hook header: git writes hit a hardcoded path deny (not overridable by allowWrite) and srb fails on LMDB's writable mmap (a syscall deny, not a path the cache dir is already writable inside cwd). Both ruled out allowlisting by testing, so unsandboxing is the only fix. Also makes the srb deny message accurate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sandbox-probe disproved the writable-mmap theory (mmap + flock both succeed sandboxed). srb still needs unsandboxing, but the mechanism is a non-path syscall deny, not mmap; suspect LMDB's POSIX named semaphores. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
srb's LMDB cache open fails sandboxed because LMDB on macOS uses SysV semaphores (MDB_USE_SYSV_SEM): the sandbox allows semget but denies semctl/semop with EPERM. Confirmed via pickletown projects/sandbox-probe (lmdb_replay.py replaying mdb_env_open). Replaces the earlier "named semaphores (sem_open)" suspicion in the comment and deny message; sem_open is not even linked into the sorbet binary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ps and top are setuid-root; the sandbox denies exec of setuid/setgid binaries, so they EPERM sandboxed (bare `ps` is shimmed through rtk, which execs /bin/ps and hits the same deny). Both are read-only and safe unsandboxed, so they fit the existing deny-and-retry pattern. Matches bare, path-qualified, and rtk-wrapped forms; anchored so psql/topgrade/ pstree don't false-trigger. Only ps/top are listed (the tools agents actually run); other setuid binaries (su, crontab) are left to fail loudly rather than auto-unsandboxed. Mechanism confirmed via pickletown projects/sandbox-probe: the sysctl ps reads (KERN_PROC) is allowed sandboxed; the friction is purely setuid exec, which allowWrite can't touch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Closing without merging. This guard proactively denied sandboxed commands that reliably fail (git-write, srb, ps/top) and told Claude to retry unsandboxed. Auto-mode now handles that sandboxed -> unsandboxed decision in the harness itself, so the proactive deny is redundant in practice. The real value here was the investigation, and that's preserved: the sandbox deny-surface (setuid/setgid exec deny, LMDB SysV-semaphore deny, the path/LaunchServices/sysctl denies) is documented in pickletown projects/sandbox-probe plus a reference script (lmdb_replay.py) and memory notes. Its complement, the sandbox-first plugin, is being removed for the same reason (pickled-claude-plugins#90). Leaving the branch up in case the non-auto-mode edges (subagents, cron, teammates) make it worth revisiting. |
Summary
bin/claude-sandbox-guard, a PreToolUse/Bash hook that denies sandboxed git-write andsrbcommands and tells Claude to retry withdangerouslyDisableSandbox=true. Both reliably fail under the sandbox (git writes hit.git/worktrees/.../index.lockand the blocked.claude/.vscode/.gitmodulespaths in pt worktrees; sorbet wants its mdb/rubocop cache) and are safe to run unsandboxed. Read-only git stays sandboxed, so the common path is untouched.claudeconfig.shto concatenatehooks.<event>arrays across base + active role instead of deep-merging them. The old merge replaced same-event arrays, so a base-levelPreToolUsehook got silently dropped whenever the active role also definedPreToolUse(work.jsonc does).srbas the clear "always retry unsandboxed" patterns./tmpwrites were deliberately left out, since the fix there is$TMPDIR, not loosening the sandbox.Test plan
npm test(lint + bats). The bats suite covers hand-written edge cases (env prefixes,git -C, compound chains, read-formlistcommands, already-unsandboxed calls, non-Bash tools) plus a sweep over 90 real commands pulled from the corpus.Activating it
Dormant until
claudeconfig.shregenerates~/.claude/settings.json. Heads up first: the live settings hasbowerbird/rtk/plannotatorhooks that aren't tracked in any role, so a regen drops them. Pre-existing drift, worth sorting out separately.