* feat(linux): hybrid titlebar mode for clickable in-app topbar
Default `CLAUDE_TITLEBAR_STYLE` is now `hybrid`: native OS frame
plus a BrowserView preload shim that convinces claude.ai's bundle
to render its in-app topbar (hamburger / sidebar / search / nav /
Cowork ghost). Stacked layout instead of Windows's combined bar,
but every button is clickable.
Why not the upstream `frame:false` + WCO config: investigation
(see docs/learnings/linux-topbar-shim.md) ruled out
`titleBarOverlay`, `titleBarStyle:'hidden'`, and the `.draggable`
CSS class as the source of the topbar click-eating drag region.
The remaining cause is a Chromium-level implicit drag region for
`frame:false` windows that exists on both X11 and Wayland and has
no Electron-API knob. With `frame:true` the OS handles dragging
and Chromium pushes no drag-region map, so the buttons receive
mouse events normally.
Modes:
- `hybrid` (default) — system frame + shim, topbar visible and
clickable
- `native` — system frame, no shim, no in-app topbar
- `hidden` — frameless + WCO config, matches Windows/macOS
upstream; topbar visible but not clickable on Linux. Kept for
Wayland comparison and future investigation
Tests: tests/launcher-common.bats grew 16 cases covering
`_resolve_titlebar_style`, `build_electron_args` flag selection
per mode, and `setup_electron_env` env-var wiring per mode.
`claude-desktop --doctor` now reports the resolved mode and
warns when `hidden` is set.
Co-Authored-By: Claude <claude@anthropic.com>
* docs(learnings): add hybrid-mode screenshot
Visual reference of the stacked layout: DE-drawn titlebar on top
with native window controls, claude.ai's in-app topbar
(hamburger / search / back-forward) immediately below it.
Co-Authored-By: Claude <claude@anthropic.com>
* docs(learnings): fix codespell hit (Pre-emptive → Preemptive)
Codespell flags hyphenated "Pre-emptive" as a misspelling of
"Preemptive". Drops the hyphen to clear the spellcheck CI gate
on PR #538.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Splits the 2124-line build.sh into a 318-line orchestrator plus
16 topical modules, grouped so CODEOWNERS can assign per-subsystem
reviewers:
scripts/_common.sh shared shell utilities
scripts/setup/ host detection, deps, download
scripts/patches/ regex patches on minified JS
_common.sh extract_electron_variable etc.
app-asar.sh wrapper injection
titlebar.sh
tray.sh menu handler + icon selection
quick-window.sh
claude-code.sh
cowork.sh cowork linux patching (largest)
scripts/staging/ post-patch file staging
build.sh now sources each module in dependency order and retains
only run_packaging, cleanup_build, print_next_steps, and main.
All globals stay at the top of build.sh and are read by sourced
modules; each module's header documents which globals it reads and
mutates (implicit-contract documentation).
This is a pure-move refactor. Function bodies were copied verbatim
— verified by byte-identical diff of the function set vs the
pre-split build.sh (34 functions, all present with identical bodies).
Note: .github/workflows/shellcheck.yml may benefit from a '-x' flag
so shellcheck follows the new '# shellcheck source=' directives, but
that CI tweak is left as a separate concern.
Co-Authored-By: Claude <claude@anthropic.com>
* fix: forward userSelectedFolders[0] as sharedCwdPath on cowork spawn (#412)
The cowork-vm-service daemon already honors a `sharedCwdPath` field on
the spawn IPC payload with priority over `cwd` (resolveWorkDir in
scripts/cowork-vm-service.js:500), but the upstream Electron app never
populates it on Linux backends. Every spawn arrives with only
`cwd=/sessions/{name}`, so the daemon derives the host path from
mountMap heuristics (PRs #389/#392/#411 cover the symptoms).
Patch 12 threads the user-selected folder through three sites so the
daemon receives the host path explicitly:
12a. At `this.getVMSpawnFunction({...})` config assembly, inject
`sharedCwdPath: SESSION.userSelectedFolders?.[0]` alongside the
existing mount config.
12b. At the Kyr() -> VMClient.spawn() call, forward
`SESSION.sharedCwdPath` as a new 13th positional argument.
12c. In the spawn() method body, accept a new trailing parameter
and set it on the IPC payload with a `VAR && (I.sharedCwdPath=VAR)`
guard matching the existing setter chain.
All three sub-patches detect prior application and no-op on re-run
(idempotent). If any site fails to match on a future upstream, the
daemon-side fallback from #392 keeps cwd resolution working — the
daemon workarounds in #389/#392/#411 remain as safety nets.
Verified against app.asar extracted from Claude-Setup-x64.exe for
version 1.3109.0. All three edits apply, output parses cleanly, a
second run is a no-op.
Co-Authored-By: Claude <claude@anthropic.com>
* refactor: simplify Patch 12 block
Reduce Patch 12 (#412) by 25 lines without changing the patched
output:
- 12a: use extractBlock('{') directly on the getVMSpawnFunction
argument and splice before its closing '}', removing the
inverted backward walk over the parenthesised block.
- 12a: replace the exec-until-null loop with matchAll + last
element to read the session-var name.
- 12c: replace the whole.replace().replace() + code.replace(whole)
reassembly chain with an index-based splice using
spawnMatch.index.
- 12c: drop the unneeded .split('') on the letter bag and inline
newArgList.
- Trim the prologue comment to match the density of Patches 1-10.
Patched index.js is byte-identical against the 1.3109.0 fixture
and the three idempotency log lines still fire on re-run.
Co-Authored-By: Claude <claude@anthropic.com>
* fix: address aaddrick's review — robust 12a anchor, 12b uniqueness assertion
Two fixes from PR #436 review:
1. **12a: drop fragile backscan, route through this.sessions.get().**
The previous `VAR.userSelectedFolders` backscan returned 10 matches
across 4 distinct vars (t, se, Ke, We) in the v1.3109.0 window —
last-match landed on `t` by coincidence, and `We` in particular is
a for-loop variable one upstream re-order away from becoming the
new "last". Swap to the canonical accessor the class already uses:
`this.sessions.get(sessionId)?.userSelectedFolders?.[0]`. The
sessionId var is extracted from the config's first field
`{sessionId:VAR` — scoped to the config block, 100% guaranteed
present, immune to unrelated references leaking in.
2. **12b: matchAll + uniqueness assertion.** The previous code used
`code.match()` which silently took the first hit if a second
upstream call site ever appeared. Switch to `matchAll` with
`length === 1` assertion; WARN-and-skip on anything else so a
wrong-site forwarding becomes detectable instead of silent.
3. **Drop misleading ordering comment in 12c.** The "12c before 12b
so property name is fixed" note was wrong — the property name is
a hardcoded string literal in both sub-patches, so the ordering
is cosmetic.
Verified: dry-run still applies all three patches on 1.3109.0 source,
output passes `node --check`, the three sharedCwdPath edits are
byte-stable across runs (the non-idempotency in Patch 9 is
pre-existing and orthogonal).
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Follow-up to #432. Instead of chmod'ing read-only files after the fact
in finalize_app_asar(), pass --no-preserve=mode on the cp invocations
in install_node_pty() so Nix-store 0444 bits never propagate into the
staging tree. This makes app.asar.contents internally consistent and
removes the need for the post-hoc chmod.
Also applied to the finalize_app_asar() cp from $pty_release_dir for
consistency, since that read also originates in the Nix store when
--node-pty-dir is set.
npm-install flows are unaffected: --no-preserve=mode forces default
(0666 & ~umask) mode, which matches what npm-installed files already
have.
Co-authored-by: Claude <claude@anthropic.com>
asar pack --unpack preserves Nix store read-only permissions on .node
files it extracts to app.asar.unpacked/. The subsequent cp -r fails
with 'Permission denied' trying to overwrite those read-only files.
Add chmod -R u+w before the copy to make any existing files writable.
* fix: suppress Cowork tab auto-select on every launch (#341)
Patch 4's empty Linux bundle manifest makes `[].every()` return
true vacuously, so `iBA()` reports "VM files present" and
`getDownloadStatus()` returns Ready on every startup. The remote
web app treats a startup observation of Ready as the same
download-completed transition that auto-navigates macOS/Windows
users to Cowork after their first download — Linux users hit it
on every launch.
Add Patch 4b to short-circuit `getDownloadStatus()` to
NotDownloaded on Linux. `iBA()` is left alone so the `download()`
IPC still succeeds instantly and the Cowork tab still works when
clicked — the web app's setup UI just passes through.
Anchor is stable: `getDownloadStatus` and the enum property
names (.Downloading, .Ready, .NotDownloaded) are readable in the
minified bundle. Verified against 1.3109.0 with an isolated
node run; idempotent on re-runs.
Co-Authored-By: Claude <claude@anthropic.com>
* refactor: destructure regex match in Patch 4b
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Catches up to current upstream URLs (1.3109.0), Joost-Maker's #418
identifier-widening fix in Patch 9, and #421's existsync/node-pty fix.
PR #410's last CI ran on April 16 against 1.2773.0 and showed
'WARNING: Could not find retry delay for auto-launch patch' — this
merge re-runs CI against current main to surface whether Patch 6's
regex anchors still match on 1.3109.0.
`install_node_pty()` copied only `lib/` and `package.json` into
`app.asar.contents/node_modules/node-pty/`, and `finalize_app_asar()`
packed app.asar without `--unpack`. The `.node` binaries were then
separately dropped into `app.asar.unpacked/.../node-pty/build/Release/`.
Result: the asar manifest had no entry for `node-pty/build/` at all.
When node-pty's loader (inside the asar) does
`require('../build/Release/pty.node')` from `lib/utils.js`, Electron's
asar -> .unpacked redirect never fires because the redirect requires
a manifest entry annotated as unpacked. The require returns
MODULE_NOT_FOUND despite the binary existing on disk, and Claude Code
mode shows "Failed to load terminal backend" on every shell session
attempt.
Two-part fix:
1. install_node_pty(): also stage `$pty_src_dir/build/` into
app.asar.contents so the pack step has the .node files to work
with.
2. finalize_app_asar(): pass `--unpack '**/*.node'` to `asar pack`
so the binaries get moved into app.asar.unpacked/ AND recorded
in the manifest as unpacked.
Verified: the new asar manifest now includes
UNPACKED: /node_modules/node-pty/build/Release/pty.node
UNPACKED: /node_modules/node-pty/build/Release/conpty.node
UNPACKED: /node_modules/node-pty/build/Release/conpty_console_list.node
and Claude Code's terminal loads successfully.
The pre-existing copy-to-.unpacked step in finalize_app_asar() is now
redundant but harmless (writes the same bytes); kept for now to
minimize diff and preserve the --node-pty-dir flow.
Co-Authored-By: Claude <claude@anthropic.com>
Patch 9 in patch_cowork_linux() extracts six minified variable names
from the win32 block to template a Linux block. The extraction regexes
used `(\w+)` which does not match `$` — JavaScript identifiers can
start with `$`, `_`, or a letter.
Claude >= 1.3109.0 renamed the local fs reference inside startVM's
win32 block from `e` to `$e` (likely to avoid shadowing the function
parameter `e`, which is the options object). The existing regex
`(\w+)\.existsSync\(` scans `$e.existsSync(U)`, skips the `$`, and
captures just `e`. Patch 9 then injects a Linux block calling
`e.existsSync(_ls)` — but `e` resolves at runtime to the options
object, so the call dies with `TypeError: e.existsSync is not a
function` and Cowork never boots on Linux.
Widen all six extraction patterns to `[$\w]+`. Also widen the
adjacent unanchored matchers in `archMatch` for consistency.
Add a defensive strip step before injection: if a future upstream
emits its own `if(process.platform==="linux"){...}` block right after
the win32 close brace, brace-count to its end and remove it so we
don't end up with two competing Linux blocks.
Verified: a clean rebuild now logs
vars: path=ae fs=$e log=qe stream=SL arch=Bre bundle=r
and the asar's injected block contains `$e.existsSync(_ls)`. Cowork
starts cleanly: `[VM:start] Startup complete, total time: 1242ms`,
the VM agent spawns, and prompts get responses end-to-end.
Fixes#418
Co-Authored-By: Claude <claude@anthropic.com>
Collapse Patch 6b console.log calls to single lines to match the
convention used in Patches 7-9. Each message fits well under 80
characters and doesn't need to be split across three lines.
Co-Authored-By: Claude <claude@anthropic.com>
Two coordinated patches in build.sh's patch_cowork_linux function
address the daemon's inability to recover after mid-session death.
Patch 6 (reworked):
- Replace the one-shot _svcLaunched boolean with a timestamp-based
_lastSpawn cooldown (10s). The retry loop can now re-fork the
daemon on subsequent iterations after a crash instead of seeing
the boolean already set and skipping the spawn forever.
- Redirect the forked daemon's stdout and stderr to
~/.config/Claude/logs/cowork_vm_daemon.log so node-level crash
output is no longer lost to stdio:"ignore". Falls back cleanly
if the log dir can't be opened.
Patch 6b (new):
- Extend the auto-reinstall delete list to also wipe
sessiondata.img and rootfs.img.zst. Upstream preserves these to
avoid re-download, but on 1.2773.0 the preserved files put the
daemon into an unstartable state that persists across app
restart and OS reboot (confirmed by issue reporter). Trade-off:
next successful startup re-extracts these images; acceptable
because auto-reinstall only runs after startup already failed.
Co-Authored-By: Claude <claude@anthropic.com>
* fix: gate quick window patch to KDE sessions only (#393)
PR #390 fixed a quick-window regression on KDE but regressed GNOME/Ubuntu —
@Andrej730 confirmed removing patch_quick_window restores quick entry on
Ubuntu 24.04. Without a reproduction environment for GNOME yet, the safe
minimum-viable fix is to gate the patch behind a runtime XDG_CURRENT_DESKTOP
check: apply on KDE (where the fix is validated), fall back to upstream
behavior everywhere else (which Ubuntu users confirmed works).
Both halves of the patch are gated:
- blur() before hide(): wrapped in a ternary so non-KDE sessions get the
original unconditional hide()
- focusFn()||show() replacement: wrapped so non-KDE sessions keep the
original focus check instead of the visibility check
Adds an idempotency pre-check in the node block (XDG_CURRENT_DESKTOP
substring near the anchor) so re-runs skip cleanly. Part 1's existing
grep idempotency still works because `Q.blur(),Q.hide()` appears inside
the ternary literally.
This is a temporary gate. VMs are being spun up to bisect which half
actually regresses GNOME; once isolated, only that half needs the gate.
Refs #393, #370, #404
Co-Authored-By: Claude <claude@anthropic.com>
* style: split de_check assignment to fit under 80 chars
Matches the concatenation style already used for the node block's
deCheck, bringing the bash literal under the style guide's line limit.
No functional change — the expanded string is identical.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: kill cowork daemon on app quit
The upstream cowork-vm-shutdown quit handler uses the Swift VM addon
which isn't available on Linux, so it's never registered. Our forked
cowork-vm-service daemon was invisible to the quit system, surviving
app exit and leaving QEMU/virtiofsd processes running.
Register a Linux-specific quit handler via the upstream
registerQuitHandler infrastructure. The handler sends SIGTERM to the
daemon (which already handles it gracefully), verifies the PID via
/proc/cmdline to prevent killing the wrong process on PID reuse, and
polls for exit up to 10 seconds.
The daemon PID is captured at fork time on a global, avoiding any
need for pgrep/execSync at quit time. The handler is registered
unconditionally for Linux so it works regardless of how the daemon
was launched.
Fixes#369
Co-Authored-By: Claude <claude@anthropic.com>
* style: simplify quit handler patch comments and scope
Add block scope for consistency with Patches 8-9, trim header comment,
remove hardcoded minified name from implementation comment, simplify
insertIdx calculation to match Patch 4 pattern.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
* fix: rewrite quick window patch with dynamic symbol extraction
The original patch from PR #147 hardcoded the minified variable name
`e` (e.g. `s/e.hide()/e.blur(),e.hide()/`), which stopped matching
after upstream minifier changes renamed the variable. This silently
regressed the fix for #144 (quick entry submit not showing main window).
Replace with two robust patches:
1. Extract the quick window variable dynamically via the unique
`setAlwaysOnTop(!0,"pop-up-menu")` anchor, then inject `blur()`
before `hide()` with correct operator precedence (wrapped in parens
to preserve the short-circuit guard).
2. Fix the main window not appearing after quick entry submit. The
upstream code gates `Lt.show()` on a focus check (`isFocused()`),
but on Linux `webContents.isFocused()` can return stale true for
hidden windows. Replace with the visibility check (`isVisible()`)
that other show-window paths in the same codebase already use.
Implemented as a Node.js inline patch anchored on unique
"[QuickEntry]" log strings, consistent with the cowork patches.
Fixes#144
Co-Authored-By: Claude <claude@anthropic.com>
* style: simplify comments in patch_quick_window
Remove version-specific minified names from comments (they change
between releases) and condense redundant explanations.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
* fix: extract minified vars dynamically in cowork patch 9 (#344)
Patch 9 (smol-bin VHDX copy) hardcoded minified variable names
(Qe, ft, vg, tt, uX) which change between upstream releases,
causing "Qe is not defined" crashes at runtime.
Extract all 6 variables dynamically from the nearby win32 block
using regex patterns that handle both minified and beautified code.
Add diagnostic logging of extracted variable names.
Also document the repo versioning system (REPO_VERSION,
CLAUDE_DESKTOP_VERSION variables and tag format) in CLAUDE.md.
Fixes#344
Co-Authored-By: Claude <claude@anthropic.com>
* style: simplify console.log calls in cowork patch 9
Remove redundant comment restating the regex pattern, and replace
unnecessarily split string concatenations in console.log calls
with template literals (consistent with the existing pattern on
the final patchCount summary line).
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
* fix: disable VM file downloads on Linux to prevent checksum loop (#334)
Patch 4 in patch_cowork_linux() previously copied win32 VM file entries
(rootfs.vhdx, vmlinuz, initrd) with Linux-specific checksums. These
checksums drifted from CDN content, causing an infinite download retry
loop for all Linux users — including bwrap users who don't need VM
files at all.
The root cause: Patch 1 opens the yukonSilver feature gate for Linux,
making the VM download path reachable even on bwrap-only installs. The
triage bot missed this because it analyzed unpatched code.
Fix: inject empty file arrays (linux:{x64:[],arm64:[]}) instead of
copying win32 entries. This is safe because:
- The VM backend is non-functional on Linux (bwrap is the only backend)
- Empty arrays make the download loop a no-op (for...of [] skips)
- [].every() returns true (vacuous truth), reporting "Ready" status
- The linux key must exist to prevent TypeError on files["linux"]["x64"]
Removes ~230 lines of checksum infrastructure from build.sh and CI that
maintained checksums for a non-functional feature.
Fixes#334Closes#329Closes#332
Co-Authored-By: Claude <claude@anthropic.com>
* style: clean up stray blank line and use durable issue reference
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
- Fix compute_checksum() stdout contamination: log messages were
captured into variables alongside hash values; redirect to stderr
- Use EXIT trap for temp file cleanup instead of repeating rm/output
in every early-exit path
- Remove redundant log messages in Patch 4 (replaceChecksums already
logs its own status)
Co-Authored-By: Claude <claude@anthropic.com>
The cowork manifest patch (Patch 4) copied win32 file entries as linux
entries. Since Anthropic now publishes Linux-specific VM images with
different content, the win32 checksums cause silent validation failures
and startVM timeouts.
Compute correct SHA-256 checksums for the linux CDN files and embed
them in build.sh. Patch 4 now replaces win32 checksums with the linux
values before injecting the manifest entry. Falls back to win32 values
if linux checksums are empty.
The check-claude-version CI workflow is extended to automatically
recompute VM checksums when a new version is detected. This is
non-blocking — if CDN files aren't published yet or computation fails,
the rest of the workflow proceeds unaffected.
Fixes#329
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>
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>
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.
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>
* feat: add KVM/bwrap isolation backends for cowork mode
Refactor cowork-vm-service.js from monolithic VMManager into a pluggable
backend architecture with three isolation levels:
- HostBackend: direct execution on host (existing Phase 1 behavior)
- BwrapBackend: bubblewrap namespace sandbox (PID/mount isolation)
- KvmBackend: full QEMU/KVM VM with vsock, virtiofs, QMP monitor
Backend auto-detected at startup (KVM > bwrap > host) or overridden
via COWORK_VM_BACKEND env var. Shared helpers extracted for env
filtering, arg cleanup, command resolution, and work dir handling.
Also:
- Add Cowork Mode section to --doctor diagnostics with per-tool
checks and distro-specific install hints
- Update Patch 4 to extract real win32 file entries for linux
bundle manifest (enables app download infrastructure)
- Update handover documentation for Phase 2/3 architecture
Co-Authored-By: Claude <claude@anthropic.com>
* fix: correct KvmBackend vsock port, direction, and kernel cmdline
The guest sdk-daemon connects TO the host (CID=2), not the other way
around. Confirmed via disassembly of the guest binary: the vsock port
is 51234 (0xC822), matching the Hyper-V GUID in the Windows service.
- Change VSOCK_GUEST_PORT from 2222 to 51234
- Reverse socat bridge: VSOCK-LISTEN (host listens) instead of
VSOCK-CONNECT (host connecting to guest)
- Add bridge server to accept persistent guest connection and route
events/responses via callback map
- Fix kernel cmdline: root=LABEL=cloudimg-rootfs (matches fstab)
Co-Authored-By: Claude <claude@anthropic.com>
* fix: patch VM download to use disk-backed temp dir on Linux
Linux systems often mount /tmp as a small tmpfs (3-4GB). The VM
rootfs download decompresses to ~9GB, causing ENOSPC. Patch the
app's mkdtemp("wvm-") call to use the bundle directory (on real
disk) instead of os.tmpdir() on Linux.
Uses regex-based dynamic variable extraction to remain
version-agnostic across minified code changes.
Co-Authored-By: Claude <claude@anthropic.com>
* fix: handle stale cowork socket (ECONNREFUSED) on Linux
Stale sockets from previous sessions give ECONNREFUSED instead of
ENOENT, bypassing the retry loop and auto-launch entirely. Fix:
- Expand ENOENT check in retry loop to include ECONNREFUSED on Linux
- Add cleanup_stale_cowork_socket() to launcher scripts (all formats)
that removes dead sockets before Electron starts
- Increase tmpdir patch search window from 1000 to 2000 chars
Co-Authored-By: Claude <claude@anthropic.com>
* fix: preserve DNS resolution inside bwrap sandbox
On systems using systemd-resolved, /etc/resolv.conf is a symlink to
/run/systemd/resolve/stub-resolv.conf. The bwrap --tmpfs /run option
wiped this out, breaking DNS resolution inside the sandbox and
preventing the spawned Claude process from reaching the API.
Bind-mount the resolved /run/systemd/resolve/ directory back into
the sandbox as read-only to restore DNS.
Co-Authored-By: Claude <claude@anthropic.com>
* fix: harden cowork isolation and build patches
- Remove broken _setupEventForwarding (events already flow through
_handleGuestData); the second bridge connection was silently ignored
- Mount $HOME read-only in bwrap sandbox; only workDir and explicit
mounts are writable (prevents writes to ~/.ssh, ~/.gnupg, etc.)
- Scope Patch 4 win32 extraction to actual win32:{} block via brace
counting to avoid crossing into darwin/linux sections
- Set _qmpAvailable flag on QMP timeout instead of silently continuing
- Wrap CID allocation at 65535 to prevent unbounded growth
- Use execFileSync instead of execSync('which ...') in detectBackend
- Coerce response ID to String for Map lookup in _handleGuestData
- Use non-greedy [\s\S]*? in Patch 6 regex for nested brace robustness
- Update patch count threshold from 4 to 5 after adding Patch 8
- Add age-based fallback for stale socket cleanup when socat is missing
- Use indexOf-based splice in Patch 8 instead of string.replace()
- Extract shared resolveSdkBinary helper to deduplicate SDK resolution
- Remove dead retryFuncRe/retryFuncMatch variables from Patch 6
Co-Authored-By: Claude <claude@anthropic.com>
* fix: address security and correctness issues from code review
- Replace execSync string interpolation with execFileSync for qemu-img
calls to eliminate shell injection risk
- Add path validation to readFile in both LocalBackend and KvmBackend
to restrict reads to within the user's home directory
- Fix QMP _sendQmpCommand timer leak by clearing timeout on success
- Fix _pendingCallbacks.delete() to use String(msg.id) matching the
String(msg.id) used in the .get() lookup
- Extract FORWARDED_EVENTS constant, cleanup helper, extractBlock
helper, and consolidate doctor tool checks (from simplifier pass)
Co-Authored-By: Claude <claude@anthropic.com>
* docs: update README cowork notice with isolation backends and doctor info
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>