Skip to content

Feature: support {src, dst} mount form in coworkBwrapMounts to allow distinct host/sandbox paths (e.g. persistent /tmp) #530

@cbonnissent

Description

@cbonnissent

Summary

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)

Today there is no workaround:

  • additionalBinds: [\"/tmp\"] would expose the host's /tmp to 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.
  • 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

{
  \"preferences\": {
    \"coworkBwrapMounts\": {
      \"additionalBinds\": [
        { \"src\": \"/home/user/.cache/claude-tmp\", \"dst\": \"/tmp\" }
      ],
      \"disabledDefaultBinds\": [\"/tmp\"]
    }
  }
}

Resulting bwrap args (excerpt):

... --proc /proc --tmpfs /run --bind /home/user/.cache/claude-tmp /tmp ...

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.

Happy to open a PR if the design is acceptable.

References


Written by Claude Opus 4.7 via Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    coworkRelated to Cowork modeenhancementNew feature or requestpriority: mediumShould be addressed when possibletriage: investigatedIssue has been triaged and investigated

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions