@@ -143,30 +143,43 @@ The JVM uses the macOS per-user tmp dir (`/private/var/folders/.../T/`)
143143for sbt's BootServerSocket regardless of shell ` $TMPDIR ` , and for
144144dependency-cache writes during Ivy / Coursier resolution. Claude
145145Bash default sandbox denies writes there. ` scripts/install.sh ` pre-
146- allows the minimum set automatically:
146+ allows the dependency-cache paths automatically:
147147
148148``` json
149149"sandbox" : { "filesystem" : { "allowWrite" : [
150+ " /private/var/folders/**/T/.sbt*/**" ,
150151 " /private/var/folders/**/.sbt/**" ,
151152 " ~/.sbt/**" ,
152153 " ~/.ivy2/**" ,
153154 " ~/.coursier/**"
154155]}}
155156```
156157
157- With ` install.sh ` run, sbt-direct works under Claude Bash without any
158- per-call bypass flag. Verified against a real multi-module Play 3 /
159- Scala 3 project: ` sbt-direct call version ` reads the project's
160- ` build.sbt ` correctly; `sbt-direct call task
161- {"task":"scalafmtCheckAll"}` runs the sbt-scalafmt plugin end-to-end
162- and surfaces per-file formatting diffs.
163-
164- Users who don't run ` install.sh ` (stand-alone deployment, bespoke
165- sandbox config) can either merge the same entries into their
166- ` ~/.claude/settings.json ` manually, or call sbt-direct with
167- ` dangerouslyDisableSandbox: true ` . The coordinator, bash wrapper, and
168- adapter are all sandbox-neutral — the socket write is the only
169- block, and it's strictly in sbt's own boot code.
158+ ` allowWrite ` is ** not** sufficient for either mode on macOS. Both
159+ ` bsp ` and ` oneshot ` load a native dylib at runtime:
160+
161+ - ` bsp ` loads ` libsbtipcsocket*.dylib ` (ipcsocket for persistent server).
162+ - ` oneshot ` loads ` libswoval-files*.dylib ` (swoval file-watcher used by
163+ sbt's ` FileTreeRepository ` during project load).
164+
165+ Both extractions call ` Files.createTempFile ` under the per-user tmpdir
166+ (` /var/folders/.../T/.sbt<digits>/swoval/... ` or ` .../sbtipcsocket/... ` )
167+ and then ` System.load ` . macOS sandbox-exec governs ` dlopen ` via
168+ ` process-exec* ` policy, orthogonal to ` file-write* ` . Adding
169+ ` allowWrite ` globs for the tmp path — including the realpath twin
170+ (` /var/folders/**/T/.sbt*/** ` alongside the ` /private/var/folders/... `
171+ prefix) — does NOT rescue the load; denial happens at dylib-exec time,
172+ not at file-create time. Verified 2026-04-22 — both bsp (prior session) and
173+ oneshot (this session with twin-prefix glob active) RED at same error
174+ signature class.
175+
176+ Call sbt-direct with ` dangerouslyDisableSandbox: true ` for either
177+ mode. There is no sandbox-compatible sbt path on macOS.
178+
179+ Verified GREEN 2026-04-22 with Claude Bash ` /sandbox disabled ` :
180+ ` SBT_DIRECT_MODE=oneshot sbt-direct call version '{}' <ws> ` against a
181+ real Play/Scala workspace boots sbt cleanly, resolves project +
182+ runner versions, no dylib "Operation not permitted" in stderr.
170183
171184## State directory
172185
0 commit comments