* launcher: add CLAUDE_GTK_IM_MODULE opt-in override (#549)
Some users hit broken IBus integration on Linux and have to wrap
every launch with `GTK_IM_MODULE=xim claude-desktop`. Forcing this
for everyone would break CJK input methods, compose keys, and
dead-key sequences (rationale in #545), so this lands as opt-in.
When `CLAUDE_GTK_IM_MODULE` is set non-empty, `setup_electron_env`
exports `GTK_IM_MODULE=$CLAUDE_GTK_IM_MODULE` before the Electron
exec and logs the override (with the prior value) to launcher.log.
Unset/empty leaves `GTK_IM_MODULE` alone — no behavior change for
users not affected by the IBus issue.
Adds a TROUBLESHOOTING.md section documenting symptoms, valid
values, the trade-off note for `xim`, and BATS coverage for the
set / unset / empty / unset-prior cases.
Closes#549. Refs #545.
Co-Authored-By: Claude <claude@anthropic.com>
* launcher: tighten CLAUDE_GTK_IM_MODULE comment and docs (#549)
Trim the in-source comment to what's not implied by the guard, drop
the underscore prefix on the local, and remove a redundant trailing
sentence + duplicated trade-off line from TROUBLESHOOTING.md. No
behavior change; all 68 BATS tests still pass.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
* launcher: log session/IME env block at startup (#548)
Adds log_session_env, called once per launch from each packaging
target (deb, rpm, AppImage, Nix). Emits a single env={ ... } block
covering display (XDG_SESSION_TYPE, WAYLAND_DISPLAY, DISPLAY,
XDG_CURRENT_DESKTOP), IME (GTK_IM_MODULE, XMODIFIERS, QT_IM_MODULE),
and Claude-specific overrides (CLAUDE_USE_WAYLAND,
CLAUDE_TITLEBAR_STYLE, CLAUDE_GTK_IM_MODULE).
Empty/unset values are emitted as `KEY=` (rather than omitted) so
absence is unambiguous in bug reports. Pure observability — no
behavior change.
Closes#548
Co-Authored-By: Claude <claude@anthropic.com>
* test: consolidate log_session_env BATS coverage (#548)
Collapse the four log_session_env cases into two, and tighten the
assertions in both:
- Old test 1 (substring match per key) + old test 4 (block braces
on their own lines) → one test using exact-line equality on the
`lines` array. Locks block structure and per-key formatting in
a single pass; substring matching could not catch a regression
that re-ordered keys, dropped indentation, or merged lines.
- Old test 2 (unset values are KEY=) + old test 3 (empty-string
is KEY=) → one test covering both code paths. Exact-line match
proves the value after `=` is truly empty; the previous
`*'KEY='*` substring would have matched `KEY=value` and the
old test-3 regex was fragile (depended on trailing newline
being literal `$'\n'` vs end-of-string `$`).
Net: 77 → 42 lines, 4 → 2 cases, stronger guarantees. No change to
the helper itself or the call sites — issue #548 acceptance criteria
still hold.
---------
Co-authored-by: Claude <claude@anthropic.com>
* feat(lifecycle): notify and offer restart on in-place package upgrade
dpkg/rpm replace app.asar via rename() while the main process keeps
its in-memory JS. Any window opened after the swap loads HTML/asset
files fresh from disk, where the hashed asset filenames now point at
v(N+1) bundles that the in-memory v(N) IPC and preload layers don't
match. Symptoms observed across recent reports: Quick Entry rendering
as raw JS text, About dialog showing minified source, and Ctrl+Q
intermittently failing — anything where a post-swap window load
crosses the version boundary.
macOS / Windows clients get this from Squirrel; Linux deb/RPM has no
equivalent, so we watch the file ourselves and surface a click-to-
restart notification. AppImage is unaffected (squashfs mount stays
pinned to the running file's contents); Nix store paths are immutable
until GC, so the running inode also stays valid until explicit
relaunch. The watcher noop-quiet on those targets is deliberate.
Implementation: stat-baseline app.asar at first require('electron'),
watch the parent dir (file-level fs.watch loses the inode across
rename-replace; inotify on the dir reports the new entry via
IN_MOVED_TO), filename-filter, debounce 5s past the last event to
clear dpkg's .dpkg-new → rename dance, compare ino+mtime to confirm
a real change, then show a Notification deferred behind whenReady.
Click → app.relaunch(); app.quit().
Co-Authored-By: Claude <claude@anthropic.com>
* refactor(lifecycle): flatten upgrade-watcher block
Hoist the in-place-upgrade detection block in frame-fix-wrapper.js
into an armUpgradeWatcher() helper so the missing-baseline path
becomes an early return instead of an outer if (baseline) wrap.
Collapse the isReady() ? show() : whenReady().then(show) ternary
to plain whenReady().then(show) — Electron's whenReady() resolves
immediately when the app is already ready, so the branch was
dead. Trim narrative comments that duplicate the previous commit
message; keep the why-comments that earn their keep (parent-dir
watch, ino+mtime, 5s debounce, watcher.unref).
Behaviour preserved: filename filter, 5s debounce, ino+mtime
double-check, watcher.unref(), best-effort try/catch, idempotent
notified guard. Net -22 lines.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
* docs(upstream-reports): draft anthropics/claude-code report for #546
Adds a ready-to-file draft of the upstream MCP double-spawn report,
matched to the anthropics/claude-code bug_report.yml schema and
written in aaddrick's documented voice.
Includes a filing checklist (GitHub issue + in-app /bug + bidirectional
comment back on #546) and a note about the template mismatch since the
form is built for the Claude Code CLI rather than Claude Desktop.
Refs #546
Co-Authored-By: Claude <claude@anthropic.com>
* docs(upstream-reports): link claude-desktop-debian repo in draft body
Co-Authored-By: Claude <claude@anthropic.com>
* docs(upstream-reports): add download-rate context next to repo link
Adds an approximate "~2,300 package downloads/day across the last 3
releases" parenthetical so the upstream report leads with a sense of
how many users the bug affects.
Computed from GitHub release asset download counts: 13,823 installable
binary downloads (deb + rpm + AppImage, both arches) across 1.5354.0,
1.5220.0, and 1.4758.0 over a ~6-day window.
Co-Authored-By: Claude <claude@anthropic.com>
* docs(upstream-reports): voice pass on draft body and title
Refines the draft for the upstream `anthropics/claude-code` issue
through the aaddrick-voice profile. Changes are surgical:
- Title: em-dash separator → colon (matches voice's documented
preference for colons; removes em-dash signal site-wide).
- "What's Wrong?": opens with personal-experience framing ("I was
reading", "What I found"), splits compound sentences, swaps
announcement-colons for periods.
- "What Should Happen?": same period-for-colon swap on the fix
paragraph.
No content shifts. Symbol table, code blocks, log signals, links,
and form-field labels untouched.
Co-Authored-By: Claude <claude@anthropic.com>
* docs(upstream-reports): drop bogus /bug filing step
User confirmed `/bug` and `/feedback` are inert in both Claude
Desktop and Claude Code. Earlier web research suggesting they route
to engineering was wrong. Replaced step 4 of the filing checklist
with a note about what's actually in the Help menu (Get Support
goes to the support chat, wrong queue) and what the Troubleshooting
submenu IS useful for (attaching Installation ID / logs to a
GitHub issue).
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
Per-coordinator-registry framing (CCD + LAM + SshMcpServerManager)
replaces the previous two-coordinator framing. Notes that each
coordinator dedups within its own scope, so the bug is strictly
cross-coordinator. Routing correction: the SDK does what it's told
- the bug is in Claude Desktop's coordinator wiring, so the SDK
repo is only a defensible secondary venue for advocacy of a
shared-transport/multiplex primitive. Symbol drift section points
at #546 for current minified symbols and extraction regexes.
Co-authored-by: Claude <claude@anthropic.com>
DNF defaults to a 48h metadata cache when metadata_expire is unset,
so users running `dnf install/reinstall claude-desktop` shortly after
a release see stale versions until either the cache expires or they
manually run `dnf clean expire-cache`.
Lower the cache TTL on the generated repo file so freshly published
releases propagate within an hour without user intervention.
Co-authored-by: Claude <claude@anthropic.com>
* fix(ci): force primary GPG key for repomd.xml signing
PR #217 added --default-key for the gpg invocation that signs
repomd.xml, but gpg's --default-key only chooses an identity, not
which key under that identity actually signs. Without a trailing
'!' on the keyid, gpg silently picks the most recent signing
subkey. rpm 4.20+ and zypper verify repomd.xml only against the
primary key, so the published signature fails verification with
"Signature verification failed for repomd.xml" / "Signing key not
found" — the exact symptom reported in #213.
Append '!' to the keyid argument to force the primary key.
Verified locally against zypper 1.14.96 / rpm 4.20.1 / gpg 2.x by
re-signing the live repomd.xml with a test primary+subkey keypair:
- Without '!': sig keyid = subkey, zypper refresh fails with
"Signature verification failed for repomd.xml" (reproduces
the production bug 1:1).
- With '!': sig keyid = primary, zypper refresh succeeds:
"Die angegebenen Repositorys wurden aktualisiert."
Fixes#213 (regression of PR #217)
Co-Authored-By: Claude <claude@anthropic.com>
* docs(ci): tighten repomd.xml signing comment
Compress the rationale block from 8 to 6 lines while preserving
the load-bearing facts (gpg picks subkey by default, rpm 4.20+ /
zypper reject subkey-signed repomd.xml, '!' forces the primary
key, #213/#217 regression history). Adds an explicit "Do not
strip it" admonition to the future reader.
No functional change.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
* test: isolate cleanup_stale_cowork_socket BATS from host pgrep state
Stub `pgrep` inside the `cleanup_stale_cowork_socket: removes stale
socket file` test so it returns nonzero. Without this, the test fails
on any developer machine running Claude Desktop because the real
`pgrep -f cowork-vm-service\.js` finds the live daemon and the
function correctly bails out before removing the socket — the
function's "daemon alive, leave socket alone" branch was leaking into
a test that was supposed to exercise the "no daemon, remove stale
socket" branch.
Fixes#533
* test: address PR #534 review — drop no-op export and stale comment
Per aaddrick's review:
- export -f pgrep is a no-op since cleanup_stale_cowork_socket runs
in the same shell and bash function lookup beats PATH
- the "socat connection should fail" comment predates the move to
pgrep checks and is now misleading
Two regex anchors in patch_cowork_linux() used \w+ to capture minified
identifiers, but on Claude Desktop 1.5354.0 those identifiers contain $
(e.g. C$i, g$i). \w excludes $, so the inner captures never matched:
- Patch 2b (vm: module assignment) silently no-op'd — no warning, no
failure. Build log went from "Applied 12" to "Applied 10".
- Patch 6 step 2 (retry-delay auto-launch) emitted a warning but still
failed to apply.
Either way, the resulting app.asar shipped half-patched and Cowork
startup failed at runtime with "Swift VM addon not available".
The fix widens both inner captures from \w+ to [\w$]+, matching the
existing precedent at scripts/patches/cowork.sh:482-501 (introduced in
PR #421 for the $e fs-reference rename in 1.3109.0). Also switches
Patch 6 from indexOf to lastIndexOf for the "VM service not running"
anchor — defensive against future versions reintroducing the string
outside the retry-loop site.
Verified end-to-end on Fedora 43 / KDE Plasma 6 / Wayland: build log
shows "Applied 12 cowork patches"; daemon auto-launches at startup
with clean lifecycle (startup → listen → SIGTERM exit code=0).
Follow-ups tracked in #559.
Resolves#558. Likely resolves#553 (named symptom) and #445 (daemon
never auto-spawned on Linux).
Co-authored-by: Joost-Maker <66303669+Joost-Maker@users.noreply.github.com>
Co-authored-by: HumboldtJoker <19808525+HumboldtJoker@users.noreply.github.com>
Co-authored-by: zabka <3833286+zabka@users.noreply.github.com>
* 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>
* docs(learnings): document MCP double-spawn upstream bug (#526)
Captures the reporter's root-cause analysis for issue #526: stdio MCP
servers in claude_desktop_config.json get spawned twice when both the
chat panel and the Code/Agent (Cowork) panel are active. The
duplication happens entirely in upstream Anthropic Claude Desktop main
(LocalSessions and LocalAgentModeSessions each hold an independent
Claude Agent SDK query whose stdio transport bypasses the global hZ
MCP registry).
Includes verification that this packaging is not implicated, the
lockfile + idempotent-write workaround pattern for affected MCP
authors, and routing guidance for upstream reports.
Co-Authored-By: Claude <claude@anthropic.com>
* docs(learnings): simplifier pass on MCP double-spawn entry
Drop redundant "Anthropic" qualifier in Status section and reword
CLAUDE.md index bullet to noun-phrase form matching siblings.
Co-Authored-By: Claude <claude@anthropic.com>
* docs(learnings): apply review fixes from #527
- Fix `LocalAgentModeSessions` IPC namespace: add missing `_$_`
separator (was `claude.web_$_LocalAgentModeSessions_*`, should be
`claude.web_$_LocalAgentModeSessions_$_*`). Verified against the
channel names in the actual minified source.
- Add back the `Logs prefix` column (`[CCD]` / `[LAM]`) the original
issue body had — these are the literal grep targets in
`~/.config/Claude/logs/` for confirming the bug hit.
- Re-route the secondary upstream venue from `anthropics/claude-code`
to `anthropics/claude-agent-sdk-typescript`. The SDK transport
(`spawnLocalProcess` / `Du.spawn`) lives in the SDK's own public
repo (issues enabled); pointing at `claude-code` while saying the
CLI isn't on the spawn path is the exact contradiction the warning
paragraph below it tries to prevent.
- Workaround note: reclaim a stale lock via `rename()` over the path,
not `unlink()` then re-open. Heads off the obvious-but-racy port
for anyone copying the pattern.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Adds:
- `*)` case + valid-values warning on both `COWORK_VM_BACKEND` switches in `scripts/doctor.sh`, factored through a shared `_warn_unknown_backend` helper. Switch A explicitly matches the empty and `bwrap` cases as no-ops alongside `kvm|host` so only truly-unknown values trigger the warn. Switch B (user-facing summary) reports cowork_backend as `auto-detect (invalid override '...' — see warning above)` so the doctor is honest about what the daemon actually does (#442 tracks the daemon-side fix).
- `COWORK_VM_BACKEND` env var row + new Cowork Backend section in `docs/CONFIGURATION.md`, placed before Cowork Sandbox Mounts.
- VM connection timeout / virtiofsd PATH / Fedora tmpfs (EXDEV) sections in `docs/TROUBLESHOOTING.md`.
- README acknowledgment for @CyPack.
Closes#293
Co-Authored-By: aaddrick <aaddrick@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(bwrap): support {src, dst} mount form for distinct host/sandbox paths
Extends coworkBwrapMounts (#339) so additionalROBinds and additionalBinds
accept entries of the form { src, dst } in addition to the existing string
form. This unlocks the persistent /tmp use case: the default --tmpfs /tmp
gets wiped between Bash tool calls because of --die-with-parent, and the
old string-only API (--bind p p) had no way to map a host directory under
$HOME onto /tmp inside the sandbox without exposing the host /tmp itself.
Validation:
- src: same checks as the string form (absolute, not in
FORBIDDEN_MOUNT_PATHS, $HOME constraint when RW)
- dst: absolute and non-forbidden only — the $HOME constraint is
intentionally skipped since the whole point of the form is to map
outside $HOME (e.g. /tmp)
- malformed objects are filtered out with a warning, matching the
existing string-validation behavior
Doctor (--doctor) renders the object form as "src -> dst" in both the
Python and Node parser branches.
100% backwards compatible: the string form is preserved unchanged. The 36
existing tests pass; 13 new tests cover accept/reject paths, mixed
string+object configs, the persistent-/tmp recipe end-to-end, and the
doctor rendering (58/58 total).
Closes#530
---
Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <claude@anthropic.com>
* docs(configuration): document {src, dst} mount form
Refs #530
---
Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <claude@anthropic.com>
* chore(bwrap): address PR #531 review feedback
- doctor: warn when an additional mount's dst lands on a default RO
mount (/usr, /etc, /bin, /sbin, /lib, /lib64, or subpaths). bwrap
honors the later mount, so the user's bind silently replaces the
default — a config footgun, not an escape, but worth surfacing
(RayCharlizard issue 1)
- docs(configuration): note the shadowing implication under
"Distinct host/sandbox paths" (RayCharlizard issue 2)
- test(bwrap-config): pin the reject contract for dst under a
forbidden path (e.g. /proc/self), beyond the existing exact-match
case (RayCharlizard issue 3)
- bwrap-config: harmonize the rejected-mount warning text — the
string-form path now reads "rejected mount" like the object-form
variants (RayCharlizard issue 4)
Tests: 61/61 passing (3 new: 1 reject-subpath + 2 doctor shadow
positive/negative).
Refs #530
---
Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
* fix(autostart): route openAtLogin through XDG Autostart on Linux (#128)
Electron's app.getLoginItemSettings()/setLoginItemSettings() are
no-ops on Linux (electron/electron#15198), so the "Run on startup"
toggle never persists and isStartupOnLoginEnabled() returns
undefined, failing the IPC handler's typeof === 'boolean' check.
Intercept both calls in frame-fix-wrapper.js and back them with
~/.config/autostart/claude-desktop.desktop, which is honoured by
GNOME/KDE/XFCE/Cinnamon/MATE/LXQt (XDG Autostart spec). Also
coerce executableWillLaunchAtLogin (Windows-only in Electron,
undefined on Linux) to a boolean so the IPC handler stops
throwing.
Fixes#128
Co-Authored-By: Claude <claude@anthropic.com>
* fix(autostart): address review — APPIMAGE runtime target, XDG_CONFIG_HOME, StartupWMClass (#128)
Addresses review comments on #450:
- Resolve Exec= and Icon= at toggle time via process.env.APPIMAGE
so AppImage users (who don't have claude-desktop on $PATH unless
integrated via AppImageLauncher) get an autostart entry that
launches the actual .AppImage bundle instead of a broken binary
reference. escapeExecArg() handles Desktop Entry Exec escaping
(quote + backslash-escape reserved chars).
- Honour $XDG_CONFIG_HOME when set and non-empty, falling back to
~/.config only otherwise. Home-manager and dotfile users who
relocate the config root were getting the entry dropped in the
wrong place silently.
- Add StartupWMClass=Claude to the generated entry, matching the
value set by scripts/packaging/{deb,rpm}.sh, so DEs group the
autostarted window with user-launched instances under a single
taskbar/dock item. Drop Categories= per review guidance
(autostart parsers ignore it).
- Comment why opts.path is intentionally ignored: process.execPath
points at the electron binary, not the launcher shim that sets
ELECTRON_FORCE_IS_PACKAGED / ozone flags / orphan cleanup —
honouring opts.path would write a broken autostart entry.
The "removed" log placement (review item 4) is already inside the
inner try, so unlinkSync throwing ENOENT short-circuits before the
log runs. Left as-is.
Co-Authored-By: Claude <claude@anthropic.com>
* docs(readme): credit lizthegrey for XDG Autostart contribution
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: aaddrick <aaddrick@gmail.com>
* fix(lifecycle): hide main window to tray on close, Linux (#448)
Electron's default window-all-closed handler quits the app on
Linux. The existing tray icon and Ctrl+Q patches keep the app
reachable while a window is alive, but as soon as the last
window is closed (stray click on X, or a sign-out flow that
closes mainWindow) the app exits and the tray goes with it —
taking any in-app schedulers / MCP servers / cron tasks
(/schedule skill) down silently until the user re-launches.
Intercept BrowserWindow.close on main windows (not popups;
Quick Entry and About already dismiss via hide(), never emit
close) and preventDefault + hide unless app is in a real quit
path. The quit path is detected via before-quit: Ctrl+Q, tray
Quit, cmd+Q, SIGTERM and app.quit() from anywhere all emit
before-quit, which arms app._quittingIntentionally so the
close handler lets the window actually close.
Gated by CLOSE_TO_TRAY, default on. Set CLAUDE_QUIT_ON_CLOSE=1
to restore the Electron-default behaviour.
Fixes#448
Co-Authored-By: Claude <claude@anthropic.com>
* fix(frame-fix-wrapper): drop superseded globalShortcut Ctrl+Q
Removes the globalShortcut.register('CommandOrControl+Q') block
that #484 superseded with the per-window
webContents.on('before-input-event') listener. Auto-merging main
into this branch left both registrations in place, which would
re-introduce the AZERTY physical-keycode grab and system-wide
shortcut steal that #484 fixed. The focus-scoped listener
already covers the original #321 hidden-menu-bar use case.
Also updates the close-to-tray comment to reference the new
listener path instead of the removed global shortcut.
Co-Authored-By: Claude <claude@anthropic.com>
* docs(readme): credit lizthegrey for close-to-tray contribution
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: aaddrick <aaddrick@gmail.com>
Replaces mutable tag refs (e.g. @v4) with full commit SHAs across all
workflows, with the version retained as a trailing comment for
readability and dependabot compatibility.
Motivation: the March 2026 trivy-action supply-chain attack poisoned 75
of 76 version tags in a single repo. Any consumer using @vX-style
references ran the compromised code automatically. SHA pinning makes
that class of attack a no-op for us — a hijacked tag cannot point at
new code without the SHA also changing.
Pinned actions:
actions/checkout@v4, actions/upload-artifact@v4,
actions/download-artifact@v4, actions/setup-python@v5,
actions/setup-node@v4, actions/github-script@v7,
softprops/action-gh-release@v2, crazy-max/ghaction-import-gpg@v6,
codespell-project/codespell-problem-matcher@v1,
codespell-project/actions-codespell@v2,
cloudflare/wrangler-action@v3,
DeterminateSystems/nix-installer-action@v21
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
* fix: update visibility function regexp
* fix(quick-window): tolerate optional var decl in visibility regex
Make the `var <name>(,<name>)*;` prefix optional so the regex
matches both the older shape (`function L7A(){return!Ct...}`,
1.3109.0) and the current one (`function aZA(){var e;return!Qt...}`,
1.3883.0). The minifier hoists `var e;` whenever the function body
uses optional chaining; if a future release adds `var e,t;` or
drops the var entirely, this still matches without another
chase-the-shape PR.
Verified end-to-end on the live 1.3883.0 build asar: extracts
`pF` / `aZA`, patches both Quick Entry anchor sites
("Navigating to existing chat", "Creating new chat with
submit_quick_entry"), JS validates, idempotent re-run confirmed.
Confirmed against the 1.3109.0 build-reference shape too.
Repro of #390 on Nobara KDE Plasma 6 (Wayland): quick-entry
submit now reliably shows the main window post-patch; no
regressions in regular chat or window restore flows.
Co-Authored-By: Claude <claude@anthropic.com>
* docs(readme): credit @Andrej730 for visibility regex fix (#495)
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: aaddrick <aaddrick@gmail.com>
Co-authored-by: Claude <claude@anthropic.com>
* fix: update Linux tray icon in place on OS theme change
Avoids a StatusNotifierItem re-registration race on KDE Plasma
where the old SNI remains registered when the new one appears,
resulting in two tray icons side by side until session logout.
`patch_tray_menu_handler` already bounds the race with a 250 ms
delay after `tray.destroy()`, but that's not enough on all setups
(reproduced on Fedora 43 KDE Plasma 6.6.4 + Wayland). Widening the
delay just moves the goalposts; the race is structural.
Fix: inject a fast-path before the existing destroy+recreate block
in the tray rebuild function. When the tray already exists and
isn't being disabled, update its icon and context menu in place
via `setImage` + `setContextMenu` — the existing StatusNotifierItem
stays registered, no DBus re-registration, no race. The slow path
(destroy + delay + re-create) is kept for the initial creation and
the tray-disable cases where it's unavoidable.
All five minified locals needed by the fast-path (tray function,
tray variable, electron module, menu function, icon path const,
menuBarEnabled flag) are extracted dynamically; the idempotency
guard re-keys off the post-rename `setImage(...)` sequence.
Triggered in KDE System Settings by any of Appearance → Colors /
Plasma Style / Global Theme, which all fire the same
`nativeTheme.on('updated')` signal.
Follow-up to #491. The broader submenu work from that PR stays
parked on features/change-icon-color pending the scope discussion
in #492; this PR ships only the duplicate-tray-icon fix that
@aaddrick asked to split out.
Co-Authored-By: Claude <claude@anthropic.com>
* fix(tray): tighten in-place patch extraction guards
Drop the redundant `electron_var_re_local` local — `electron_var_re`
is already a sourced global from `_common.sh` with the same value.
Replace the silent `head -1` on `enabled_var` extraction with an
explicit count-and-bail. The grep matches `const X=fn("menuBarEnabled")`
across the whole file; today there's exactly one site (inside the
tray function), but if upstream ever ships a second the previous
code would silently bind to whichever the minifier emitted first.
Bail loudly with a count diagnostic instead.
Verified on the live 1.3883.0 build asar: all five extractions
resolve (`Nh`/`wAt`/`t`/`e`) — note the symbol drift vs. the
build-reference's `fh`/`CZe`. Fast-path injects, JS validates,
idempotent re-run confirmed, duplicate-icon repro gone on Nobara
KDE Plasma 6 (Wayland) under Appearance → Colors / Plasma Style /
Global Theme.
Co-Authored-By: Claude <claude@anthropic.com>
* docs(readme): credit @IliyaBrook for tray duplicate-icon fix
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: aaddrick <aaddrick@gmail.com>
Quad9 now resolves pkg.claude-desktop-debian.dev to Cloudflare IPs;
the hourly check is no longer needed.
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
* ci: run BATS test suite on push and PR
The /tests/ directory has 186 BATS tests
(launcher-common, launcher-xrdp-detection, and four
cowork-*.bats files) but no workflow ever invoked `bats`
— the entire suite was effectively inert.
A regression in launcher-common.sh or
cowork-vm-service.js would not fail any check,
including the BATS suite added by PR #395.
Add a standalone tests.yml workflow that:
- installs bats + nodejs
- runs `bats tests/*.bats`
- executes on every PR
- executes on pushes to main
Push triggers are path-filtered to:
- tests/
- scripts/
- .github/workflows/tests.yml
PR triggers remain unfiltered so required-check
behaviour stays predictable.
Kept this standalone rather than extending
test-artifacts.yml so unit tests run in seconds
instead of waiting for full artifact builds.
This can be promoted to a build gate later once
it proves stable in CI.
CODEOWNERS
- adds /.github/workflows/tests.yml under @sabiut
- keeps /tests/cowork-*.bats ownership with @RayCharlizard
This PR only enables CI coverage for existing tests
and does not modify cowork test logic.
* fix(tests): unset XDG_CONFIG_HOME in cowork-bwrap-config setup
The "doctor: reports custom bwrap mounts" and "doctor: warns
about disabled critical mount /usr" tests failed in CI but
passed locally.
Root cause:
- _doctor_check_bwrap_mounts in scripts/doctor.sh resolves
the config dir via ${XDG_CONFIG_HOME:-$HOME/.config}/Claude
- The test setup() only sandboxes HOME via TEST_TMP
- GitHub Actions runners export XDG_CONFIG_HOME ambient
- Function reads the runner's real config dir, not the test
fixture, and silently emits no output
- Assertions on /opt/tools, WARN, etc. fail
Surfaced by PR #520 wiring BATS into CI for the first time;
the bug existed before but was hidden by the suite never
running.
Fix: unset XDG_CONFIG_HOME in setup() so the function falls
back to \$HOME/.config (which is sandboxed). Comment in the
file documents why HOME alone is insufficient.
Verified: 186/186 pass with XDG_CONFIG_HOME set ambient
(reproduces CI env).
* chore(monitoring): hourly Quad9 DNS check for pkg.claude-desktop-debian.dev
Adds a workflow that fires hourly via cron, runs `dig +short` against
Quad9 (9.9.9.9), and appends a result line to the body of issue #524.
On the first successful resolution, the workflow tags @aaddrick and
self-disables via `gh workflow disable`.
Includes workflow_dispatch so the check can be triggered on demand
without waiting for the next cron tick. Token scope is the default
GITHUB_TOKEN with issues:write + actions:write.
Refs #521#524
Co-Authored-By: Claude <claude@anthropic.com>
* chore(dns-monitor): pass step output through env, not bash interpolation
Routing `steps.dig.outputs.line` through `env:` matches the pattern
used by `apt-repo-heartbeat.yml` and avoids interpolating arbitrary
text directly into the shell command.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Swaps the two markdown blocks so the apt scheme-downgrade signpost is
the first thing a user sees when they open the bug template — the
privacy notice still renders, just below it.
Co-Authored-By: Claude <claude@anthropic.com>
The .wrangler/ directory is a Cloudflare Wrangler tool cache (local
dev sessions, build cache, simulated KV/D1 state) that's regenerated
on demand by `wrangler dev` / `wrangler deploy`. Cloudflare's docs
recommend gitignoring it. Currently shows up as untracked after any
local Worker work — quieting the `git status` noise.
Co-authored-by: Claude <claude@anthropic.com>
Adds a contact_link on the issue chooser that surfaces the apt
scheme-downgrade symptom verbatim and links the README migration
section, plus a markdown callout at the top of bug_report.yml with
the inline sed one-liner. Catches reports like #516 and #519 before
they're filed as bugs.
Co-authored-by: Claude <claude@anthropic.com>
Top-of-README callout swapped from the EXPERIMENTAL Cowork Mode
summary to a short pointer advisory for existing APT users whose
sources.list still targets aaddrick.github.io.
Rationale: the cowork block is accurate but describes routine
operational behavior; the migration advisory describes an imminent,
user-visible break on next `apt update` that needs action. The
detailed migration instructions live in the Installation section
(#510), so this callout is just a pointer, not duplication.
DNF users don't need to do anything (DNF follows the downgrade
silently); called out explicitly to avoid unnecessary sed-by-reflex.
Refs #493
Phase 5 docs follow-up to #493. The plan doc was deleted in #511;
this replaces it with a learnings file aimed at future maintainers
(and future-me) rather than a design spec.
docs/learnings/apt-worker-architecture.md covers:
- The problem (100MB push cap) and why other fixes were rejected
- Redirect chains for both legacy github.io users and direct
pkg.<domain> users
- Why raw.githubusercontent.com is the origin (Pages 301 loop)
- Why Pages emits http:// (no cert, and why the cert can't be had)
- File map (worker source, wrangler.toml, deploy workflow, heartbeat)
- Credential ownership (Cloudflare account, registrar, API token scopes)
- Heartbeat failure runbook — 5 ordered steps to work through
- Rollback paths and documented fallbacks if Cloudflare becomes
unavailable
- Known gotchas including the smoke-test-URL-is-intentionally-github.io
note so future cleanup passes don't "fix" it
CLAUDE.md gains:
- Link to the new learnings file in the Learnings list
- New "Distribution" section under CI/CD with a one-paragraph summary
and pointers to the key files
Refs #493
The plan doc served its purpose through #494 (merge) → #498
(scaffolding) → #502 / #503 / #504 / #506 / #509 / #510 (cutover).
v2.0.5+claude1.3883.0 is the first release through the new pipeline,
verified end-to-end on five distros. #493 is closed.
Removes docs/worker-apt-plan.md and the two architecture-pointer
comments in worker/src/worker.js and worker/wrangler.toml that
referenced it. Both files now carry a short self-contained summary
of what the Worker does and why.
Also corrects worker.js's CDN-hostname reference from
objects.githubusercontent.com (the old name) to release-assets
(current, matches #509's regex fix).
Git history retains the full plan doc for anyone who needs the
design rationale; nothing is actually lost.
Phase 4a-APT cutover (#493, #503) moves binary distribution behind a
Cloudflare Worker at pkg.claude-desktop-debian.dev. The Worker serves
repo metadata directly and 302-redirects .deb/.rpm requests to GitHub
Release assets, which makes the >100 MB .deb push cap irrelevant.
GitHub Pages auto-301s legacy aaddrick.github.io/claude-desktop-debian
URLs to pkg.claude-desktop-debian.dev, but the redirect uses http://
(Pages has no cert for pkg.<domain> — DNS points at Cloudflare, so
Pages can never pass domain verification). apt refuses that scheme
downgrade as a security policy, so existing users' sources.list
silently breaks on the next `apt update`. DNF accepts the downgrade
and keeps working.
Changes:
- README.md: install snippets (APT + DNF) now point at
pkg.claude-desktop-debian.dev directly. New users never touch the
Pages redirect chain.
- README.md: add a "Migrating from the old aaddrick.github.io URL"
section with sed one-liners for existing users + a short background
paragraph explaining why the change was needed.
- .github/workflows/ci.yml: release-notes install snippets (APT + DNF,
both branches) and the generated claude-desktop.repo file's baseurl
and gpgkey all point at pkg.<domain>. Smoke-test chain walkers
deliberately keep starting at github.io (they test the full 3-hop
Pages→Worker→Releases chain for clients that do follow the
downgrade, like curl-without-L and dnf).
Refs #493, #503
v2.0.4 rerun of update-apt-repo made it past hops 0 and 1 (the smoke
test scheme fix in #506 worked — Pages' http:// redirect no longer
trips the chain walker), but failed on hop 2:
Hop 2: 302 .../releases/download/v2.0.4+claude1.3883.0/...deb
-> https://release-assets.githubusercontent.com/...
::error::Hop 2 mismatch: expected https://objects\.githubusercontent\.com/,
got https://release-assets.githubusercontent.com/...
GitHub migrated the Release asset CDN from objects.githubusercontent.com
to release-assets.githubusercontent.com (both have been serving in the
past; release-assets is the current canonical hostname). Accept either
hostname via alternation.
Verified against the actual v2.0.4 Release:
$ curl -Is https://github.com/aaddrick/claude-desktop-debian/releases/download/v2.0.4+claude1.3883.0/claude-desktop_1.3883.0-2.0.4_amd64.deb \
| grep -i location
location: https://release-assets.githubusercontent.com/github-production-release-asset/...
Same fix in three sites:
- .github/workflows/ci.yml (update-apt-repo smoke test)
- .github/workflows/ci.yml (update-dnf-repo smoke test)
- .github/workflows/apt-repo-heartbeat.yml (daily heartbeat)
docs/worker-apt-plan.md has historical references to
objects.githubusercontent.com too; those can be updated in a follow-up
docs sweep — the architectural claim (binary bytes flow direct from
GitHub CDN, never through Cloudflare) is unchanged.
Refs #493, #503
* fix: strip CRLF from cowork-plugin-shim.sh during staging
The shim originates from the upstream Windows .exe extract and ships
with CRLF line endings. Bash fails to exec a script with CRLF
shebangs/commands ("$'\r': command not found", syntax errors at
function braces), so on Nix where the installed file is read-only the
Claude Code subprocess crashes immediately and every cowork session
reports `process_crashed`. Debian/AppImage installs inherit the same
CRLF file but bite less often because the shim is only invoked once
cowork is actively used.
Normalise at the single staging point both the deb and Nix paths
read from. The conversion is a no-op on LF-only input, so if upstream
ever switches to LF this patch remains safe.
Fixes#499
Reported-by: @olafkfreund
Co-Authored-By: Claude <claude@anthropic.com>
* test: use read builtin instead of sha256sum | awk
Style guide prefers parameter expansion / bash builtins over forking awk.
Same ground covered: the first whitespace-separated field of sha256sum
output is captured.
Co-Authored-By: Claude <claude@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
* fix(ci): smoke test allows http:// on Pages 301 hop
Phase 4a-APT's first rerun of update-apt-repo succeeded all the way
through strip + push (v2.0.3 metadata is live on gh-pages now), but
the smoke test failed at hop 0:
Hop 0: 301 https://aaddrick.github.io/.../*.deb
-> http://pkg.claude-desktop-debian.dev/.../*.deb
Hop 0 mismatch: expected https://pkg..., got http://pkg...
Pages emits http:// in the Location header because https_enforced is
unsettable on the repo's Pages config: DNS for pkg.<domain> points at
Cloudflare (Worker custom_domain), so Pages can never pass domain
verification to provision its own cert. Cloudflare serves both schemes
for pkg.<domain>, so the http vs https in Pages' redirect is cosmetic
— the chain still terminates correctly.
Relax hop 0's regex in both smoke tests (update-apt-repo,
update-dnf-repo) and the heartbeat workflow to accept https?://.
Later hops stay https-only since GitHub's Release-asset redirects
are always HTTPS.
Failure was the tail-end of run 24836419696's rerun:
https://github.com/aaddrick/claude-desktop-debian/actions/runs/24836419696
Refs #493, #503
* chore: retrigger CI (previous trigger lost to GH flake)
Once the CNAME file is in place on gh-pages (Phase 4a-APT), GitHub
Pages auto-301s all aaddrick.github.io/claude-desktop-debian/* traffic
to pkg.claude-desktop-debian.dev/*. The Worker's origin fetch against
aaddrick.github.io gets 301'd by Pages, the 301 passes through to the
client, the client follows it back to pkg.<domain>, and the Worker
runs again — infinite loop.
Observed immediately after merging #503 and Pages finishing the CNAME
build:
$ curl -I https://pkg.claude-desktop-debian.dev/dists/stable/InRelease
HTTP/2 301
location: http://pkg.claude-desktop-debian.dev/dists/stable/InRelease
x-github-request-id: 3C94:286425:...
x-served-by: cache-yyz4566-YYZ
via: 1.1 varnish
(Scheme-downgrade to http is a separate Pages quirk when
https_enforced=false, which is the case here because DNS points
at Cloudflare, not Pages, so Pages can't provision a cert.)
raw.githubusercontent.com serves the same gh-pages branch content
without Pages' routing layer. All five metadata paths verified to
return 200:
/dists/stable/InRelease
/dists/stable/main/binary-amd64/Packages
/KEY.gpg
/rpm/x86_64/repodata/repomd.xml
/rpm/x86_64/repodata/repomd.xml.asc
Also fixes the deploy-worker.yml post-deploy probe which still
hardcoded pkg-staging. That's what made #503's deploy show as
failed in the Actions UI even though the wrangler deploy itself
succeeded — route bound and Worker live, but the probe was
resolving a hostname wrangler had just removed.
Refs #493, #503
Co-authored-by: Claude <claude@anthropic.com>
Phase 2 container validation passed against
pkg-staging.claude-desktop-debian.dev — APT (debian:stable,
ubuntu:24.04, debian:testing) and DNF (fedora:latest, rockylinux:9)
both install the current pool version via the Worker chain. The one
remaining failure is #500's sha256 mismatch on RPM download, and
PR #502's gh release upload --clobber fix runs on the next release
that reaches update-dnf-repo.
This flip binds the Worker to pkg.claude-desktop-debian.dev. Once
this is deployed, the strip step's liveness probe in update-apt-repo
and update-dnf-repo will start succeeding, stripping .debs/.rpms from
the local pool tree before push — the original #493 blocker.
Pre-merge checklist (manual, outside this PR):
1. Add CNAME file containing pkg.claude-desktop-debian.dev to the
gh-pages branch root (via Pages settings UI or direct push).
2. Wait for GitHub Pages cert provisioning. Typical ~1h; verify in
repo Settings > Pages that the green cert indicator shows.
3. Merge this PR. CI deploys the Worker to the new route via
deploy-worker.yml.
4. Confirm production probe responds:
curl -fsI https://pkg.claude-desktop-debian.dev/dists/stable/InRelease
5. Re-run the failed update-apt-repo + update-dnf-repo jobs from the
v2.0.3+claude1.3883.0 run (gh run rerun 24836419696 --failed) —
this simultaneously validates #500's fix and completes the v2.0.3
release for apt/dnf users.
Rollback: remove the CNAME file from gh-pages, unbind the Worker
route via the Cloudflare dashboard. gh-pages .deb assets from the
pre-strip history still exist and serve directly via github.io.
Refs #493, #500
Co-authored-by: Claude <claude@anthropic.com>
Fix#500: rpmsign --addsign mutates RPMs in place, so the Release
asset uploaded by the release job (unsigned) diverged from the
signed copy in gh-pages. The Worker redirects to the Release asset,
so dnf saw a sha256 that didn't match repodata. Re-upload the signed
RPMs to the Release via gh release upload --clobber after signing.
Fix#501: The imported GPG keyring contains two keys; reprepro signs
InRelease with one and rpmsign signs repomd.xml.asc with the other,
but the published KEY.gpg only contained one of them. Strict clients
like rockylinux:9 rejected repo metadata with "Bad GPG signature".
Export the full keyring (all public keys) to KEY.gpg so both
signatures verify.
Validation (per issue reproduction steps):
- Re-run update-dnf-repo on a test tag
- sha256 of gh-pages RPM must match the Release asset download
- fedora:latest dnf install should succeed (was "All mirrors tried")
- rockylinux:9 dnf makecache should succeed (was "Bad GPG signature")
Co-authored-by: Claude <claude@anthropic.com>
* feat: APT/DNF Worker scaffolding (#493)
Adds the implementation scaffolding for the Cloudflare Worker that
fronts the APT/DNF repo, per docs/worker-apt-plan.md.
New files:
- worker/src/worker.js: redirects /pool/.../*.deb and /rpm/*/*.rpm
to GitHub Release assets via 302; passes metadata through to
the gh-pages origin
- worker/wrangler.toml: bound to pkg-staging.claude-desktop-debian.dev
initially; Phase 4a switches to pkg.claude-desktop-debian.dev
- .github/workflows/deploy-worker.yml: deploys Worker on worker/**
push, post-deploy probe verifies route bound + Worker responding
- .github/workflows/apt-repo-heartbeat.yml: daily cron, deb+rpm
matrix, walks ordered redirect chain + size match against Releases
asset, opens format-specific tracking issue on failure (auto-close
on recovery), gates on Worker liveness (skips silently before
Phase 4a)
Modified:
- .github/workflows/ci.yml: gated strip step + ordered-chain smoke
test added to update-apt-repo and update-dnf-repo; the destructive
strip only fires when the production Worker probe succeeds, so this
PR can land before Phase 4a without affecting current behavior
- docs/worker-apt-plan.md: bake in real domain values, mark Decisions
table entries as concrete, fix Cloudflare API token permissions
list (current names: Workers Scripts Edit, Account Settings Read,
Workers Routes Edit; previous "Zone:Zone:Read" name no longer
matches the dropdown)
Pre-Phase-4a behavior: the strip step's liveness probe targets the
production hostname which doesn't exist yet, so it always skips and
.debs/.rpms are pushed to gh-pages exactly as today. Smoke tests skip
on the same gate. Heartbeat workflow's gate skips before the Worker
is live. Nothing destructive happens until Phase 4a explicitly cuts
the Worker over to production.
Co-Authored-By: Claude <claude@anthropic.com>
* refactor: simplify worker scaffolding per cdd-code-simplifier review
- worker.js: use named capture group `asset` instead of opaque `m[1]`
positional reference; inline single-use `tagFor()` helper; demote
unused `arch` capture to non-capturing group.
- ci.yml: hoist `WORKER_DOMAIN` from per-step env to job-level env in
both `update-apt-repo` and `update-dnf-repo` (matches the pattern
already used in `apt-repo-heartbeat.yml`).
- apt-repo-heartbeat.yml: use github-script's native `context.serverUrl`
/ `context.runId` instead of reconstructing from process.env; spread
`...context.repo` instead of repeating owner/repo on every API call;
destructure `{ data: open }` to flatten `open.data` references.
All changes preserve behaviour. The contrarian-fix mechanisms (positive
Worker liveness probe gating the strip step, hop-by-hop ordered chain
walk in smoke tests) are unchanged. APT/DNF strip + smoke pairs remain
in-place per reviewer-readability preference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds docs/worker-apt-plan.md, the implementation plan for fixing CI
run 24811974733 where update-apt-repo was rejected by GitHub's 100 MB
per-file push cap (.deb is 130 MB after upstream growth + ion-dist).
Approach: Cloudflare Worker on a custom domain fronts existing GitHub
Pages metadata and 302-redirects binary requests to GitHub Release
assets (which CI already publishes successfully). Existing user
sources.list entries preserved via Pages auto-301 from *.github.io to
the custom domain.
Plan went through two contrarian review rounds. Replaces the prior
gh-pages-split-plan.md draft (split-into-separate-repo approach is
no longer needed once .debs stop being committed to gh-pages).
Co-authored-by: Claude <claude@anthropic.com>
The Claude Desktop app registers a custom app:// protocol handler rooted
at process.resourcesPath/ion-dist — that directory holds the static SPA
assets for internal windows like Third-Party Inference setup, Connectors
config, etc. The Windows installer ships ion-dist under lib/net45/resources
but scripts/staging/cowork-resources.sh never copied it into the electron
resources dest, so every app://localhost/* request fell through to the
static file handler's index.html fallback, which also failed because the
whole directory was missing.
Net effect: the Third-Party Inference setup window (Developer → Configure
Third-Party Inference…) opened as a blank window with
ERR_FILE_NOT_FOUND / ERR_UNEXPECTED on loadURL.
Add an ion-dist copy step to copy_cowork_resources() matching the
existing smol-bin / plugin-shim pattern (warn-and-continue on absence),
and a matching install stanza in nix/claude-desktop.nix so NixOS users
get the fix too — without the Nix hunk, ion-dist lands in the
nix-resources/ sentinel but the installPhase doesn't cherry-pick it.
Verified end-to-end:
- Fresh build ./build.sh --build appimage picks up ion-dist from the
1.3883.0 Windows installer (84 MB uncompressed, ~42 MB delta in the
AppImage after squashfs).
- Live install on CachyOS daily-driver; Developer → Configure Third-Party
Inference… now renders the full 6-tab config UI (Connection / Sandbox
& workspace / Connectors & extensions / Telemetry & updates / Usage
limits / Plugins & skills / Egress Requirements) per the upstream
docs at support.claude.com/en/articles/14680741.
- Logs clean of ERR_UNEXPECTED / ERR_FILE_NOT_FOUND on setup-desktop-3p.
Fixes#488
Co-authored-by: Claude <claude@anthropic.com>
* feat: add BATS unit tests for launcher-common.sh
scripts/launcher-common.sh is 798 lines handling critical startup logic —
display detection, Electron arg building, stale lock cleanup, cowork daemon
cleanup, and the full --doctor diagnostic system — but had zero test coverage.
A regression in any of these functions could silently break app launches across
display servers and package formats.
Add 48 BATS tests covering:
- setup_logging / log_message (XDG_CACHE_HOME fallback)
- detect_display_backend (X11, Wayland, XWayland, Niri auto-detect)
- build_electron_args (all display × package-type combinations)
- setup_electron_env (ELECTRON_FORCE_IS_PACKAGED, title bar)
- cleanup_stale_lock (dead PID removal, live PID preservation)
- cleanup_stale_cowork_socket (stale unix socket removal)
- Doctor helpers:
- _pass / _fail / _warn / _info output
- _cowork_distro_id
- _cowork_pkg_hint (distro-specific package mapping)
- _electron_version
Tests run fully sandboxed:
- HOME, XDG_CACHE_HOME, XDG_CONFIG_HOME, and XDG_RUNTIME_DIR redirected to a temp directory
- Host display variables cleared in setup() to prevent state leakage
* refactor: extract has_electron_arg helper to reduce test boilerplate
Replace repeated loop-and-flag patterns across 7 build_electron_args
tests with a shared has_electron_arg helper that supports glob matching.
Removes ~40 lines of duplicated code with no change in test coverage.