Files
wehub-resource-sync 3e076d4dd9
Deploy Worker / deploy (push) Failing after 1s
Shellcheck / Check shell scripts (push) Failing after 1s
CI / Build Packages (amd64 - appimage) (push) Has been skipped
CI / Build Packages (amd64 - deb) (push) Has been skipped
CI / Build Packages (amd64 - rpm) (push) Has been skipped
CI / Build Packages (arm64 - appimage) (push) Has been skipped
CI / Build Packages (arm64 - deb) (push) Has been skipped
CI / Test Flags Parsing (push) Failing after 1s
BATS Tests / BATS unit tests (push) Failing after 1s
CI / Build Packages (arm64 - rpm) (push) Has been skipped
CI / Test Build Artifacts (amd64) (push) Has been skipped
CI / Test Build Artifacts (arm64) (push) Has been skipped
CI / Update APT Repository (push) Has been cancelled
CI / Mirror Official .deb to Release (push) Has been cancelled
CI / Update DNF Repository (push) Has been cancelled
CI / Update AUR Package (push) Has been cancelled
CI / Create Release (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:29:21 +08:00

32 KiB
Raw Permalink Blame History

AGENTS.md

Required reading

These documents are the source of truth. If anything in this file conflicts with them, they win. Read them before opening a non-trivial issue or PR.

This file is a fast reference for the highest-leverage rules and the project's accumulated archaeology. New policy goes in the style guides or CONTRIBUTING.md.

Project Overview

This project repackages Anthropic's official Claude Desktop for Linux .deb into the formats Anthropic doesn't serve (RPM, AppImage, Nix, AUR) plus our own .deb, and wraps every format in a launcher with Linux-environment fixes (Wayland opt-in, GPU-crash recovery, --doctor diagnostics). Since the v3.0.0 rebase (decision D-002) the contract is patch-zero: the official app.asar ships byte-identical unless a patch justifies itself against official bytes as compensating a genuine Linux gap.

Learnings

The docs/learnings/ directory contains hard-won technical knowledge from debugging and fixing issues — things that aren't obvious from reading the code or docs alone. Consult these before working on related areas. Add new entries when you discover something non-obvious that would save future contributors (human or AI) significant time. Docs whose subject no longer ships live in docs/archive/ with an obsolescence header — they stay findable as diagnosis records.

  • official-deb-rebase-verification.md — patch-necessity matrix verified against Anthropic's official Linux .deb (which legacy patches the v3.0.0 rebase deletes, the two survivor candidates, and why), plus the install-layout facts the rebase depends on: process.resourcesPath helper resolution (relocation-safe), the hardcoded OVMF/AAVMF firmware probe list (not distro-safe), per-arch dependency contracts, SUID recording in data.tar.xz, and the official postinst's AppArmor + apt self-registration behavior; its "Open items" section is the live pre-ship checklist
  • patching-minified-js.md — general lessons from maintaining a long-lived patch suite against an actively re-minified upstream: anchor selection (literals over identifiers), the \w vs $ identifier-capture trap, beautified false-negatives, idempotency guards, multi-site coordination, non-unique anchor disambiguation, and the SHA-256-pinned hypothesis-verification recipe — still load-bearing for the two survivor patches
  • cross-build-host-vs-target.md — the host-vs-target conflation class caught twice in the CI cutover: tools that run during the build key on uname -m, artifacts key on --arch; symptom is Exec format error on cross legs
  • packaging-permissions.md — restrictive-umask permission traps across deb/rpm/AppImage: app.asar.unpacked traversability, dpkg-deb --root-owner-group, the rpm %defattr file-mode trap
  • nix.md — the official-deb Nix derivation: design contract, the live SRI auto-bump sed anchors, the sandbox SUID extraction trap, why the old Electron resource-path hack must not return, and testing without NixOS
  • apt-worker-architecture.md — APT/DNF binary distribution via Cloudflare Worker + GitHub Releases, redirect chain, credential ownership, heartbeat runbook
  • wayland-global-shortcuts-portal.md — why Quick Entry's hotkey is focus-bound on GNOME Wayland (mutter dropped XWayland global key grabs), the native-Wayland + GlobalShortcutsPortal launcher change (opt-in via CLAUDE_USE_WAYLAND=1; fixes GNOME ≤49, default GNOME stays on XWayland), the "only the last --enable-features switch wins → merge into one flag" trap, the tri-state CLAUDE_USE_WAYLAND escape hatch, and the proof that GNOME 50 / xdg-desktop-portal ≥1.20 is still blocked upstream because Electron/Chromium never calls the host Registry.Register app-id handshake (electron#51875); wlroots (Niri/Sway/Hyprland) lack a portal GlobalShortcuts backend entirely
  • mcp-double-spawn.md — Stdio MCPs spawn 2× when chat and Code/Agent panels are both active, root cause in upstream session managers, MCP-author workaround; now first-party-reproducible → upstream report drafted
  • plugin-install.md — Anthropic & Partners plugin install flow, gate logic, backend endpoints, and DevTools recipes
  • tray-rebuild-race.md — the KDE Plasma SNI re-registration race and the in-place setImage + setContextMenu fast-path; validated — the official build converged on the same fix, our tray patch is deleted
  • cowork-vm-daemon.md — the 2.x bwrap Cowork daemon lifecycle; superseded on KVM hosts by the official coworkd, kept as reference for the 3.1 fallback investigation
  • test-harness-electron-hooks.md — why constructor-level BrowserWindow wraps were silently bypassed by the (now-deleted) frame-fix Proxy, and the prototype-method hook pattern that remains correct for harness code
  • test-harness-ax-tree-walker.md — five non-obvious traps in the v7 fingerprint walker after the AX-tree migration: AX-enable async lag, navigateTo-to-same-URL no-op, claude.ai's flat dialog>button[] lists, the more options for X per-row shape, and sidebar virtualization vs the lookup-failure threshold
  • config-wipe-guard.md — the poisoned-cache config wipe (silent {} loader fallback + whole-file serialize on every settings write) that stubs out claude_desktop_config.json; where the renderer's grouping state actually lives (IndexedDB pin-statepersisted.* localStorage → epitaxyPrefs mirror); the launcher-side backup rotation (backup_user_config) that is the patch-zero-clean primary fix; and why the in-band asar guard (config.sh, R1/R2/R3 restore rules, lazy-clone non-stickiness, the CF-1 no-resurrect constraint) is kept hardened but parked after a contrarian review, with local-stores.sh deleted outright
  • quit-cleanup-scope-fence.md — the two systemd-scope namespaces behind the #709 quit-cleanup slice: KDE/GNOME's KProcessRunner desktop-id scope (app-claude-desktop-<pid>.scope, GUI-launch-only, renamed by v3.0.0 to -unofficial) vs Electron's own StartTransientUnit app-id self-scope (app-com.anthropic.Claude-<pid>.scope, all launch paths, but the app-id is versioned so derive it — was io.github.aaddrick...); why the self-scope still can't fence the zygote-descended helpers on a terminal launch (they stay in the caller's shell scope, next to a user's own MCP server → the unsolved gate-3 bystander-kill risk); the finding that nothing orphans on clean quit or SIGKILL (Chromium reaps its tree cgroup-agnostically) so the slice has no survivor to catch; and the test traps (pgrep -f self-match → use /proc/PID/exe, setsid+disown to dodge the exit-144 startup signal, scope-existence ≠ liveness)
  • test-methodology-and-coverage.md — how a green test run is kept honest, distilled from @sabiut's test/doctor PRs and reviews: the half-pinned-test failure class (run-subshell discards _doctor_failures mutations → assert directly not via run; near-miss anchor fixtures; stubs that mirror the prod call can't catch a change to it; [PASS] on unread data; poll predicate must equal the reaper's own predicate; SC2314 negative-assertion no-ops), host-state isolation (stub in-shell vs PATH-shim subshell calls, unset every XDG_*/_DOCTOR_* fallback), the setsid+kill -- -PGID launch-smoke reaper with a readiness marker, and the mutation-check review discipline (revert the fix; if nothing goes red the test is decoration)

Archived (still useful as diagnosis records): docs/archive/linux-topbar-shim.md — the four topbar gates and the WCO/implicit-drag-region investigation (shim deleted; official builds render the topbar on Linux, and Bugs A/B/C moved to docs/upstream-reports/); docs/archive/cowork-linux-handover.md — the 2.x patch-based Cowork stack handover.

Code Style

All shell scripts in this project must follow the Bash Style Guide. Key points:

  • Tabs for indentation, lines under 80 characters (exception: URLs and regex patterns)
  • Use [[ ]] for conditionals, $(...) for command substitution
  • Single quotes for literals, double quotes for expansions
  • Lowercase variables; UPPERCASE only for constants/exports
  • Use local in functions, avoid set -e and eval

Anti-patterns

  • Don't set -e. It interacts badly with $(...) capture and function return values, and the project has historically debugged enough silent exits to settle the question. Check status explicitly: cmd || handle_err.
  • Don't eval. Use arrays for argv composition (cmd "${args[@]}"). eval defeats every parser and is a permanent SC2046 magnet.
  • Don't use POSIX [ ... ]. Always [[ ... ]]. POSIX [ mis-parses unquoted expansions in ways [[ does not.
  • Don't backtick. Always $(...). Backticks don't nest cleanly and conflict with markdown when patches are pasted into PR comments.
  • Don't hardcode the work directory. Scripts that operate during a build use $work_dir (set by build.sh). A hardcoded path silently breaks the AppImage build, which runs in a different layout from the deb/rpm builds.
  • Don't wrap commands in if cmd; then true; else false; fi-style scaffolding. Just cmd — the exit code is already there.
  • Don't append to a baseline file to silence shellcheck. Fix the underlying issue. If a warning is genuinely a false positive, use a per-line # shellcheck disable=SCXXXX with a comment explaining why.

Linting

Shell scripts are checked with shellcheck and GitHub Actions workflows with actionlint before pushing. When lint issues are found:

  1. Fix the code - Correct the underlying issue rather than suppressing the warning
  2. Disable directives are a last resort - Only use # shellcheck disable=SCXXXX when:
    • The warning is a false positive
    • The pattern is intentional and unavoidable
    • Always add a comment explaining why the disable is needed
  3. Run /lint to check manually - Use this skill to check for issues before pushing

Docs

  • One declarative sentence then a code block or list at the top of every page. No "In this guide we will explore…" preamble. See docs/styleguides/docs_styleguide.md.
  • Lowercase kebab-case filenames for everything in docs/. Order belongs in docs/index.md, not filenames or numeric prefixes.
  • Real domain nouns over foo/bar in walkthroughs. The project vocabulary is patches, the launcher, the worker, app.asar, the minified bundle, the asar archive, the doctor surface.
  • Subsystem deep-dives go under docs/learnings/. Surfacing knowledge there beats burying it in commit messages or in patch-script comments. Add an entry when you discover something non-obvious that would save the next contributor significant time.
  • Decisions go in docs/decisions.md (ADR format). Don't relitigate a settled direction inside a how-to page; link the decision instead.
  • Troubleshooting headings are the literal symptom, not editorialized prose. ## Black screen on Fedora KDE under Wayland, not ## Troubles with Wayland. Search ranks headings.
  • CHANGELOG follows Keep a Changelog 1.1.0. Bullets grouped under Added / Fixed / Changed / Deprecated / Removed / Security; one bullet per change; PR link for the deep dive; inline BREAKING prefix for breaking changes. See CHANGELOG.md for the current state and RELEASING.md for when entries get promoted from [Unreleased].

GitHub Workflow

General Approach

  • Use gh CLI for all GitHub interactions
  • Create branches based on issue numbers: fix/123-description or feature/123-description
  • Reference issues in commits and PRs with #123 or Fixes #123
  • After creating a PR, add a comment to the related issue with a summary and link to the PR

Investigating Issues

For older issues, review the state of the code when the issue was raised - it may have already been addressed:

# Get issue creation date
gh issue view 123 --json createdAt

# Find the commit just before the issue was created
git log --oneline --until="2025-08-23T08:48:35Z" -1

# View a file at that point in time
git show <commit>:path/to/file.sh

# Search for relevant changes since the issue was created
git log --oneline --after="2025-08-23" -- path/to/file.sh

# View a specific commit that may have fixed the issue
git show <commit>

This helps identify if the issue was already fixed, and allows referencing the specific commit in the response.

Attribution

For PR descriptions, include full attribution:

---
Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <model-name> <noreply@anthropic.com>
<XX>% AI / <YY>% Human
Claude: <what AI did>
Human: <what human did>
  • Use the actual model name (e.g., Claude Opus 4.5, Claude Sonnet 4)
  • The percentage split should honestly reflect the contribution balance for that specific work
  • This provides a trackable record of AI-assisted development over time

For issues and comments, use simplified attribution:

---
Written by Claude <model-name> via [Claude Code](https://claude.ai/code)

For commits, include a Co-Authored-By trailer:

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

Contributor Credits

ACKNOWLEDGMENTS.md credits external contributors in chronological order (by merge date or fix date); the README Acknowledgments section keeps only the three inspirational projects and links there. Update ACKNOWLEDGMENTS.md when:

  1. Merging an external PR — Add the author to the list with a link to their GitHub profile and a brief description of their contribution.
  2. Implementing a fix suggested in an issue — If an issue author (or commenter) provided a concrete fix, workaround, code snippet, or detailed technical analysis that was directly used, credit them too.

Contributors are listed in chronological order: inspirational projects first (k3d3, emsi, leobuskin), then contributors ordered by when their contribution was merged or implemented.

Working with Minified JavaScript

Important Guidelines

  1. Always use regex patterns when modifying the source JavaScript. Patches live in scripts/patches/*.shapp-asar.sh is the orchestrator with the explicit active_patches array (currently quick-window.sh, org-plugins.sh, virtiofsd-probe.sh, and cowork-bwrap.sh; config.sh is sourced but parked/unwired). An empty array ships the official app.asar byte-identical (patch-zero). Since upstream 1.19367.0 the main process is code-split: .vite/build/index.js is a stub that require()s a content-hashed index.chunk-<hash>.js main chunk, so patches operate on $main_js (resolved by _resolve_main_js in app-asar.sh), not on index.js directly — one patch can even span chunks (see cowork-bwrap.sh's warm chunk). Variable and function names are minified and change between releases; full anchor-craft and code-split lessons are in docs/learnings/patching-minified-js.md.

  2. The beautified code in build-reference/ has different spacing than the actual minified code in the app. Patterns must handle both:

    • Minified: oe.nativeTheme.on("updated",()=>{
    • Beautified: oe.nativeTheme.on("updated", () => {
  3. Use -E flag with sed for extended regex support when patterns need grouping or alternation.

  4. Extract variable names dynamically rather than hardcoding them. Example (from scripts/patches/quick-window.sh), where $index_js is ${main_js:-…/index.js} — the resolved main chunk:

    # The minified Quick Entry window var, anchored on a stable literal
    quick_var=$(grep -oP '[$\w]+(?=\.setAlwaysOnTop\(\s*!0\s*,\s*"pop-up-menu"\))' \
        "$index_js")
    
  5. Handle optional whitespace in regex patterns:

    # Bad: assumes no spaces
    sed -i 's/oe.nativeTheme.on("updated",()=>{/...'
    
    # Good: handles optional whitespace
    sed -i -E 's/(oe\.nativeTheme\.on\(\s*"updated"\s*,\s*\(\)\s*=>\s*\{)/...'
    

Reference Files

  • build-reference/app-extracted/ - Extracted and beautified source for analysis
  • build-reference/tray-icons/ - Tray icon assets for reference

Patch Orchestration (patch-zero)

scripts/patches/app-asar.sh owns the asar patch stage:

  • active_patches array — the only place a patch gets wired in. Empty array ⇒ no extract, no repack, official app.asar ships byte-identical.
  • productName guard — the build fails if upstream's productName stops matching WM_CLASS (breaks StartupWMClass in every .desktop file).
  • Upstream tripwires (AU-1/MB-1) — the build fails if the official bundle stops shipping apt_channel_pending (autoupdater still pending, see D-001) or menuBarEnabled:!0 (menu-bar default). These replace the per-patch WARNINGs that left with the v3.0.0 deletions.
  • Config-wipe recovery is launcher-side, not an asar patchbackup_user_config in launcher-common.sh rotates backups of claude_desktop_config.json and the Cowork stores before each launch (patch-zero-clean). The in-band config.sh guard is parked; if ever re-armed, its CFG-1 anchor-miss returns non-zero. See docs/learnings/config-wipe-guard.md.
  • Repack invariant — the unpacked-file set is derived from the shipped app.asar.unpacked tree and must match after repack, so upstream native helpers can't silently inline.

The 2.x frame-fix wrapper (frame-fix-wrapper.js require('electron') interception) is gone — the official build owns its window behavior. Any proposal to intercept Electron APIs again must clear the patch-zero bar in D-002.

Setting Up build-reference

If build-reference/ is missing or you need to inspect source for a new version, extract and beautify the bundle from the official Linux .deb (the Windows-installer recipe died with the v3.0.0 rebase).

Prerequisites

# Install required tools (ar comes from binutils)
sudo apt install binutils wget xz-utils zstd nodejs npm

# Install asar and prettier globally (or use npx)
npm install -g @electron/asar prettier

Step 1: Download the official .deb

The pinned version, pool path, and SHA-256 live in scripts/setup/official-deb.sh (OFFICIAL_DEB_*). To fetch the pinned amd64 build:

mkdir -p build-reference && cd build-reference

# Read the current pin
source ../scripts/setup/official-deb.sh 2>/dev/null || true
wget -O claude-desktop.deb \
  "https://downloads.claude.ai/claude-desktop/apt/stable/$OFFICIAL_DEB_POOL_AMD64"
echo "$OFFICIAL_DEB_SHA256_AMD64  claude-desktop.deb" | sha256sum -c

To inspect the newest pool entry instead, resolve it from the Packages index (resolve_official_deb in official-deb.sh does the same thing):

curl -fsS "https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages" \
  | awk -v RS='' '/claude-desktop/' | grep -E '^(Version|Filename|SHA256):'

Step 2: Extract the .deb

No dpkg required — ar + tar handle every member (the data member has shipped as both .tar.zst and .tar.xz; check with ar t):

ar t claude-desktop.deb                     # list members
ar p claude-desktop.deb data.tar.xz | tar -J -x   # or --zstd for .tar.zst

# The app tree lands at usr/lib/claude-desktop/
cp usr/lib/claude-desktop/resources/app.asar .
cp -a usr/lib/claude-desktop/resources/app.asar.unpacked .

# Optional: hicolor icons for reference
cp -a usr/share/icons/hicolor tray-icons

Step 3: Extract app.asar

asar extract app.asar app-extracted

Step 4: Beautify the JavaScript Files

The extracted JS files are minified. Use prettier to make them readable:

# Beautify all JS files in the build directory. Since 1.19367.0 the main
# process is code-split, so index.js is a tiny stub and the real main
# code lives in index.chunk-<hash>.js — the glob covers every chunk.
npx prettier --write "app-extracted/.vite/build/*.js"

# The main-process chunk is the biggest .vite/build/*.js (index.js just
# require()s it). Resolve it from the stub if you want to beautify only it:
main_chunk=$(grep -oP 'require\("\./\Kindex\.chunk-[^"]+\.js(?="\))' \
    app-extracted/.vite/build/index.js)
npx prettier --write "app-extracted/.vite/build/$main_chunk"

Step 5: Clean Up (Optional)

# Keep only what's needed for reference
rm -rf usr claude-desktop.deb
rm -rf app.asar app.asar.unpacked  # Keep only app-extracted

Final Structure

build-reference/
├── app-extracted/
│   ├── .vite/
│   │   ├── build/
│   │   │   ├── index.js                 # Main-process entry stub
│   │   │   ├── index.chunk-<hash>.js     # Main process (code-split, 1.19367.0+)
│   │   │   ├── mainWindow.js             # Main window preload
│   │   │   ├── mainView.js               # Main view preload
│   │   │   └── ...
│   │   └── renderer/
│   │       └── ...
│   ├── node_modules/
│   │   └── @ant/claude-native/   # Rust native binding (real on Linux)
│   └── package.json
└── tray-icons/                   # Official hicolor icons (optional)

Remember that patterns verified against beautified output need the whitespace-tolerant form when applied to the shipped minified bytes (see the guidelines above).

Adding New Package Formats or Repositories

When adding support for new distribution formats (e.g., RPM, Flatpak, Snap) or package repositories, follow these guidelines to avoid iterative debugging in CI.

Research Before Implementing

  1. Understand the target system's constraints - Each package format has specific rules:

    • Version string formats (e.g., RPM cannot have hyphens in Version field)
    • Required metadata fields
    • Signing requirements and tools
  2. Search for existing CI implementations - Look for "GitHub Actions [format] signing" or similar. Existing workflows reveal required flags, environment setup, and common pitfalls.

  3. Check tool behavior in non-interactive environments - CI has no TTY. Tools like GPG need flags like --batch and --yes to work without prompts.

Consider Concurrency

  1. Multiple jobs writing to the same branch will race - If APT and DNF repos both push to gh-pages, add:

    • Job dependencies (needs: [other-job]), or
    • Retry loops with git pull --rebase before push
  2. External processes may also modify branches - GitHub Pages deployment runs automatically and can cause push conflicts.

Test the Full Pipeline

  1. Test CI steps locally first - Run the signing/packaging commands manually to catch errors before committing.

  2. Use a test tag for new infrastructure - Create a non-release tag to validate the full CI pipeline before merging to main.

  3. Verify the end-user experience - After CI succeeds, actually test the install commands from the README on a clean system.

Common CI Pitfalls

Issue Solution
GPG "cannot open /dev/tty" Add --batch flag
GPG "File exists" error Add --yes flag to overwrite
Push rejected (ref changed) Add git pull --rebase before push, with retry loop
Version format invalid Research target format's version constraints upfront
Signing key not found Ensure key is imported before signing step, check key ID output

CI/CD

Triggering Builds

# Trigger CI on a branch
gh workflow run CI --ref branch-name

# Watch the run
gh run watch RUN_ID

# Download artifacts
gh run download RUN_ID -n artifact-name

Build Artifacts

  • claude-desktop-unofficial_VERSION_amd64.deb / claude-desktop-unofficial_VERSION_arm64.deb - Debian packages
  • claude-desktop_1.16000.0-1_all.deb - transitional apt package (produced by the amd64 leg) that migrates legacy claude-desktop installs from our repo to claude-desktop-unofficial
  • claude-desktop-unofficial-VERSION-1.x86_64.rpm / claude-desktop-unofficial-VERSION-1.aarch64.rpm - RPM packages
  • claude-desktop-unofficial-VERSION-amd64.AppImage / claude-desktop-unofficial-VERSION-arm64.AppImage - AppImages (+ .zsync in CI)
  • result/ - Nix build output (symlink, gitignored; the derivation is a stub until the @typedrat rework lands)

One cross-building build.yml produces all of these from ubuntu-latest via the --arch input (see docs/learnings/cross-build-host-vs-target.md for the host-vs-target trap).

Distribution

APT and DNF binaries are fronted by a Cloudflare Worker at pkg.claude-desktop-debian.dev. Metadata (InRelease, Packages, KEY.gpg, repodata/*) passes through to the gh-pages branch; binary requests (/pool/.../*.deb, /rpm/*/*.rpm) get 302'd to the corresponding GitHub Release asset. This keeps .deb / .rpm files out of gh-pages entirely, so they never hit GitHub's 100 MB per-file push cap.

Key files:

  • worker/src/worker.js — Worker source
  • worker/wrangler.toml — Worker config (route, custom_domain = true)
  • .github/workflows/deploy-worker.yml — deploys on push to main when worker/** changes
  • .github/workflows/apt-repo-heartbeat.yml — daily chain validation, auto-opens tracking issue on failure
  • update-apt-repo and update-dnf-repo jobs in .github/workflows/ci.yml — gate a strip step on Worker liveness, so binaries are removed from the local pool tree before push

Repo secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID. Token scoped to the "Edit Cloudflare Workers" template.

Full details including the redirect chain, the http-scheme-downgrade gotcha, credential ownership, and heartbeat failure runbook: docs/learnings/apt-worker-architecture.md.

Testing

Local Build

./build.sh --build appimage --clean no

Nix Build

nix build .#claude-desktop
nix build .#claude-desktop-fhs

The derivation repackages the official .deb (fetchurl + autoPatchelfHook, no nixpkgs Electron). Build-verified on x86_64 only — runtime on real NixOS and the aarch64 leg are open validation items (owner @typedrat; design contract and testing recipe in docs/learnings/nix.md).

Testing AppImage

# Run with logging
./test-build/claude-desktop-*.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log

Debugging Workflow

Inspecting the Running App's Code

# Find the mounted AppImage path
mount | grep claude
# Example: /tmp/.mount_claudeXXXXXX

# Extract the running app's asar for inspection (official bare
# co-located layout: ELF + chrome-sandbox + resources/ side by side)
npx asar extract /tmp/.mount_claudeXXXXXX/usr/lib/claude-desktop/resources/app.asar /tmp/claude-inspect

# Search for patterns in the extracted code. Since 1.19367.0 the main
# process is code-split, so grep across all chunks (index.js is a stub);
# main-process anchors live in index.chunk-<hash>.js.
grep -rn "pattern" /tmp/claude-inspect/.vite/build/

Checking DBus/Tray Status

# List registered tray icons
gdbus call --session --dest=org.kde.StatusNotifierWatcher \
  --object-path=/StatusNotifierWatcher \
  --method=org.freedesktop.DBus.Properties.Get \
  org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems

# Find which process owns a DBus connection
gdbus call --session --dest=org.freedesktop.DBus \
  --object-path=/org/freedesktop/DBus \
  --method=org.freedesktop.DBus.GetConnectionUnixProcessID ":1.XXXX"

Log Locations

  • Launcher log: ~/.cache/claude-desktop-debian/launcher.log
  • App logs: ~/.config/Claude/logs/
  • Run with logging: ./app.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log

Useful Locations

  • App data: ~/.config/Claude/
  • Logs: ~/.config/Claude/logs/
  • SingletonLock: ~/.config/Claude/SingletonLock
  • Launcher log: ~/.cache/claude-desktop-debian/launcher.log

Versioning

Release versions are managed via two GitHub Actions repository variables (not files):

  • REPO_VERSION - The project's own version (e.g., 1.3.23). Bump this manually via gh variable set REPO_VERSION --body "X.Y.Z" when shipping project changes.
  • CLAUDE_DESKTOP_VERSION - The upstream Claude Desktop version (e.g., 1.1.8629). Updated automatically by the check-claude-version workflow when a new upstream release is detected.

Tag format

Tags follow the pattern v{REPO_VERSION}+claude{CLAUDE_DESKTOP_VERSION}, e.g., v1.3.23+claude1.1.7714. Pushing a tag triggers the CI release build.

# Check current values
gh variable get REPO_VERSION
gh variable get CLAUDE_DESKTOP_VERSION

# Bump repo version and tag a release
gh variable set REPO_VERSION --body "1.3.24"
git tag "v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)"
git push origin "v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)"

When upstream Claude Desktop updates, the check-claude-version workflow resolves the newest entry from the official APT Packages indexes (both arches, with a cross-arch agreement gate), seds the OFFICIAL_DEB_* pins in scripts/setup/official-deb.sh (and the Nix SRI hashes once the derivation stops being a stub), updates CLAUDE_DESKTOP_VERSION, and creates a new tag — no manual intervention needed. Do not run it by hand from a branch: the auto-tag cuts a release with whatever REPO_VERSION is staged.

Common Gotchas

  • .zsync files - Used for delta updates, can be ignored/deleted
  • AppImage mount points - Running AppImages mount to /tmp/.mount_claude*; check with mount | grep claude
  • Killing the app - Must kill all electron child processes, not just the main one:
    pkill -9 -f "mount_claude"
    
  • SingletonLock - If app won't start, check for stale lock: ~/.config/Claude/SingletonLock
  • Node version - Build requires Node.js; the script downloads its own if needed (keyed to the HOST arch — see the cross-build learning)
  • Version pins - The official .deb version, pool paths, and SHA-256 sums are pinned in scripts/setup/official-deb.sh (OFFICIAL_DEB_*), updated automatically by check-claude-version on main (which also seds the Nix SRI once the derivation lands). Before committing scripts/setup/official-deb.sh, ensure your branch carries the latest pins:
    # Check repo variable (source of truth)
    gh variable get CLAUDE_DESKTOP_VERSION
    
    # Check the pinned version on your branch
    grep -oP "^OFFICIAL_DEB_VERSION='\K[^']+" scripts/setup/official-deb.sh
    
    # What the official pool currently serves
    curl -fsS "https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages" \
      | grep -E '^Version:' | sort -V | tail -1
    
  • data.tar compression varies - Upstream has shipped both data.tar.zst and data.tar.xz; _extract_deb_member in official-deb.sh handles zst/xz/gz/plain, so never hardcode one