You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
coworkBwrapMounts (added in #339) lets users add bind mounts to the bwrap
sandbox but only as a string — which mergeBwrapArgs translates to --bind <p> <p> (host path mounted at the same path inside the sandbox).
This means the host source and the sandbox destination are always identical,
which prevents a legitimate use case: replacing a default sandbox mount with a
persistent host directory mounted at the same in-sandbox path.
Motivating use case: persistent /tmp across Bash tool calls
Each Bash tool invocation in local-agent / Cowork mode spawns a fresh bwrap
process with --tmpfs /tmp (cowork-vm-service.js:1277) and --die-with-parent (cowork-vm-service.js:1365). The tmpfs is destroyed when
the command exits, so anything written to /tmp is lost before the next call.
This breaks workflows that legitimately stage state in /tmp between commands:
multi-step downloads (curl -o /tmp/... then process in a later call)
pip install --user writing wheels to /tmp/... and reusing them
tools that hardcode /tmp paths (get-pip.py, some installers)
There is no way to say "mount ~/.cache/claude-tmp from the host as /tmp inside the sandbox" — which is exactly what the user wants.
Proposal
Extend additionalROBinds and additionalBinds to also accept entries of the
form { \"src\": \"...\", \"dst\": \"...\" } in addition to the existing string form.
Example: persistent /tmp mapped to a host cache dir
Files written to /tmp inside the sandbox now survive across Bash calls,
without exposing the host /tmp.
Validation rules
src: same checks as the current string form
absolute, not in FORBIDDEN_MOUNT_PATHS (/, /proc, /dev, /sys)
if RW (additionalBinds): must be under \$HOME
dst: only the absolute + non-forbidden checks
the \$HOME constraint is intentionally skipped because the whole point
of {src, dst} is to map onto sandbox paths outside \$HOME (/tmp, /sandbox/..., etc.)
Malformed objects (missing src/dst, non-string values) are silently
filtered out with a BwrapConfig: rejected ... warning, matching the
existing string-validation behavior
Backwards compatibility
100%. The string form (\"/path\" → --bind /path /path) is preserved exactly
as-is. All 36 existing tests in tests/cowork-bwrap-config.bats pass without
modification.
Implementation status
I have a local patch that:
Adds object-form support in loadBwrapMountsConfig.filterMounts
(scripts/cowork-vm-service.js)
Updates mergeBwrapArgs to emit --bind src dst per type
(scripts/cowork-vm-service.js)
Updates _doctor_check_bwrap_mounts to render the object form as src -> dst in --doctor output (scripts/doctor.sh, both Python and
Node parser branches)
Adds 13 new BATS tests covering accept/reject paths, mixed string+object
configs, the persistent-/tmp recipe end-to-end, and the doctor
rendering — all passing (58/58 total)
Tested end-to-end on a locally rebuilt .deb: the resulting bwrap command
line contains the expected --bind ~/.cache/claude-tmp /tmp and the
default --tmpfs /tmp is correctly stripped via disabledDefaultBinds.
Summary
coworkBwrapMounts(added in #339) lets users add bind mounts to the bwrapsandbox but only as a string — which
mergeBwrapArgstranslates to--bind <p> <p>(host path mounted at the same path inside the sandbox).This means the host source and the sandbox destination are always identical,
which prevents a legitimate use case: replacing a default sandbox mount with a
persistent host directory mounted at the same in-sandbox path.
Motivating use case: persistent
/tmpacross Bash tool callsEach Bash tool invocation in local-agent / Cowork mode spawns a fresh
bwrapprocess with
--tmpfs /tmp(cowork-vm-service.js:1277) and--die-with-parent(cowork-vm-service.js:1365). The tmpfs is destroyed whenthe command exits, so anything written to
/tmpis lost before the next call.This breaks workflows that legitimately stage state in
/tmpbetween commands:curl -o /tmp/...then process in a later call)pip install --userwriting wheels to/tmp/...and reusing them/tmppaths (get-pip.py, some installers)Today there is no workaround:
additionalBinds: [\"/tmp\"]would expose the host's/tmpto the sandbox(regression of the isolation the bwrap rewrite in fix: bwrap backend mounts at guest paths and uses minimal sandbox root #309 set up), and even then
it requires
disabledDefaultBinds: [\"/tmp\"]to remove the default tmpfs.~/.cache/claude-tmpfrom the host as/tmpinside the sandbox" — which is exactly what the user wants.Proposal
Extend
additionalROBindsandadditionalBindsto also accept entries of theform
{ \"src\": \"...\", \"dst\": \"...\" }in addition to the existing string form.Example: persistent
/tmpmapped to a host cache dir{ \"preferences\": { \"coworkBwrapMounts\": { \"additionalBinds\": [ { \"src\": \"/home/user/.cache/claude-tmp\", \"dst\": \"/tmp\" } ], \"disabledDefaultBinds\": [\"/tmp\"] } } }Resulting bwrap args (excerpt):
Files written to
/tmpinside the sandbox now survive across Bash calls,without exposing the host
/tmp.Validation rules
src: same checks as the current string formFORBIDDEN_MOUNT_PATHS(/,/proc,/dev,/sys)additionalBinds): must be under\$HOMEdst: only the absolute + non-forbidden checks\$HOMEconstraint is intentionally skipped because the whole pointof
{src, dst}is to map onto sandbox paths outside\$HOME(/tmp,/sandbox/..., etc.)src/dst, non-string values) are silentlyfiltered out with a
BwrapConfig: rejected ...warning, matching theexisting string-validation behavior
Backwards compatibility
100%. The string form (
\"/path\"→--bind /path /path) is preserved exactlyas-is. All 36 existing tests in
tests/cowork-bwrap-config.batspass withoutmodification.
Implementation status
I have a local patch that:
loadBwrapMountsConfig.filterMounts(
scripts/cowork-vm-service.js)mergeBwrapArgsto emit--bind src dstper type(
scripts/cowork-vm-service.js)_doctor_check_bwrap_mountsto render the object form assrc -> dstin--doctoroutput (scripts/doctor.sh, both Python andNode parser branches)
configs, the persistent-
/tmprecipe end-to-end, and the doctorrendering — all passing (58/58 total)
Tested end-to-end on a locally rebuilt
.deb: the resulting bwrap commandline contains the expected
--bind ~/.cache/claude-tmp /tmpand thedefault
--tmpfs /tmpis correctly stripped viadisabledDefaultBinds.Happy to open a PR if the design is acceptable.
References
coworkBwrapMountsfeature--tmpfs /tmpwas introduced)scripts/cowork-vm-service.js—loadBwrapMountsConfig/mergeBwrapArgs(lines 739–855)
Written by Claude Opus 4.7 via Claude Code