32 KiB
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.
CONTRIBUTING.md— what we accept, what goes upstream, subsystem owners, AI-attribution policy.docs/styleguides/bash_styleguide.md— shell-script conventions (forked from YSAP). Tabs, 80 cols,[[ ]], noset -e, noeval.docs/styleguides/docs_styleguide.md— page anatomy, naming, antipatterns for thedocs/tree.docs/index.md— entry point for the rest of the repo docs.SECURITY.md— vulnerability reporting; what's in scope vs. upstream.
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.resourcesPathhelper resolution (relocation-safe), the hardcoded OVMF/AAVMF firmware probe list (not distro-safe), per-arch dependency contracts, SUID recording indata.tar.xz, and the official postinst's AppArmor + apt self-registration behavior; its "Open items" section is the live pre-ship checklistpatching-minified-js.md— general lessons from maintaining a long-lived patch suite against an actively re-minified upstream: anchor selection (literals over identifiers), the\wvs$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 patchescross-build-host-vs-target.md— the host-vs-target conflation class caught twice in the CI cutover: tools that run during the build key onuname -m, artifacts key on--arch; symptom isExec format erroron cross legspackaging-permissions.md— restrictive-umask permission traps across deb/rpm/AppImage:app.asar.unpackedtraversability,dpkg-deb --root-owner-group, the rpm%defattrfile-mode trapnix.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 NixOSapt-worker-architecture.md— APT/DNF binary distribution via Cloudflare Worker + GitHub Releases, redirect chain, credential ownership, heartbeat runbookwayland-global-shortcuts-portal.md— why Quick Entry's hotkey is focus-bound on GNOME Wayland (mutter dropped XWayland global key grabs), the native-Wayland +GlobalShortcutsPortallauncher change (opt-in viaCLAUDE_USE_WAYLAND=1; fixes GNOME ≤49, default GNOME stays on XWayland), the "only the last--enable-featuresswitch wins → merge into one flag" trap, the tri-stateCLAUDE_USE_WAYLANDescape hatch, and the proof that GNOME 50 / xdg-desktop-portal ≥1.20 is still blocked upstream because Electron/Chromium never calls the hostRegistry.Registerapp-id handshake (electron#51875); wlroots (Niri/Sway/Hyprland) lack a portal GlobalShortcuts backend entirelymcp-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 draftedplugin-install.md— Anthropic & Partners plugin install flow, gate logic, backend endpoints, and DevTools recipestray-rebuild-race.md— the KDE Plasma SNI re-registration race and the in-placesetImage+setContextMenufast-path; validated — the official build converged on the same fix, our tray patch is deletedcowork-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 investigationtest-harness-electron-hooks.md— why constructor-levelBrowserWindowwraps were silently bypassed by the (now-deleted) frame-fix Proxy, and the prototype-method hook pattern that remains correct for harness codetest-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 flatdialog>button[]lists, themore options for Xper-row shape, and sidebar virtualization vs the lookup-failure thresholdconfig-wipe-guard.md— the poisoned-cache config wipe (silent{}loader fallback + whole-file serialize on every settings write) that stubs outclaude_desktop_config.json; where the renderer's grouping state actually lives (IndexedDBpin-state→persisted.*localStorage →epitaxyPrefsmirror); 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, withlocal-stores.shdeleted outrightquit-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 ownStartTransientUnitapp-id self-scope (app-com.anthropic.Claude-<pid>.scope, all launch paths, but the app-id is versioned so derive it — wasio.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 -fself-match → use/proc/PID/exe,setsid+disownto 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_failuresmutations → assert directly not viarun; 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 everyXDG_*/_DOCTOR_*fallback), thesetsid+kill -- -PGIDlaunch-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
localin functions, avoidset -eandeval
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[@]}").evaldefeats 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 bybuild.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. Justcmd— 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=SCXXXXwith a comment explaining why.
Linting
Shell scripts are checked with shellcheck and GitHub Actions workflows with actionlint before pushing. When lint issues are found:
- Fix the code - Correct the underlying issue rather than suppressing the warning
- Disable directives are a last resort - Only use
# shellcheck disable=SCXXXXwhen:- The warning is a false positive
- The pattern is intentional and unavoidable
- Always add a comment explaining why the disable is needed
- Run
/lintto 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 indocs/index.md, not filenames or numeric prefixes. - Real domain nouns over
foo/barin walkthroughs. The project vocabulary ispatches,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.mdfor the current state andRELEASING.mdfor when entries get promoted from[Unreleased].
GitHub Workflow
General Approach
- Use
ghCLI for all GitHub interactions - Create branches based on issue numbers:
fix/123-descriptionorfeature/123-description - Reference issues in commits and PRs with
#123orFixes #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:
- Merging an external PR — Add the author to the list with a link to their GitHub profile and a brief description of their contribution.
- 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
-
Always use regex patterns when modifying the source JavaScript. Patches live in
scripts/patches/*.sh—app-asar.shis the orchestrator with the explicitactive_patchesarray (currentlyquick-window.sh,org-plugins.sh,virtiofsd-probe.sh, andcowork-bwrap.sh;config.shis sourced but parked/unwired). An empty array ships the officialapp.asarbyte-identical (patch-zero). Since upstream 1.19367.0 the main process is code-split:.vite/build/index.jsis a stub thatrequire()s a content-hashedindex.chunk-<hash>.jsmain chunk, so patches operate on$main_js(resolved by_resolve_main_jsinapp-asar.sh), not onindex.jsdirectly — one patch can even span chunks (seecowork-bwrap.sh's warm chunk). Variable and function names are minified and change between releases; full anchor-craft and code-split lessons are indocs/learnings/patching-minified-js.md. -
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", () => {
- Minified:
-
Use
-Eflag with sed for extended regex support when patterns need grouping or alternation. -
Extract variable names dynamically rather than hardcoding them. Example (from
scripts/patches/quick-window.sh), where$index_jsis${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") -
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 analysisbuild-reference/tray-icons/- Tray icon assets for reference
Patch Orchestration (patch-zero)
scripts/patches/app-asar.sh owns the asar patch stage:
active_patchesarray — the only place a patch gets wired in. Empty array ⇒ no extract, no repack, officialapp.asarships byte-identical.- productName guard — the build fails if upstream's
productNamestops matchingWM_CLASS(breaksStartupWMClassin every.desktopfile). - Upstream tripwires (AU-1/MB-1) — the build fails if the official bundle stops shipping
apt_channel_pending(autoupdater still pending, see D-001) ormenuBarEnabled:!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 patch —
backup_user_configinlauncher-common.shrotates backups ofclaude_desktop_config.jsonand the Cowork stores before each launch (patch-zero-clean). The in-bandconfig.shguard is parked; if ever re-armed, its CFG-1 anchor-miss returns non-zero. Seedocs/learnings/config-wipe-guard.md. - Repack invariant — the unpacked-file set is derived from the shipped
app.asar.unpackedtree 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
-
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
-
Search for existing CI implementations - Look for "GitHub Actions [format] signing" or similar. Existing workflows reveal required flags, environment setup, and common pitfalls.
-
Check tool behavior in non-interactive environments - CI has no TTY. Tools like GPG need flags like
--batchand--yesto work without prompts.
Consider Concurrency
-
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 --rebasebefore push
- Job dependencies (
-
External processes may also modify branches - GitHub Pages deployment runs automatically and can cause push conflicts.
Test the Full Pipeline
-
Test CI steps locally first - Run the signing/packaging commands manually to catch errors before committing.
-
Use a test tag for new infrastructure - Create a non-release tag to validate the full CI pipeline before merging to main.
-
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 packagesclaude-desktop_1.16000.0-1_all.deb- transitional apt package (produced by the amd64 leg) that migrates legacyclaude-desktopinstalls from our repo toclaude-desktop-unofficialclaude-desktop-unofficial-VERSION-1.x86_64.rpm/claude-desktop-unofficial-VERSION-1.aarch64.rpm- RPM packagesclaude-desktop-unofficial-VERSION-amd64.AppImage/claude-desktop-unofficial-VERSION-arm64.AppImage- AppImages (+.zsyncin 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 sourceworker/wrangler.toml— Worker config (route,custom_domain = true).github/workflows/deploy-worker.yml— deploys on push tomainwhenworker/**changes.github/workflows/apt-repo-heartbeat.yml— daily chain validation, auto-opens tracking issue on failureupdate-apt-repoandupdate-dnf-repojobs 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 viagh 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 thecheck-claude-versionworkflow 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
.zsyncfiles - Used for delta updates, can be ignored/deleted- AppImage mount points - Running AppImages mount to
/tmp/.mount_claude*; check withmount | 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
.debversion, pool paths, and SHA-256 sums are pinned inscripts/setup/official-deb.sh(OFFICIAL_DEB_*), updated automatically bycheck-claude-versionon main (which also seds the Nix SRI once the derivation lands). Before committingscripts/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.zstanddata.tar.xz;_extract_deb_memberinofficial-deb.shhandles zst/xz/gz/plain, so never hardcode one