Summary
The boundless filesystem sandbox now keeps the Git exec-surface paths inside the working tree -- .git/hooks and .git/config -- read-only, even though the rest of cwd stays writable. Those paths hold code Git later runs as the operator, outside the sandbox: hook scripts, and config directives that execute a command (core.hooksPath, core.fsmonitor, core.sshCommand, core.pager, alias.* = "!cmd"). Confining them read-only closes the "plant a payload now, fire it on the operator's next git in the repo" path. Git's own commands only ever read them, so the carve-out costs nothing in normal work.
It is enforced on Linux (bubblewrap) and for boundless's in-process file tools (write/edit/copy) on every platform except Windows. On Windows it is a no-op: .git stays fully writable. This tracks closing that gap when mxc gains the primitive it needs.
Why Windows cannot express it today
The carve-out needs "readable but not writable" for a subpath of a writable parent. No mxc Windows backend can express that:
- DACL / AppContainer tiers: only additive allow-ACLs plus a single full-access deny mask. A full-access deny on
.git/config would also block Git's reads of config and break the repo. There is no write-only deny ACE in the model as wired.
- base-container tier: grants writable subtrees; no mechanism to subtract a read-only hole from a granted-writable parent.
Verified by reading the Windows backends in microsoft/mxc (filesystem_dacl.rs, filesystem_bfs.rs, the appcontainer runner, the base-container share path).
On Linux it falls out for free: the bubblewrap builder emits readonly binds after readwrite binds and "later mounts win, overriding any rw parent" (src/backends/bubblewrap/common/src/bwrap_command.rs), so a .git/hooks ro-bind layered after the cwd rw-bind wins. On macOS (seatbelt) the kernel-layer carve-out is an inert allow-read -- seatbelt readonly cannot subtract an existing write grant -- but the in-process file-tool guard still covers write/edit/copy there.
Coverage matrix (current)
- Linux: bash (bubblewrap ro-bind) + file tools (in-process) -- full
- macOS: file tools (in-process) only; bash unprotected (seatbelt limitation)
- Windows: neither --
.git fully writable
Where it lives
computeGitProtectedPaths in packages/less/src/tools/sandbox-policy.ts is the single source of the protected-path set and the Windows gate (platform === "win32" returns []). It feeds both buildPolicy (readonlyPaths, the mxc kernel guard) and checkWritePath (the in-process file-tool guard). Documented in README (boundless section) and CONTRIBUTING (Common Gotchas).
Resolution trigger
Revisit when mxc exposes a write-only-deny (or readable-but-not-writable subpath) primitive on a Windows backend. Then: drop the win32 short-circuit in computeGitProtectedPaths (or narrow it to whichever tiers still lack the primitive) and extend integration coverage to the Windows lane.
Threat-model note
This is defense-in-depth on one narrow escalation path, not a wall. An agent that can write arbitrary code into the working tree (source files, dist, node_modules) already achieves "runs as the operator later" the next time the operator builds or runs the project. The .git carve-out closes the most surprising version of that -- a hook firing on a plain git status -- not the general class. Calibrate accordingly.
Summary
The boundless filesystem sandbox now keeps the Git exec-surface paths inside the working tree --
.git/hooksand.git/config-- read-only, even though the rest of cwd stays writable. Those paths hold code Git later runs as the operator, outside the sandbox: hook scripts, and config directives that execute a command (core.hooksPath,core.fsmonitor,core.sshCommand,core.pager,alias.* = "!cmd"). Confining them read-only closes the "plant a payload now, fire it on the operator's nextgitin the repo" path. Git's own commands only ever read them, so the carve-out costs nothing in normal work.It is enforced on Linux (bubblewrap) and for boundless's in-process file tools (write/edit/copy) on every platform except Windows. On Windows it is a no-op:
.gitstays fully writable. This tracks closing that gap when mxc gains the primitive it needs.Why Windows cannot express it today
The carve-out needs "readable but not writable" for a subpath of a writable parent. No mxc Windows backend can express that:
.git/configwould also block Git's reads of config and break the repo. There is no write-only deny ACE in the model as wired.Verified by reading the Windows backends in microsoft/mxc (filesystem_dacl.rs, filesystem_bfs.rs, the appcontainer runner, the base-container share path).
On Linux it falls out for free: the bubblewrap builder emits readonly binds after readwrite binds and "later mounts win, overriding any rw parent" (src/backends/bubblewrap/common/src/bwrap_command.rs), so a
.git/hooksro-bind layered after the cwd rw-bind wins. On macOS (seatbelt) the kernel-layer carve-out is an inert allow-read -- seatbelt readonly cannot subtract an existing write grant -- but the in-process file-tool guard still covers write/edit/copy there.Coverage matrix (current)
.gitfully writableWhere it lives
computeGitProtectedPathsinpackages/less/src/tools/sandbox-policy.tsis the single source of the protected-path set and the Windows gate (platform === "win32"returns[]). It feeds bothbuildPolicy(readonlyPaths, the mxc kernel guard) andcheckWritePath(the in-process file-tool guard). Documented in README (boundless section) and CONTRIBUTING (Common Gotchas).Resolution trigger
Revisit when mxc exposes a write-only-deny (or readable-but-not-writable subpath) primitive on a Windows backend. Then: drop the
win32short-circuit incomputeGitProtectedPaths(or narrow it to whichever tiers still lack the primitive) and extend integration coverage to the Windows lane.Threat-model note
This is defense-in-depth on one narrow escalation path, not a wall. An agent that can write arbitrary code into the working tree (source files,
dist,node_modules) already achieves "runs as the operator later" the next time the operator builds or runs the project. The.gitcarve-out closes the most surprising version of that -- a hook firing on a plaingit status-- not the general class. Calibrate accordingly.