Commit Graph

677 Commits

Author SHA1 Message Date
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
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
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
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
Aaddrick
5c8191e82f feat(linux): hybrid titlebar mode for clickable in-app topbar (#538)
* 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>
v2.0.7+claude1.5354.0
2026-05-01 02:47:16 -04:00
Travis
c973f4922b docs(readme): credit cbonnissent for bwrap {src, dst} mount form (#543)
Adds the new bullet under their existing entry for the
coworkBwrapMounts {src, dst} polymorphic form merged in #531,
which closed #530.

Refs #530, #531
v2.0.6+claude1.5354.0
2026-05-01 00:50:02 -04:00
Travis
646a658fc5 docs(learnings): document MCP double-spawn upstream bug (#526) (#527)
* 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>
2026-04-30 23:51:08 -04:00
CyPack
b5339d0f0b fix(doctor): warn on unknown COWORK_VM_BACKEND, document Cowork backend (#324)
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>
2026-04-30 23:09:41 -04:00
Charles Bonnissent
8ac73e6ba9 feat(bwrap): support {src, dst} mount form in coworkBwrapMounts (#531)
* 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>
2026-04-30 09:34:20 -05:00
github-actions[bot]
17db18393e Update Claude Desktop download URLs to version 1.5354.0
Updated download URLs resolved from official redirect endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
v2.0.5+claude1.5354.0
2026-04-30 01:40:21 +00:00
github-actions[bot]
73463cd2cc Update Claude Desktop download URLs to version 1.5220.0
Updated download URLs resolved from official redirect endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
v2.0.5+claude1.5220.0
2026-04-29 01:40:43 +00:00
Liz Fong-Jones
412b267710 fix(autostart): route openAtLogin through XDG Autostart on Linux (#450)
* 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>
2026-04-28 19:02:47 -04:00
Liz Fong-Jones
8530342b2e fix(lifecycle): hide main window to tray on close, Linux (#451)
* 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>
2026-04-28 18:16:56 -04:00
Aaddrick
4cc63bff7a ci: pin third-party actions to commit SHAs (#535)
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>
2026-04-28 07:25:28 -04:00
Andrej730
d4db72865b fix: update visibility function regexp (#496)
* 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>
2026-04-27 09:20:46 -04:00
IliyaBrook
cf2b0fc357 fix: update Linux tray icon in place on OS theme change (#515)
* 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>
2026-04-27 09:19:36 -04:00
Andrej730
31c557acca build.sh: improve regexp quick window patch regexp readibility (#420)
* build.sh: improve regexp quick window patch regexp readibility

* docs(readme): credit @Andrej730 for quick-window regex readability

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

---------

Co-authored-by: aaddrick <aaddrick@gmail.com>
Co-authored-by: Claude <claude@anthropic.com>
2026-04-27 09:18:45 -04:00
Aaddrick
ea9b8aa0ab ci: remove Quad9 DNS monitor (#528)
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>
2026-04-27 07:21:43 -04:00
github-actions[bot]
5304fa145e chore: update flake.lock 2026-04-27 03:18:43 +00:00
Sum Abiut
0217a2c0e1 ci: run BATS test suite on push and PR (#520)
* 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).
2026-04-27 11:48:21 +11:00
Aaddrick
7f4cf49431 chore(monitoring): hourly Quad9 DNS check for pkg.claude-desktop-debian.dev (#525)
* 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>
2026-04-25 10:02:28 -04:00
aaddrick
95b65dd333 chore(issue-template): hoist apt-update callout above the privacy notice
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>
2026-04-25 08:44:25 -04:00
Aaddrick
944fc5a4db chore(gitignore): ignore worker/.wrangler/ cache (#523)
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>
2026-04-25 08:41:53 -04:00
Aaddrick
6dd667cd2b chore(issue-template): funnel apt-update legacy-URL reports to migration docs (#522)
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>
2026-04-25 08:41:37 -04:00
github-actions[bot]
6d281c93b6 Update Claude Desktop download URLs to version 1.4758.0
Updated download URLs resolved from official redirect endpoints.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
v2.0.5+claude1.4758.0
2026-04-25 01:37:46 +00:00
Aaddrick
aecd25a519 docs(readme): replace cowork callout with APT migration advisory (#514)
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
2026-04-23 18:10:33 -04:00
Aaddrick
e86f17bb3e docs: add APT/DNF Worker learnings + CLAUDE.md Distribution section (#512)
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
2026-04-23 16:37:33 -04:00
Aaddrick
9b4f051f09 docs: remove worker-apt-plan.md now that Phase 4a has shipped (#511)
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.
2026-04-23 16:25:56 -04:00
Aaddrick
8bce730056 docs: point install instructions at pkg.claude-desktop-debian.dev (#510)
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
2026-04-23 16:12:05 -04:00
Aaddrick
de19c1bb36 fix(ci): smoke test accepts release-assets CDN hostname (#509)
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
v2.0.5+claude1.3883.0
2026-04-23 11:06:31 -04:00