check-issue
Take one or more claude-desktop-extra GitHub bug reports from triage to a shipped fix - read, classify ours vs upstream, reproduce without touching the user's running app, spec with the maintainer, im
SKILL.md.claude/skills/check-issue/SKILL.md
GitHubCheck issue - from bug report to shipped fix
Issues: $ARGUMENTS
Open issues right now: !gh -R patrickjaja/claude-desktop-extra issue list --state open --limit 10 2>/dev/null || echo "(gh not ready)"
Latest release: !gh -R patrickjaja/claude-desktop-extra release list --limit 1 2>/dev/null || echo "(gh not ready)"
Upstream version patches target: !cat .upstream-version 2>/dev/null
The main thread orchestrates. Investigation, implementation, review and live tests run in dispatched workers. Follow the global subagent tier rules: name the tier and the reason in every dispatch.
Gates. Ask before each of these, and treat each approval as covering only its own step:
- the spec
- building or installing (the maintainer does this themselves)
- commit and push
- the release
- every public issue comment
Phase 1 - read everything
for n in <numbers>; do gh issue view $n --json title,author,createdAt,body; gh issue view $n --comments; done
--commentsshows only the comments, never the body. Always fetch the body separately with--json body.- Pull any
--diagnosegist the reporter linked withgh gist view <id> --raw > <scratch>/gist<n>/diagnose.txt. Then grep it for session type, compositor, scale, bridges and versions. - Compare the reporter's version with
.upstream-versionand the installed package (pacman -Q claude-desktop-extra). If the extract intmp/matches the upstream version the reports are against, it can be reused read-only.
Phase 2 - investigate in parallel (read-only)
Dispatch one worker per issue. A contained parsing or logic bug goes to sonnet. Coordinate math, renderer lifecycle, or minified-anchor work goes to opus.
Each brief carries:
- the issue text and the suspected file:line
- "READ-ONLY, no edits, no commits"
- a scratch dir per issue
- this question: ours or upstream?
Ours or upstream:
- Computer Use (
js/cu_*, the bridges) is always ours, because upstream ships no Linux CU. - For anything else:
- Find upstream's code in the pristine bundle (
grep -aacrosstmp/app.asar.contents/.vite/build/index*.js). - Confirm that our patches do not touch that path.
- Search
anthropics/claude-codeissues for the same symptom.
- Find upstream's code in the pristine bundle (
- An upstream bug that only breaks on Linux is still ours to patch. It counts as a functional gap, not a cosmetic one.
Reproduce without the user's running app. The user's Claude is in use: never signal its processes, and identify yours by your own --user-data-dir.
- Renderer, main-process or Electron behaviour: run on a private Xvfb display with
env -iand a throwaway HOME, XDG_RUNTIME_DIR, CLAUDE_CONFIG_DIR and--user-data-dir. Use the installed binary or a scratch patched tree. A hung renderer is simulated withSIGSTOPon the main renderer. - A logged-in scratch instance (cloud session timing, anything that needs the account): copy
~/.config/Claudeminus logs, caches, vm_bundles andbridge-state.jsoninto a 0700 scratch dir and use it as--user-data-dir, pass the realDBUS_SESSION_BUS_ADDRESSso libsecret can decrypt the cookies, and delete the copy afterwards.--remote-debugging-portmakes the app refuse to start; append a probe to the scratch asar'sindex.jsinstead (wrap main-side hooks,executeJavaScriptinto the claude.ai page every few seconds). On Xvfb the CU overlay renders solid black, so the model never clicks anything: good for lock, turn-end and API timing, useless for click accuracy. Upstream re-shows the session page when the lock is released, so a 'page not mounted' run has to re-assert the home after that. - Wayland, wlroots or fractional scale: use headless sway per
/test-native, and run the bridge CLI directly against it. The Wayland socket path must stay under 108 bytes, so use a shortmktemp -d /tmp/xx.XXXXruntime dir rather than the scratchpad. - GNOME portal (gnome-portal-bridge) without a VM: headless nested GNOME on the host (
dbus-run-session -- gnome-shell --wayland --no-x11 --devkit --virtual-monitor WxH, private XDG_RUNTIME_DIR, own xdg-desktop-portal + pipewire), with the consent dialog and "Stop sharing" driven over accessibility/Shell.Eval. Good for bridge-level session tests; it cannot host a logged-in Claude. - VMs: allowed, but never start Cowork inside a VM. Turn off the guest's idle lock before handing a VM to a worker: auto mode rightly refuses to get past a lock screen, so a locked VM stalls the run.
Session types. Upstream's Computer Use lock lives differently per session type: the Code tab and local Cowork release it after every turn, cloud Cowork only after an idle sweep or Stop. Anything of ours tied to the lock (bridge sessions, latches) must be checked against each type, and against the reporter's type from their first comment.
Verify before presenting. Spot-check a worker's key upstream claim in the pristine bundle yourself before calling it the root cause; the maintainer will ask whether it was audited, and whether there is an event to hook instead of a timer. When a report says one of our hooks never fired, check first whether the hook logs on entry: a silent early return (#293 __cuTaskBoundary) makes 'never arrived' and 'arrived too late' look the same. Reading the remote claude.ai SPA needs the Chrome worker: fetch chunks from the page origin with fetch() in javascript_tool (host curl gets 403), and expect its output filter to block raw minified code (swap = & ; ? for other characters to get snippets through).
Fallback paths. When a backend has a fallback path, check which path the reporter's data came from before blaming the primary one. In #298 the bridge was correct and the Electron fallback was not.
Phase 3 - spec, with the maintainer
Load agent-skills:spec-driven-development. Write .agent/SPEC-<slug>.md. The .agent/ dir is gitignored, so never commit it or mention it in tracked docs. The spec contains:
- Capability map: one module per issue, plus its dependencies.
- Ours vs upstream table: one row per issue, with the evidence.
- Per-module design.
- A "Linux compatibility" section. Check it against every row of the
/linuxmatrix. No hard dependency on one distro's or one desktop's tools: optional tools are probed with_hasCmd, and the common path needs none of them. Name the Debian, AppImage and NixOS gaps explicitly. - Commands, testing strategy, boundaries and success criteria.
- A "Rulings during implementation" section, appended as decisions are made.
Ask the real design forks with AskUserQuestion: one question per fork, with the recommended option first. Approval of the spec covers "break into tasks and dispatch". Save the plan and the ledger to .agent/tasks/plan-<slug>.md and .agent/tasks/todo-<slug>.md.
Phase 4 - implement in parallel
File ownership:
- Two tasks that edit the same file each get their own worktree, or else run in sequence. A worktree task leaves its diff uncommitted. The orchestrator then runs
git apply --checkandgit applyon the main tree (-3fails on a dirty index) and copies over any untracked files. - Implementers never touch
CHANGELOG.md,docs/,README.md, orEXPECTED_TEST_HARNESSESinscripts/run-feature-tests.sh. The integration task owns those, so parallel edits cannot conflict. New harnesses run with plainnode <file>until the count is bumped.
Rules every brief must state:
- No
build-*.sh, no package build, no install, no commit. makedoes not trackjs/. After a JS edit,touchthe embedding.nimfile beforemake.- Patch work follows AGENTS.md strictness: every sub-patch must apply, idempotency checks positive end state,
[\w$]+anchors. - A new harness must fail against the old code. Ask for the before/after count.
- A hook on an upstream event logs one line on entry, before any early return, so an empty grep later means the event never arrived.
- A harness that runs the compiled patch is only valid after
makeran AFTER the lastjs/edit (the binary embeds the JS); reviewers rebuild in a scratch copy before trusting counts. - A harness must fire the trigger upstream really uses. If it only calls our own injected hook (e.g. the CU lock hook), it misses upstream paths that bypass that hook: #300 passed ~400 harness cases and a private-display rig, then failed in the first real Code-tab turn.
- Live checks run on scratch trees only.
Reviews:
- Every task gets an independent per-task review.
- Feed the findings back to the same implementer through SendMessage, so it keeps its context.
- Do not re-review small verified fixes.
Phase 5 - integrate and gate
The integration worker (sonnet):
- bumps
EXPECTED_TEST_HARNESSESby the number of new harness files - updates
docs/patches.mdrows and feature docs only where they are now wrong - writes one
## <today>CHANGELOG section, as solutions not history, without blaming our patches and without naming contributors in prose - runs the full local gate:
touch patches/linux/<embedding>.nim && (cd patches && make -j"$(nproc)")
python3 scripts/check-upstream-absorbed.py tmp/app.asar.contents tmp/extract/usr/lib/claude-desktop/resources/ion-dist.asar
bash scripts/check-patch-sibling-noop.sh
bash scripts/run-feature-tests.sh linux # plus core and community
python3 scripts/apply_patches.py patches <scratch copy of tmp/app.asar.contents>
# then: node --check on every patched chunk
Final review. One review covers the whole change across all repos. Run it on fable when 2+ of these hold, and on opus otherwise:
- the change carries risk-bearing semantics
- tasks shared files
- a ruling changed a design decision
- the change is over 500 non-test lines
A worker that ends without a report (the notification says none was delivered) is asked for it with SendMessage; never re-dispatch the work.
Weigh each finding against the live evidence. A reviewer's mechanism claim can be contradicted by a measured run, as finding 1 of the #299 review was. Record accepted findings as rulings in the spec.
Phase 6 - ship
- Local build and live check BEFORE committing (the maintainer usually wants to test first):
build-local.shbundles pre-built bridges from../computer-use/<bridge>/target/x86_64-unknown-linux-musl/release/when present, without checking freshness. Rebuild them (cargo build --release --target x86_64-unknown-linux-musl) after any bridge change, and verify the sha inside the package.- Build only when the maintainer asks; they install (
sudo pacman -U ...). Check that the running app started after the install before testing. - Run the Xvfb repro against
/usr/lib/claude-desktop/claudein a worker. - Exercise Computer Use in a real Code tab session. A plain chat never gets
computer_*tools on Linux, and its "computer use is off" answer is misleading. When the maintainer authorizes it (e.g. while AFK), a worker may drive the real logged-in app with the bundled x11-bridge: back up and restore the settings files, kill only its own processes, never signal Claude, and use a virtual display for anything it starts.- Start the app detached (
setsid -f claude-desktop), never undertimeoutor any wrapper that signals it. Make the maintainer restart the app after every reinstall (a running instance keeps the old asar). Before the first Code-tab turn, expect a "Sign in again" dialog after a long idle; ask the maintainer to log in, do not retry. - Multi-monitor, display names and compositor-vs-toolkit string differences (window titles, app ids) only show up live; the harness cannot. Plan one rebuild round for what the live run finds.
- Bridge-level VM runs (KDE, Hyprland) need no login and are worth it when a backend's coordinate math has only run on a private bus. Run them in parallel with the host test.
- Start the app detached (
- Read
~/.config/Claude/logs/claude-patches.logafterwards to confirm the exact argv or path.
- Bridge repos first. That covers
patrickjaja/{wlroots,x11,gnome,kwin-portal}-bridge(local checkouts usually sit next to this repo, e.g.../computer-use/<bridge>). CI fetches bridge HEAD from GitHub, so push the bridges before the main repo. A new shared crate goes first of all: bridges can only pin its git rev after it is pushed. Auto mode blocks pushes as external writes even after the maintainer's go; commit locally, then hand the maintainer one! git -C <repo> push ...line per repo. - Main repo. Commit to
masterper/commit-fast. A push rejected because CI committed README versions or the Nix hash is safe togit rebase origin/masterand push again; check that the CI commits touch other files first. Reference the issue numbers without closing keywords, so the issues close with the release comment instead. - Release:
gh workflow run build-and-release.yml --ref master -f force_rebuild=true(or/deploy force). Watch the run in the background withgh run watch <id> --exit-status, then confirm the tag withgh release view v<ver>-<n>.
Phase 7 - close the loop with the reporter
Use the /comment voice for every comment:
- one per issue
- a fix comment is two or three sentences: thanks, what now works (and any new switch, by its menu path), the release. No bullet lists, no rationale, no edge cases
- name the fix and the release in bold. The maintainer may want it posted while the release run is still going; then say the build is running and name the expected tag, and close the issue once the tag exists
- no blame on our patches
- no internals and no speculation
- for a new UI feature, attach screenshots with a rectangle around what is new. The maintainer wants them as attachments of the comment itself.
ghcannot upload those, so a worker uses claude-in-chrome with the maintainer's GitHub session to drag them into the comment editor. Release assets (gh release uploadplus embedded URLs) are only a fallback; ask before using them. Check every PNG for personal data (names, avatar, sidebar, paths, other windows) before uploading.
Asking for more data. Only after local reproduction is exhausted: the maintainer dislikes round-trips with reporters, and a logged-in scratch instance (Phase 2) answers most timing and lifecycle questions here. Give one action plus one copy-paste command, and end with "empty output is fine too" when that is true. Never ask them to look for strings themselves. Never explain what each line would mean. claude-desktop --diagnose is the default collector, but never ask for it again when the thread already has one. Use a targeted grep only when the evidence is not in --diagnose, which does not include claude-patches.log.
After posting:
- Close fixed BUG issues with
--reason completed. - A FEATURE REQUEST stays open: once the tag exists, EDIT the fix comment (never add a second one) so it links the release and asks whether they have time to test it and report back, naming
claude-desktop --diagnoseas the thing to attach for anything that fails. Close it once they confirm or go quiet. - Keep a bug issue open only if its comment waits on reporter data.
- Answer follow-up questions from reporters in one or two lines.
Phase 8 - self-improve (always, last step)
Before reporting done, update this file: .claude/skills/check-issue/SKILL.md.
- List what went wrong or cost time in this run. That includes a wrong assumption, a confusing comment, a rejected push, a tool trap, a maintainer correction, or a gate that fired late.
- Fold, don't append. If a lesson changes how a phase should be done, edit that phase's text. Only lessons with no natural home go under "Lessons" below, as one dated line each.
- Prune. Delete a Lessons line once its rule lives in a phase. Keep this file under ~200 lines, and merge or drop the weakest lines rather than growing it.
- Never write session-specific values into the phases (minified names, pids, versions). Those go stale on the next upstream release.
- If a lesson belongs to another skill (
/comment,/test-native,/linux,/deploy), edit that skill instead and leave a pointer only if it is needed. - Tell the maintainer in one line what changed in the skill. The edit ships with the next
/commit-fastlike any other tracked file.
Lessons
- 2026-10-26 (#297-#299): an upstream reload that "should" run after a watchdog crash request was lost in live runs. Prefer guaranteed recovery over avoiding a duplicate reload, and measure before accepting a reviewer's claim about the mechanism.
- 2026-10-26: when a renderer is resumed after upstream's crash request, it executes the queued crash. Do not make "a resumed renderer must survive" an acceptance criterion.
- 2026-10-30 (#305):
nativeTheme.shouldUseDarkColorsfollows Claude's own Appearance setting (upstream setsthemeSourcefrom it), not the desktop or panel. Anything "theme-aware" on Linux is only desktop-aware with Appearance = System; offer a switch for the mismatch instead of guessing the panel color. - 2026-10-30 (#305): when a refresh hook calls into upstream code, call upstream's own wrapper (crash containment, retries) rather than the inner function it wraps.
- 2026-11-02 (#293 round 8): upstream main has no task-end signal for cloud Cowork sessions; the page's
releaseRemoteCuLockis view-scoped and the model'scomputer_release_lockis optional. The server'sworker_status(upstream's ownrefreshRemoteChromeSessionMode, cookie-authenticated) flips to idle with theresultevent; poll it instead of guessing with a timer.