Commit Graph

703 Commits

Author SHA1 Message Date
Sum Abiut
b676519c58 test: add headless launch + --doctor smoke tests for AppImage artifact (#592)
The AppImage artifact test only validated package structure (extraction,
AppDir layout, asar contents) — runtime regressions like frame-fix-wrapper
syntax errors, bad asar patches, or Electron startup crashes silently
passed CI. The .deb path already ran `--doctor` as a smoke check; the
AppImage path now has parity plus a 10s headless launch under Xvfb.

`setsid` + `kill -- -PGID` is load-bearing: xvfb-run's EXIT trap leaks
Xvfb on signal kill, so running the whole stack in its own process group
lets the teardown reap xvfb-run, Xvfb, dbus, AppRun, electron, and zygote
children together. `procps` (for pkill), `dbus-x11`, and `xvfb` added to
the CI apt line.

The headless probe catches main-process startup failures only — GPU /
renderer-process crashes like #583 leave the main process alive and pass
this check; that scope disclaimer is inlined at test-artifact-appimage.sh
lines 114-117 so future contributors don't try to claim #583 coverage by
switching Xvfb off.

Co-authored-by: Sum Abiut <sabiut@users.noreply.github.com>
2026-05-16 10:15:39 -04:00
Sum Abiut
4b2b1d3390 ci: add concurrency group to test-flags workflow (#606)
Prevents manual workflow_dispatch invocations from stacking on the
same ref. Uses cancel-in-progress: false to match ci.yml so a
reusable workflow_call invocation inside an in-flight CI run isn't
killed when a new push lands.

Co-authored-by: Sum Abiut <sum.abiut@titanfx.com>
2026-05-16 20:31:02 +11:00
github-actions[bot]
d50e5c366e Update Claude Desktop download URLs to version 1.7196.1
Updated download URLs resolved from official redirect endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
v2.0.11+claude1.7196.1
2026-05-16 01:39:50 +00:00
aaddrick
b017c72e8f docs(readme): credit Hayao0819 for About window titleBarStyle diagnosis and fix (#481, #489)
Co-Authored-By: Claude <claude@anthropic.com>
2026-05-15 18:53:42 -04:00
Aaddrick
25abb00e61 Merge pull request #489 from Hayao0819/fix/about-window-hiddenInset
fix: handle upstream titleBarStyle change for About window
2026-05-15 18:52:28 -04:00
aaddrick
d632fdb253 fix: catch About window after upstream titleBarStyle change, guard Hardware Buddy (#481)
Upstream migrated the About window from titleBarStyle:"" to
titleBarStyle:"hiddenInset" (build-reference/.../index.js:524746).
isPopupWindow() stopped matching it, so About got frame:true and the
main-window resize handlers, breaking its render on GNOME/X11.

Picks up @Hayao0819's #489 with two adjustments on top of his fix:

- Refresh the stale window-kind comment block. All three of the
  documented titleBarStyle values were wrong post-migration; the block
  now reflects current upstream and the Hardware Buddy child modal at
  index.js:536666 that didn't exist when the original comment was
  written.
- Add a 'parent' in options early-return. Hardware Buddy declares
  `parent: Et ?? void 0` and shares the About window's titleBarStyle /
  no-minWidth shape -- without the parent check, the extended match
  would strip its frame and attach main-window handlers to it. Same
  failure mode as #481, just on a different window.

Hayao0819's diagnosis and one-line logic extension are preserved.

Fixes #481

Co-Authored-By: hayao <shun819.mail@gmail.com>
Co-Authored-By: Claude <claude@anthropic.com>
2026-05-14 07:24:23 -04:00
aaddrick
04f9a18b69 docs(readme): credit JoshuaVlantis for RPM SUID, autoUpdater no-op, and node-pty install hardening (#539, #567, #401)
Co-Authored-By: Claude <claude@anthropic.com>
2026-05-14 07:19:45 -04:00
Aaddrick
16f1bc8be1 Merge pull request #598 from JoshuaVlantis/fix/node-pty-install-fail-loudly
fix(node-pty): fail loudly on npm install failure; require gcc/make/python3
2026-05-14 07:14:10 -04:00
Aaddrick
56d22ca97a Merge pull request #596 from JoshuaVlantis/fix/linux-disable-autoupdater-567
fix(linux): no-op autoUpdater on Linux to defend against feed activation
2026-05-14 07:14:06 -04:00
Aaddrick
c9df9e2f2d Merge pull request #595 from JoshuaVlantis/fix/rpm-suid-attr-539
fix(rpm): set chrome-sandbox suid via %attr instead of %post chmod
2026-05-14 06:37:04 -04:00
Aaddrick
5a9c7eb00c Merge pull request #587 from aaddrick/claude/electron-get-durable-fix
fix(deps): fetch electron binary via @electron/get, drop ^41 pin
2026-05-14 06:33:31 -04:00
aaddrick
57cfab8c37 fix(deps): resolve @electron/get from work_dir, not script dir
The helper at scripts/setup/fetch-electron-binary.js was bare
`require('@electron/get')`. Node resolves that relative to the
script's directory and walks up — so it searches
$project_root/scripts/setup/node_modules, $project_root/scripts/
node_modules, $project_root/node_modules. None of those have
@electron/get; the package is installed in $work_dir/node_modules
by setup_electron_asar at install time.

On the current electron@41.5.0 pin this is dormant — the upstream
postinstall populates dist/, the helper short-circuits, and the
broken require() never runs. Caught by validating the helper path
against an electron@42 sandbox build, which is exactly the scenario
the helper exists to handle.

Fix: createRequire(path.join(cwd, 'package.json')) so module
resolution is anchored at work_dir. The temporary package.json that
setup_electron_asar writes is always present at this point.

Co-Authored-By: Claude <claude@anthropic.com>
2026-05-14 06:25:51 -04:00
JoshuaVlantis
8796aa2c82 fix(linux): mask thenable / coercion traps on autoUpdater no-op Proxy
The chainable-Proxy get-trap returned chainNoop for every property,
including `then`. V8's thenable check calls `then(resolve, reject)`
on anything with a function-typed `.then`, so `await someAutoUpdaterExpr`
or `Promise.resolve(autoUpdater).then(...)` invoked chainNoop with the
resolve/reject pair, got the Proxy back, and the await hung forever
with no error and no Sentry breadcrumb.

Verified against the actual Proxy in node:20-alpine: three await
scenarios (`await proxy`, `Promise.resolve(proxy).then(...)`,
`await proxy.checkForUpdates()`) all hung for the full 1500ms timeout.
After this change all three resolve to the Proxy itself, and existing
chain behavior (`.on().once().setFeedURL().checkForUpdates()`,
`emit.bind(proxy)`) keeps working.

Also masks `Symbol.toPrimitive` and `Symbol.iterator` so string
coercion / `for..of` / spread fail loudly rather than feeding the
Proxy back through V8's primitive- or iterator-protocol machinery.

Comment block updated to note the get-trap shadows reads but lets
writes land on the target.

Addresses review feedback on #567.
2026-05-14 12:18:34 +02:00
JoshuaVlantis
b0be17dd36 fix(deps): dedupe packages mapped from multiple commands
The dependency map sends gcc/g++/make all to build-essential and
wrestool/icotool both to icoutils. The previous loop appended once
per missing command, so a clean machine printed:

  System dependencies needed:  p7zip-full wget icoutils icoutils
    imagemagick build-essential build-essential build-essential python3

apt dedupes internally so the install worked, but the log line read
as if the script was broken. Skip the append when the package is
already in deps_to_install; the bordered substring match handles
both build-essential and icoutils with a single rule.

Verified in a clean debian:stable-slim container with no build tools
present: build-essential and icoutils now appear exactly once in
each invocation.

Addresses review feedback on #401.
2026-05-14 12:17:53 +02:00
aaddrick
3b86003a6b fix(deps): pin electron@41.5.0 to match upstream app.asar ABI
Re-add the exact Electron version pin that #586 had, hoisted to a
named local in scripts/setup/dependencies.sh so the next bump is
intentional. Upstream Claude Desktop's app.asar binds to a specific
V8/NAPI ABI, Chromium pairing, and node-pty native surface; running
a different Electron major against it is unsupported and was the
reproducibility hole left when this PR dropped the ^41 pin.

Also:
- Wrap node fetch-electron-binary.js in a 2-attempt retry so a
  transient 503 from electron's CDN doesn't red the whole build.
- Whitelist supported archs (x64, arm64, armv7l, ia32) with an
  actionable error in fetch-electron-binary.js, instead of 404'ing
  on electron's release server for exotic-arch hosts.
- Document ELECTRON_MIRROR / ELECTRON_CUSTOM_DIR in docs/BUILDING.md
  and clarify that ELECTRON_CACHE is NOT honored by @electron/get
  (the original PR body's claim was wrong).

Refs #584. Addresses self-review feedback on #587.

Co-Authored-By: Claude <claude@anthropic.com>
2026-05-14 06:04:42 -04:00
Aaddrick
b23e0aea12 Merge pull request #585 from aaddrick/feature/583-gpu-mitigations
launcher+doctor: GPU FATAL mitigations (mitigates #583)
2026-05-14 05:26:38 -04:00
github-actions[bot]
755f283431 Update Claude Desktop download URLs to version 1.7196.0
Updated download URLs resolved from official redirect endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
v2.0.10+claude1.7196.0
2026-05-13 01:41:37 +00:00
JoshuaVlantis
cf085711f2 docs(test): broaden chrome-sandbox suid guard comment
Reframe the assert_setuid comment from "guards against the old %post
chmod pattern" to "guards against any regression that strips the suid
bit" — including but not limited to a %post chmod revert.

The assertion itself catches any loss of the setuid bit on
chrome-sandbox, not just the specific %post chmod regression path.
Per review feedback on #595.
2026-05-11 07:32:12 +02:00
github-actions[bot]
fa8f3441c0 chore: update flake.lock 2026-05-11 03:21:34 +00:00
JoshuaVlantis
3db7866e69 fix(node-pty): fail loudly on npm install failure; require gcc/make/python3
Two complementary changes that close the silent-failure path surfaced
during testing for #401.

1. install_node_pty: when `npm install node-pty` fails, abort the build
   with an explicit error message that names the likely cause (missing
   C/C++ toolchain or python3) and the per-distro fix command. Previously
   the function printed a one-line warning and continued, which left
   pty_src_dir empty, skipped the entire copy block, and shipped the
   upstream Windows node-pty binaries unchanged — exactly the failure
   mode in #401.

2. check_dependencies: add gcc, g++, make, python3 to the dependency
   check so the build environment is auto-provisioned before
   install_node_pty runs (build-essential on Debian/Ubuntu, gcc /
   gcc-c++ / make / python3 on Fedora). Skipped when --node-pty-dir
   is set (Nix and explicit overrides bring their own pre-built node-pty).

Together: the dep check makes the failure rare, and the install_node_pty
abort makes the rare failure obvious instead of silent.

Verified two ways:

- Isolated test: forced npm to a fake binary that exits 1, called
  install_node_pty, confirmed exit code 1, error message contains the
  Debian/Fedora install hints, and the staging copy block did not run.
- Full deb build: ran build.sh in an ubuntu:24.04 container with no
  pre-installed compilers (only ca-certificates, wget, sudo, nodejs,
  npm). check_dependencies ran `apt install ... build-essential`
  automatically; npm install node-pty subsequently compiled cleanly;
  shipped pty.node is ELF 64-bit LSB shared object, x86-64.
2026-05-09 17:02:04 +02:00
JoshuaVlantis
d5a4104684 fix(linux): no-op autoUpdater on Linux to defend against feed activation (#567)
Wrap require('electron').autoUpdater on Linux with a Proxy that no-ops
every method and returns chainable stubs for EventEmitter calls. The
bundled app's lii() gate sets a feed URL of api.anthropic.com/api/desktop/linux/
when app.isPackaged is true (which we set unconditionally via
ELECTRON_FORCE_IS_PACKAGED), and registers update-check listeners.
Today this is a happy accident: Electron's Linux autoUpdater is
unimplemented and the calls log "AutoUpdater is not supported on Linux"
and no-op. If a future Electron implements it, every install would
start hitting that feed and would either 404 or — worse — receive
content the install wasn't prepared for. .deb/.rpm/AppImage updates
flow through the OS package manager (or AppImageUpdate); the Anthropic
feed has no Linux artifacts.

The Proxy is installed via the existing Module.prototype.require
interceptor, so it covers gA = require("electron"); gA.autoUpdater.X()
and any equivalent destructure. getFeedURL returns '' so any code
that inspects the URL gets a well-typed empty string.

Verified: built deb in ubuntu:24.04 container, extracted shipped
app.asar, confirmed autoUpdaterNoop block + intercept present in
frame-fix-wrapper.js. Runtime test loaded the shipped wrapper with a
mock electron whose autoUpdater records every real call; replayed
the bundled app's setFeedURL/on/checkForUpdates/quitAndInstall/
chained .on() patterns — zero real calls were recorded, confirming
the Proxy intercepts every access path.
2026-05-09 14:07:13 +02:00
JoshuaVlantis
15813ca11f fix(rpm): set chrome-sandbox suid via %attr instead of %post chmod (#539)
Move the suid bit on chrome-sandbox into the rpm spec's %files section
via %attr(4755, root, root). The previous %post chmod 4755 only ran on
fresh installs and silently regressed when the scriptlet was skipped
(e.g., --noscripts), leaving a non-suid chrome-sandbox that breaks
sandboxing on every launch.

Also add an assert_setuid helper to tests/test-artifact-common.sh and
wire it up in test-artifact-rpm.sh so a future spec regression to the
old %post pattern fails CI rather than shipping silently.

Verified: built rpm in fedora:42 container, installed via dnf,
ls confirms -rwsr-xr-x on chrome-sandbox, %post no longer chmods.
2026-05-09 14:06:55 +02:00
github-actions[bot]
429d191f77 Update Claude Desktop download URLs to version 1.6608.2
Updated download URLs resolved from official redirect endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
v2.0.10+claude1.6608.2
2026-05-09 01:39:39 +00:00
github-actions[bot]
ab5636ef29 Update Claude Desktop download URLs to version 1.6608.0
Updated download URLs resolved from official redirect endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
v2.0.10+claude1.6608.0
2026-05-08 01:40:45 +00:00
github-actions[bot]
0d67646d21 Update Claude Desktop download URLs to version 1.6259.1
Updated download URLs resolved from official redirect endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
v2.0.10+claude1.6259.1
2026-05-07 01:40:18 +00:00
Claude
cf64b78611 fix(deps): fetch electron binary via @electron/get, drop ^41 pin
The hotfix in #586 pinned electron to ^41 because electron@42.0.0
removed the postinstall that fetches the prebuilt binary into
node_modules/electron/dist/. That left us tethered to electron 41.x
and would re-break whenever 41 ages out of npm or the upstream
behavior shifts again.

This change does the binary fetch ourselves so we no longer depend on
electron's postinstall:

* New scripts/setup/fetch-electron-binary.js uses @electron/get to
  download the matching prebuilt binary for the host platform/arch
  and extract-zip to unpack it into dist/. Reads the version from
  the installed electron's package.json so it always matches what
  npm resolved.
* setup_electron_asar runs the helper only when npm install leaves
  dist/ missing. If electron later restores the postinstall, this
  is a no-op.
* Drops the 'electron@^41' pin so we follow latest electron again.
* @electron/get honors ELECTRON_MIRROR / ELECTRON_CACHE, so users
  behind proxies/mirrors keep working without further changes.

Refs #584

Co-Authored-By: Claude <claude@anthropic.com>
2026-05-06 13:18:04 +00:00
Aaddrick
f7c4daeb89 fix(deps): pin electron to ^41 to restore postinstall binary fetch (#584) (#586)
electron@42.0.0 (published 2026-05-06) removed the postinstall script
that downloads the prebuilt binary into node_modules/electron/dist.
Without dist/ the existence check in setup_electron_asar fails and the
build aborts with "Failed to find Electron distribution directory".

Pin to electron@^41 as a hotfix to unblock source builds. A durable
fix using @electron/get to fetch the binary explicitly will land
separately so we no longer depend on electron's postinstall behavior.

Refs #584

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-05-06 09:15:12 -04:00
aaddrick
51e0bc7acd test(launcher-common): cover CLAUDE_DISABLE_GPU in log_session_env
CI fail on PR #585: the existing log_session_env block test did
exact-line matching on the env block contents, so adding
CLAUDE_DISABLE_GPU to log_session_env's key list (88676f4) shifted
the closing '}' index and broke both block tests.

Updates both tests in launcher-common.bats:
- "all required keys" — sets CLAUDE_DISABLE_GPU=1, asserts new line
  at index 11, '}' moves to index 12
- "unset/empty values render as KEY=" — asserts the new key emits
  empty form at index 11

70/70 launcher-common.bats pass locally.

Co-Authored-By: Claude <claude@anthropic.com>
2026-05-06 08:31:44 -04:00
aaddrick
368f83490e doctor: surface recent Electron crashes with #583 pointer
Adds _doctor_check_recent_crashes, called from run_doctor before the
log-file section. When systemd-coredump shows ≥3 Electron crashes in
the last 7 days, surfaces a [WARN] with two workarounds (Settings
toggle, CLAUDE_DISABLE_GPU=1) and a link to the tracking issue.

Filters by the caller-supplied electron_path when entries match, falls
back to all-electron entries with a footnote when they don't (covers
AppImage's transient mount paths and other Electron apps installed
side-by-side).

Silent when coredumpctl isn't on PATH (non-systemd hosts), when there
are zero matches, or when the count is below threshold.

Co-Authored-By: Claude <claude@anthropic.com>
2026-05-06 08:28:25 -04:00
aaddrick
88676f44a6 launcher: add CLAUDE_DISABLE_GPU=1 opt-in (#583)
Mitigation for the Chromium GPU process FATAL exhaustion tracked in
#583. CLAUDE_DISABLE_GPU=1 adds --disable-gpu and
--disable-software-rasterizer to Electron's argv, providing the same
effect as the upstream Settings → disable hardware acceleration toggle
but persistable via the environment.

Co-occurrence with the existing XRDP block does not stack duplicate
flags: a single _disable_gpu sentinel gates the args+= push.

CLAUDE_DISABLE_GPU joins the other CLAUDE_* keys logged by
log_session_env so bug reports include the value.

This is a workaround, not a fix. The underlying Electron/Chromium GPU
process lifecycle issue remains tracked at #583.

Co-Authored-By: Claude <claude@anthropic.com>
2026-05-06 08:28:17 -04:00
github-actions[bot]
a2411b8928 Update Claude Desktop download URLs to version 1.6259.0
Updated download URLs resolved from official redirect endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
v2.0.10+claude1.6259.0
2026-05-06 01:39:56 +00:00
Alexis Williams
59ec0c6918 fix(nix): make electron binary executable (#581) 2026-05-05 16:49:14 -04:00
Aaddrick
8882f0fe26 fix(cowork.sh): WARNING on Patch 2a/2b inner anchor miss (#576)
D5 of #559's followup. Patch 2a (vmClient log gate, line 107) and
Patch 2b (vm module assignment, line 123) had no else branch on the
inner anchor regex. A miss silently ships a half-patched asar — the
exact PR #555 failure mode that took hours to diagnose because the
build log printed "Applied 10 cowork patches" with no warning.

Three-branch pattern matches Patch 4b at line 227-234:
- regex matches: patch + log + count
- post-patch literal already in code: "already applied"
- otherwise: WARNING naming the patch site

Empirically validated against the pinned 1.5354.0 installer:
deliberately broke the 2a anchor (replaced "vmClient" with "XXMISSXX"
in the regex) → WARNING fires, verify-cowork-patches.sh from PR #575
catches missing vmclient-log-gate marker. Same for 2b. Baseline
unchanged: no new WARNINGs on a fresh upstream.

Refs #559. Builds on #575 (D6 verification scaffolding).

Co-authored-by: Claude <claude@anthropic.com>
2026-05-05 07:32:46 -04:00
Aaddrick
9df8b88e3a verify(cowork): static-grep shipped asar for PR #555 markers (#559) (#575)
* verify(cowork): static-grep shipped asar for PR #555 markers

D6 of #559's followup plan: post-build check that greps the shipped
app.asar for 9 known cowork patch markers and exits non-zero if any
are missing. Catches the half-patched-asar failure mode from PR #555,
where two of three failed gates had no else branch and the build log
showed "Applied 10 cowork patches" instead of warning.

- scripts/cowork-patch-markers.tsv: single source of truth.
  Tab-separated name<TAB>pcre<TAB>sample. Both verify and BATS read it.
- scripts/verify-cowork-patches.sh: accepts a .js, an .asar (npx
  @electron/asar extract), or a directory containing
  app.asar.contents/.vite/build/index.js. Exits 0/1/2.
- tests/verify-cowork-patches.bats: regex-matches-sample integrity,
  positive full fixture, per-marker negative fixtures, input-shape
  coverage. 9 new BATS cases.
- .github/workflows/build-amd64.yml: runs verify against the deb
  build's asar. Pinned to deb because the patched JS is identical
  across formats.

Validated end-to-end against the pinned 1.5354.0 installer:
unpatched -> 9/9 miss; cowork.sh patched -> all 9 present.

Refs #559.

Co-Authored-By: Claude <claude@anthropic.com>

* verify(cowork): share TSV parser between verify.sh and BATS

Realises the library-mode plumbing the previous commit added but
didn't use: BATS now sources verify-cowork-patches.sh and calls
load_markers, so a TSV format change cannot desync the two consumers.
Drops the duplicate parser in tests/verify-cowork-patches.bats.

Also tightens main()'s loop (for over indexed while, drop redundant
missing counter) and the BATS index loops.

Behaviour-preserving; bats tests/verify-cowork-patches.bats still 9/9.

Co-Authored-By: Claude <claude@anthropic.com>

* rename: verify-cowork-patches → verify-patches (generic)

Rename the verify infra to make its generic intent explicit. Per
sabiut's review note on #575, the script + TSV are reusable for
non-cowork patch sets in principle — drop "cowork" from the script
and BATS filenames to reflect that, and accept an optional second
arg for the marker TSV path so other patch sets can plug their own
TSV in without forking the script.

The TSV itself stays cowork-specific (`cowork-patch-markers.tsv`)
because its contents are cowork markers; the script defaults to it
so existing CI keeps working without changes beyond the rename.

Routing implication noted by sabiut: filename now lives under
`/tests/` → @sabiut codeowner mapping (intentionally; the verify
infra is generic). Cowork-specific marker changes still touch the
TSV under `/scripts/`, which routes to @aaddrick/@RayCharlizard via
the cowork-* CODEOWNERS rule.

Co-Authored-By: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
2026-05-05 07:25:22 -04:00
Aaddrick
ccce3eab37 docs(learnings): add patching-minified-js + CONTRIBUTING (#559) (#574)
PR 1 of 3 for issue #559 — docs and conventions, no behaviour change.

- New `docs/learnings/patching-minified-js.md` covering anchor
  selection, identifier capture (`\w` vs `$`), beautified
  false-negative trap, whitespace tolerance, replacement-string
  escaping, idempotency, multi-site coordination, lastIndexOf
  disambiguation, and the SHA-256-pinned hypothesis-verification
  recipe.
- New `CONTRIBUTING.md` as a navigation hub: scope policy
  (no net-new features outside Linux-environment parity), upstream
  routing, subsystem owners, PR checklist, AI-assisted contribution
  disclosure format, and the patch-script regex intent comment +
  markdown wrapping conventions.
- Fix CLAUDE.md:126 example regex `\w+` → `[$\w]+` (same class of
  bug the new learnings doc documents).
- CLAUDE.md learnings index entry for the new doc.

PRs 2 (`verify-cowork-patches.sh` + BATS) and 3 (silent-no-op
WARNING retrofits) follow.

Refs #559

Co-authored-by: Claude <claude@anthropic.com>
2026-05-05 07:15:42 -04:00
Aaddrick
0efa67d417 doctor: detect IBus/GTK misconfigurations that break input (#572)
* doctor: detect IBus/GTK misconfigurations that break input (#550)

Adds _doctor_check_im_modules helper covering the four input-method
failure modes from #545:

  - ibus-gtk3 package missing while GTK_IM_MODULE=ibus
  - GTK immodules cache stale (active module not listed by
    gtk-query-immodules-3.0 --update-cache fixes it)
  - XWayland session routing IBus through XIM (lossy for some IMEs;
    informational note pointing at CLAUDE_USE_WAYLAND=1 for native
    Wayland IME)
  - CLAUDE_GTK_IM_MODULE override visibility (informational, so
    users can verify the resolved value)

Each check is gated so it only fires when relevant — e.g. the
package check is skipped when GTK_IM_MODULE isn't ibus, the cache
check is skipped when gtk-query-immodules-3.0 isn't installed, and
the package check returns silently on distros without dpkg/rpm/pacman
to avoid false negatives.

Adds tests/doctor.bats with 17 cases covering each gating branch and
the _cowork_pkg_hint mapping for ibus-gtk3 (Arch maps to plain ibus
since it bundles the GTK3 immodule).

Hoists _distro_id resolution to the top of run_doctor so the IM
check and the existing Cowork section share one /etc/os-release
read.

Closes #550. Refs #545, #549.

Co-Authored-By: Claude <claude@anthropic.com>

* doctor: simplify IM-check helper and DRY out doctor.bats setup

Mechanical clean-up of the #550 diff after self-review:

scripts/doctor.sh
  - tighten the _doctor_check_im_modules docblock: drop the "each
    check is gated" paragraph (self-evident in the code) and inline
    the XWayland/XIM rationale into the failure-mode bullet
  - drop the inline section comments that just restated the next
    block's purpose; keep the rc=1/rc=2 comment because the value
    distinction is the load-bearing detail
  - replace the `local _pkg_rc=0; ... || _pkg_rc=$?; if ((_pkg_rc == 1))`
    dance with a `case $?` on the direct call

tests/doctor.bats
  - hoist the `command -v gtk-query-immodules-3.0 → not-found` shim
    into a `_skip_gtk_query` helper (it was duplicated across 11 of
    the 17 cases)
  - default `_pkg_installed() { return 2; }` in setup so per-test
    stubs only appear when the test cares about rc=0 or rc=1
  - drop dead `_skip_gtk_query` calls from cases where the function
    returns earlier (no IM selected, package warn fires) so the
    shim is only present where it actually changes behaviour

No behaviour change — all 17 doctor.bats cases still pass, plus the
68 launcher-common.bats cases. Shellcheck is unchanged from baseline.

Co-Authored-By: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
2026-05-05 07:08:36 -04:00
Aaddrick
023a736f1c launcher: add CLAUDE_GTK_IM_MODULE opt-in override (#571)
* 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>
2026-05-05 06:58:58 -04:00
Aaddrick
3ddfb7353c launcher: log session/IME env block at startup (#570)
* 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>
2026-05-05 06:45:42 -04:00
Aaddrick
3506c14918 test(harness): add Linux compatibility test harness (#579)
Build out a Playwright-based regression-detection harness covering
the compat-matrix surfaces (KDE-W, KDE-X, GNOME, Sway, i3, Niri,
packaging formats). Adds:

- Planning + decision docs under docs/testing/ — README, matrix,
  runbook, automation, cases/ (11 case files), quick-entry-closeout
- Playwright scaffolding (config, tsconfig)
- 78 spec runners under tools/test-harness/src/runners/ — T## case-
  doc runners and S## distribution/smoke runners
- Substrate primitives in tools/test-harness/src/lib/: AX-tree
  loader (snapshotAx + waitForAxNode + axTreeToSnapshot), focus-
  shifter, eipc-registry, niri-native bridge, drag-drop bridge,
  electron-mocks, claudeai page-objects, inspector client

S03 (DEB Depends declared) and S04 (RPM Requires declared) ship
marked test.fail() — they're regression detectors for the case-doc
gap (deb.sh emits no Depends:, rpm.sh sets AutoReqProv: no), and
the expected-failure shape lets them report green on every host
until upstream packaging starts declaring runtime deps.

127 files, no runtime changes; harness is opt-in via
'cd tools/test-harness && npx playwright test'.

Co-authored-by: Claude <claude@anthropic.com>
2026-05-04 23:17:37 -04:00
Liz Fong-Jones
b8e1a1fc30 feat(lifecycle): notify and offer restart on in-place package upgrade (#564)
* 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>
2026-05-04 19:15:44 -04:00
github-actions[bot]
0bbb550421 chore: update flake.lock 2026-05-04 03:19:54 +00:00
Aaddrick
b351d42a2d docs: archive upstream report draft for #546 (filed as anthropics/claude-code#55353) (#552)
* 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>
2026-05-03 12:38:12 -04:00
Aaddrick
b404ebd5f1 docs(learnings): refine mcp-double-spawn root cause and routing (#546) (#547)
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>
2026-05-03 12:37:48 -04:00
Aaddrick
14d04c2dab fix(dnf): set metadata_expire=1h on generated .repo (#551)
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>
2026-05-03 12:37:06 -04:00
aaddrick
fd352f4390 docs(readme): credit jslatten for KDE Wayland desktopName fix (#562)
Co-Authored-By: Claude <claude@anthropic.com>
2026-05-03 08:22:32 -04:00
Justin Slatten
5a98854137 set desktopName for Wayland grouping (#562) 2026-05-03 08:20:09 -04:00
aaddrick
0c99f2119f docs(readme): credit ProfFlow for re-fix of RPM repodata signing (#566)
Co-Authored-By: Claude <claude@anthropic.com>
2026-05-03 07:44:54 -04:00
Niklas
912c04ee1d fix(ci): force primary GPG key for repomd.xml signing (#566)
* 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>
2026-05-03 07:43:30 -04:00
Sum Abiut
b367f8e5cc test: isolate cleanup_stale_cowork_socket BATS from host pgrep state (#534)
* 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
2026-05-02 18:45:30 -04:00
sirfaber
244c08a3bd fix(cowork.sh): allow $ in minified identifier anchors; defensive lastIndexOf (#555)
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>
v2.0.8+claude1.5354.0
2026-05-02 08:53:02 -04:00