bwrap launcher uses --bind for optional hardware paths (/opt/cuda*); fails on systems without CUDA installed

Resolved 💬 3 comments Opened Apr 19, 2026 by greogory Closed May 28, 2026

Summary

On Linux hosts without an NVIDIA/CUDA installation — AMD-GPU systems, Intel iGPU systems, ARM hosts, CPU-only containers, and most CI runners — the bubblewrap (bwrap) sandbox launcher fails during namespace setup because it uses --bind (hard requirement) rather than --ro-bind-try / --bind-try (soft, no-op if source missing) for hardware-dependent paths such as /opt/cuda and its subdirectories. The Bash tool becomes unusable in every session on affected hosts, and the failure happens before any user command runs, so dangerouslyDisableSandbox: true cannot rescue it.

Environment

  • Host: CachyOS (Arch-based), kernel 6.19.12, systemd-boot, BTRFS root
  • GPU: AMD (no NVIDIA hardware in the system, no cuda package installed)
  • Claude Code: 2.1.114
  • bubblewrap: 0.x (system default)
  • /opt/cuda does not exist on disk

Observed behaviour

Running any Bash tool call (including trivial echo ok) returns:

bwrap: Can't mkdir /opt/cuda: Permission denied

After sudo mkdir -p /opt/cuda as a workaround, the next failure is:

bwrap: Can't create file at /opt/cuda/bin: Permission denied

…and so on through the expected CUDA subdirectory layout (bin, lib, lib64, include, etc.). Each missing subpath produces a separate bwrap failure, because the launcher appears to bind each of these as a top-level --bind rather than relying on the parent bind to cover them, and none of them use the -try variants.

Related symlink failure (separate root cause, same pattern)

On systems where ~/.claude is a symlink to a location not otherwise in the launcher's bind list (e.g., ~/.claude -> /mnt/projects/.claude), the namespace setup fails earlier with:

bwrap: Can't create file at /home/USER/.claude: No such file or directory

This is because --bind on a symlink dereferences the symlink at mount-namespace setup time, and the target's parent filesystem isn't bound into the namespace. The same --*-try gap is present, and additionally the launcher could realpath() the source before binding, or bind the resolved target path directly.

Expected behaviour

The launcher should treat hardware-dependent or user-configurable paths as optional binds — present iff the source exists on the host — and silently skip them when the source is missing. bubblewrap provides --ro-bind-try and --bind-try specifically for this case; they are no-ops when the source is absent and bind normally otherwise.

Suggested fix

For any host path whose presence depends on installed hardware, optional packages, distro-specific layouts, or user-specific filesystem topology — including at minimum /opt/cuda and all its expected subdirectories — replace --bind / --ro-bind with --bind-try / --ro-bind-try.

Pseudocode:

- "--bind", "/opt/cuda", "/opt/cuda",
- "--bind", "/opt/cuda/bin", "/opt/cuda/bin",
- "--bind", "/opt/cuda/lib64", "/opt/cuda/lib64",
+ "--ro-bind-try", "/opt/cuda", "/opt/cuda",
+ "--ro-bind-try", "/opt/cuda/bin", "/opt/cuda/bin",
+ "--ro-bind-try", "/opt/cuda/lib64", "/opt/cuda/lib64",

The --*-try variants are the standard bubblewrap idiom for optional binds and have been supported since bubblewrap 0.1. No behavioural regression on CUDA-equipped hosts.

Additionally, for bind sources that may be symlinks pointing to filesystems not otherwise bound (e.g., user-relocated dotfile directories), resolving the source via realpath() before passing to --bind would avoid the symlink-dereferencing failure described above.

Workarounds while a fix ships

  1. Pre-create the expected CUDA skeleton even on CUDA-less hosts:

``
sudo mkdir -p /opt/cuda/{bin,lib,lib64,include,share,targets,extras,nvvm}
``

  1. If ~/.claude is a symlink, replace it with a real directory plus a systemd .mount bind unit (or fstab bind) so the kernel resolves the path to a directory before bwrap lstats it.

Both are kludges — the proper fix is on the launcher side, and is a small change with no downside for CUDA-equipped systems.

Impact

Any user on AMD, Intel-iGPU, ARM, CPU-only hardware, or inside a container without CUDA installed will hit this on a fresh Claude Code install. In practice this is a large fraction of developer machines outside ML-focused setups, so the portability impact is meaningful.

Why this is worth a quick turnaround

The fix is a handful of lines, requires no feature work, and removes a first-run failure mode for a significant slice of the user base. Swapping --bind--bind-try for optional paths is the standard bubblewrap portability idiom and has no regressions on hardware where the paths do exist.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗