- Gate /dev/kvm fix hints behind _kvm_active check (was leaking
unconditionally)
- Check COWORK_VM_BACKEND override in --doctor backend summary
to match daemon's detectBackend() behavior
- Log hint when KVM deps are present but bwrap wins auto-detect,
so upgrading users know about COWORK_VM_BACKEND=kvm
Co-Authored-By: Claude <claude@anthropic.com>
Replace repeated ${_kvm_flag,,} == kvm tests with a single
_kvm_active boolean. Use glob pattern [Kk][Vv][Mm] for
case-insensitive matching.
Co-Authored-By: Claude <claude@anthropic.com>
Swap auto-detection order from KVM → bwrap → host to
bwrap → KVM → host. KVM remains available via
COWORK_VM_BACKEND=kvm.
- detectBackend(): check bwrap before KVM
- --doctor: bwrap checked first; KVM deps shown as info
(not warnings) unless COWORK_VM_BACKEND=kvm is set
- Fix header comment inaccuracy about rootfs.qcow2 check
- Update README and handover docs to reflect new default
Fixes#326
Co-Authored-By: Claude <claude@anthropic.com>
Patch 9 previously widened the win32 platform gate to include Linux,
which also activated the _.configure() call meant for Windows HCS
setup. This caused "Request timed out: configure" on Linux because
the RPC client expected an id-echoing response that the daemon didn't
provide (fixed separately in #313, but the call is unnecessary either
way).
Replace the widened gate with a separate Linux-only block injected
after the win32 block. This copies smol-bin VHDX to the bundle dir
(needed for KVM guest SDK access) without calling _.configure().
Fixes#315
Co-Authored-By: Claude <claude@anthropic.com>
On NixOS, Electron is a separate Nix store path from the app, so
process.resourcesPath points to Electron's resources dir rather than
the app's. Claude's app code uses app.isPackaged to choose between
resourcesPath-based and development-style fallback paths. Setting
ELECTRON_FORCE_IS_PACKAGED=true (introduced by PR #305) forces the
resourcesPath code paths, which resolve to the wrong location on
NixOS, causing a silent startup hang.
Skip the env var for 'nix' package type so isPackaged remains false,
restoring the working behavior from before PR #305.
Fixes#311
Co-Authored-By: Claude <claude@anthropic.com>
Resolve conflict: take main's updated URLs (v1.1.7714) while
keeping the new claude_exe_sha256 variable from this branch.
Co-Authored-By: Claude <claude@anthropic.com>
The socket server did not echo back the `id` field from client
requests. The Claude Desktop client has two RPC modes: one-shot
connections (no id needed) and persistent multiplexed connections
(responses matched by id). When the persistent mode is active,
responses without an id are silently dropped as "orphaned", causing
all RPC calls (configure, startVM, isGuestConnected, etc.) to
timeout after 30 seconds.
Fixes#312
Co-Authored-By: Claude <claude@anthropic.com>
The BwrapBackend had two issues with its sandbox setup:
1. **Security: workDir fell back to $HOME making entire home writable.**
The spawn `cwd` arrives as `/sessions/<name>` (a session root without
the `/mnt/<mount>` suffix). `resolveWorkDir` could not translate this
pattern via `translateGuestPath` and fell back to `os.homedir()`.
This caused `--bind $HOME $HOME` to override the preceding
`--ro-bind $HOME $HOME`, making the entire home directory writable
inside the sandbox — defeating the purpose of read-only home
protection for ~/.ssh, ~/.gnupg, etc.
2. **Broken paths: claude-code-vm translates all paths to /sessions/.**
The claude-code-vm binary internally maps all paths (both input and
output) to `/sessions/<name>/mnt/<mount>/` guest paths. The old code
translated guest paths to host paths via `cleanSpawnArgs` and
`buildSpawnEnv`, but the binary translated them right back. Since
`/sessions/` did not exist inside the bwrap namespace, all file
operations (ls, read, write) on the working directory failed.
The fix replaces the `--ro-bind / /` full-root approach with a minimal
tmpfs-based sandbox that only mounts what is needed:
- `--tmpfs /` as an empty root
- `/usr`, `/etc`, `/dev`, `/proc` bound read-only for system tools
- Fedora-style symlinks for `/bin`, `/lib`, `/lib64`, `/sbin`
- SDK binary directory bound read-only
- `/sessions/<name>/mnt/` created via `--dir`, with host directories
bind-mounted at their guest paths (matching the KVM backend layout)
- `--chdir` sets the cwd inside the sandbox to the guest working dir
This eliminates the guest-to-host path translation for bwrap entirely
(args and env are passed through as-is) since the guest paths now
exist natively inside the sandbox.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add integrity verification for downloaded artifacts in build.sh:
- New verify_sha256() helper function for reusable hash checking
- Claude Desktop EXE: verified after download (skipped for --exe)
- Node.js tarball: verified against official SHASUMS256.txt
- CI workflow updated to compute and store hex SHA-256 hashes
alongside existing SRI hashes for Nix
Hash placeholders are empty until the next CI version update
populates them. The verify function gracefully warns and skips
when no hash is available, so builds are not blocked.
appimagetool is excluded as it uses a rolling "continuous"
release where checksums change frequently.
mapped-xattr stores guest uid/gid in extended attributes, which can
pollute host files and fail on filesystems without xattr support.
security_model=none is like passthrough but silently ignores chown
failures — specifically designed for unprivileged QEMU operation.
Co-Authored-By: Claude <claude@anthropic.com>
VIRTIOFS_GUEST_MOUNT and VIRTIO9P_MOUNT_TAG were named after specific
transport types but are now used for both virtiofs and 9p shares.
Rename to HOME_SHARE_GUEST_MOUNT and HOME_SHARE_MOUNT_TAG, and use
the mount tag constant in both device argument lines instead of
hardcoding 'claudeshared' in the virtiofs path.
Co-Authored-By: Claude <claude@anthropic.com>
The Rust virtiofsd (v1.10.0+) crashes when spawned as an unprivileged
user due to setgroups/capability requirements that fail outside
interactive shells. This affects Debian 13, Ubuntu 24.04, and other
modern distros.
Add virtio-9p as an automatic fallback:
- Try virtiofsd first (best performance)
- If virtiofsd fails to create its socket, fall back to virtio-9p
- virtio-9p is built into QEMU — no external daemon, no privileges
- Uses mapped-xattr security model for proper file ownership
- Same mount tag (claudeshared) so guest mount path is unchanged
Track share type with homeShareType ('virtiofs', '9p', or null):
- Only enable shared memory backend for virtiofs (not needed for 9p)
- Both types use the same guest mount path for path translation
- Properly reset on stopVM
Refs #288, #306
Co-Authored-By: Claude <claude@anthropic.com>
The Nix build used makeWrapper which bypassed launcher-common.sh
entirely. NixOS users had no --doctor support, no CLAUDE_USE_WAYLAND
env var handling, no display detection, and no startup logging.
Replace makeWrapper with a full bash launcher script that sources
launcher-common.sh, matching the deb/RPM/AppImage launchers:
- --doctor diagnostic support
- CLAUDE_USE_WAYLAND env var for native Wayland mode
- Display backend auto-detection
- Stale lock and socket cleanup
- Startup logging to ~/.cache/claude-desktop-debian/launcher.log
- Cowork resources (smol-bin, plugin shim) installed to resources
Also adds 'nix' package type to build_electron_args for proper
--no-sandbox handling on Wayland.
Refs #282
Co-Authored-By: Claude <claude@anthropic.com>
All three launchers (deb, RPM, AppImage) used bare `exit` after
`run_doctor`, discarding the return code. Now uses `exit $?` so
non-zero exit codes from doctor diagnostics propagate correctly.
Also adds joekale-pp to README contributors for RPM doctor support.
Co-Authored-By: Claude <claude@anthropic.com>
Follow-up to PR #299:
- Log original env var value instead of lowercased in alias resolution
- Use single quotes for literal string per style guide
- Document yes/no aliases in CONFIGURATION.md table
- Update noctuum's contributor entry for boolean alias work
Co-Authored-By: Claude <claude@anthropic.com>
The Windows installer contains smol-bin.x64.vhdx (SDK binaries for
KVM guest) and cowork-plugin-shim.sh (MCP plugin sandboxing). Both
were previously unavailable on Linux.
build.sh:
- Add copy_cowork_resources() to extract both files from the
Windows installer nupkg to Electron resources directory
- Patch index.js smol-bin copy to run on Linux (was win32-only)
cowork-vm-service.js:
- Convert smol-bin.vhdx to qcow2 at startVM time (same pattern as
rootfs conversion)
- Check bundleDir for smol-bin before falling back to VM_BASE_DIR
Refs #288
Co-Authored-By: Claude <claude@anthropic.com>
The app downloads VM images (rootfs.vhdx, vmlinuz, initrd) to
~/.config/Claude/vm_bundles/claudevm.bundle/ and passes this as
bundlePath in startVM params. The KVM backend was ignoring it and
looking in ~/.local/share/claude-desktop/vm/ instead.
- Use bundlePath from startVM params for rootfs, vmlinuz, initrd
- Convert VHDX to qcow2 at startVM time (app downloads VHDX format)
- Fall back to VM_BASE_DIR if bundle dir has no rootfs
- Check bundleDir and VM_BASE_DIR for smol-bin.qcow2
- Remove rootfs check from backend detection (rootfs location not
known until startVM, app may not have downloaded it yet)
- Downgrade missing smol-bin from error to info (SDK accessible
via virtiofs when available)
Refs #288
Co-Authored-By: Claude <claude@anthropic.com>
Use consistent template literals for log messages and restructure
the path.join assignment for readability.
Co-Authored-By: Claude <claude@anthropic.com>
- Extract _ensureSdkInstalled() to deduplicate SDK install logic
in spawn() and installSdk()
- Hoist virtiofsSock declaration before try block to avoid
redundant redeclaration in the virtiofs chardev section
- Add VIRTIOFS_GUEST_MOUNT constant to replace repeated magic string
- Use template literal consistently for error log message
Co-Authored-By: Claude <claude@anthropic.com>
The guest sdk-daemon treats stdin as a JSON-RPC notification
(fire-and-forget, no id, no response), not as a request. Sending it as
{type:"request"} causes the guest to reject with "unknown method" for
every method name variant. The correct wire format is:
{type: "notification", method: "stdin", params: {id, data}}
This was the final blocker preventing Cowork from becoming interactive.
Co-Authored-By: Claude <claude@anthropic.com>
The guest sdk-daemon needs to know where the SDK binary is on the
virtiofs mount. Compute the guest-side path and forward the installSdk
RPC call to the guest. If the guest isn't connected yet, defer until
just before spawn.
Co-Authored-By: Claude <claude@anthropic.com>
Three protocol fixes for communication with the guest sdk-daemon:
- Wrap outgoing messages with {type:"request"} — the guest expects this
envelope and ignores bare messages
- Handle guest event format {type:"event", event:"networkStatus"} in
addition to the direct {type:"networkStatus"} format
- Extract msg.result from responses instead of passing the full envelope
to callbacks
- Log decoded JSON for all guest messages and errors for debugging
Co-Authored-By: Claude <claude@anthropic.com>
The guest sdk-daemon mounts virtiofs with tag 'claudeshared' at
/mnt/.virtiofs-root, not 'hostshare' at /mnt/host. Mismatched tag
causes "wrong fs type, bad superblock" errors.
Co-Authored-By: Claude <claude@anthropic.com>
The guest sdk-daemon expects:
- /dev/vdb: a blank writable disk for session storage (ext4)
- /dev/vdc: the smol-bin disk (readonly) containing SDK binaries
Without the session disk, the guest tries to format smol-bin and fails.
Without smol-bin, the guest sdk-daemon crashes at startup.
Co-Authored-By: Claude <claude@anthropic.com>
vhost-user-fs-pci requires shared memory. Without memory-backend-memfd
with share=on, QEMU fails with vhost VQ ring errors. Only enabled when
virtiofsd is running.
Co-Authored-By: Claude <claude@anthropic.com>
virtiofsd is spawned asynchronously but QEMU needs the socket to exist
when it starts. Without this wait, QEMU fails with "Connection refused"
and dies silently. Poll every 100ms for up to 5s.
Co-Authored-By: Claude <claude@anthropic.com>
The guest sdk-daemon sends {"type":"event","event":"ready"} after boot.
Without 'ready' in FORWARDED_EVENTS, this event is silently dropped as
"unhandled guest message" and the app stays stuck on "starting up".
Co-Authored-By: Claude <claude@anthropic.com>