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