This project repackages Claude Desktop (Electron app) for Debian/Ubuntu Linux, applying necessary patches for Linux compatibility.
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.
nix.md— NixOS packaging, Electron resource path resolution, testing without NixOScowork-vm-daemon.md— Cowork VM daemon lifecycle, respawn logic, crash diagnosisplugin-install.md— Anthropic & Partners plugin install flow, gate logic, backend endpoints, and DevTools recipesapt-worker-architecture.md— APT/DNF binary distribution via Cloudflare Worker + GitHub Releases, redirect chain, credential ownership, heartbeat runbooktray-rebuild-race.md— why destroy + recreate onnativeThemeupdates briefly duplicates the tray icon on KDE Plasma, and the in-placesetImage+setContextMenufast-path that avoids the SNI re-registration racemcp-double-spawn.md— Stdio MCPs spawn 2× when chat and Code/Agent panels are both active, root cause in upstream session managers, MCP-author workaroundlinux-topbar-shim.md— why claude.ai's in-app topbar is missing on Linux, the four gates that hide it, why the upstreamframe:false+ WCO config has unclickable buttons on X11 (Chromium-level implicit drag region), and the resolution: hybrid mode (system frame + UA-spoof shim → stacked layout, full button functionality)
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
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
- 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
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.
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>
The README Acknowledgments section credits external contributors in chronological order (by merge date or fix date). Update it when:
- Merging an external PR — Add the author to the Acknowledgments 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.
-
Always use regex patterns when modifying the source JavaScript. Patches live in
scripts/patches/*.sh(one file per subsystem:tray.sh,cowork.sh,claude-code.sh, etc.);build.shis only an orchestrator that sources them. Variable and function names are minified and change between releases. -
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. Shared extraction helpers live in
scripts/patches/_common.sh. Example:# Extract function name from a known pattern TRAY_FUNC=$(grep -oP 'on\("menuBarEnabled",\(\)=>\{\K\w+(?=\(\)\})' app.asar.contents/.vite/build/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*\{)/...'
build-reference/app-extracted/- Extracted and beautified source for analysisbuild-reference/tray-icons/- Tray icon assets for reference
The app uses a wrapper system to intercept and fix Electron behavior for Linux:
frame-fix-wrapper.js- Interceptsrequire('electron')to patch BrowserWindow defaults (e.g.,frame: truefor proper window decorations on Linux)frame-fix-entry.js- Entry point that loads the wrapper before the main app
These are injected by scripts/patches/app-asar.sh (inside patch_app_asar) and referenced in package.json's main field. The wrapper pattern allows fixing Electron behavior without modifying the minified app code directly.
If build-reference/ is missing or you need to inspect source for a new version, follow these steps to download, extract, and beautify the source code.
# Install required tools
sudo apt install p7zip-full wget nodejs npm
# Install asar and prettier globally (or use npx)
npm install -g @electron/asar prettierThe Windows installer contains the app.asar which has the full Electron app source.
# Create working directory
mkdir -p build-reference && cd build-reference
# Download URL pattern (update version as needed):
# x64: https://downloads.claude.ai/releases/win32/x64/VERSION/Claude-COMMIT.exe
# arm64: https://downloads.claude.ai/releases/win32/arm64/VERSION/Claude-COMMIT.exe
# Example for version 1.1.381:
wget -O Claude-Setup-x64.exe "https://downloads.claude.ai/releases/win32/x64/1.1.381/Claude-c2a39e9c82f5a4d51f511f53f532afd276312731.exe"# Extract the exe (it's a 7z archive)
7z x -y Claude-Setup-x64.exe -o"exe-contents"
# Find and extract the nupkg
cd exe-contents
NUPKG=$(find . -name "AnthropicClaude-*.nupkg" | head -1)
7z x -y "$NUPKG" -o"nupkg-contents"
cd ..
# Copy out the important files
cp exe-contents/nupkg-contents/lib/net45/resources/app.asar .
cp -a exe-contents/nupkg-contents/lib/net45/resources/app.asar.unpacked .
# Optional: copy tray icons for reference
mkdir -p tray-icons
cp exe-contents/nupkg-contents/lib/net45/resources/*.png tray-icons/ 2>/dev/null || true
cp exe-contents/nupkg-contents/lib/net45/resources/*.ico tray-icons/ 2>/dev/null || true# Extract the asar archive
asar extract app.asar app-extractedThe extracted JS files are minified. Use prettier to make them readable:
# Beautify all JS files in the build directory
npx prettier --write "app-extracted/.vite/build/*.js"
# Or beautify specific files
npx prettier --write app-extracted/.vite/build/index.js
npx prettier --write app-extracted/.vite/build/mainWindow.js# Remove intermediate files, keep only what's needed for reference
rm -rf exe-contents
rm Claude-Setup-x64.exe
rm -rf app.asar app.asar.unpacked # Keep only app-extractedbuild-reference/
├── app-extracted/
│ ├── .vite/
│ │ ├── build/
│ │ │ ├── index.js # Main process (beautified)
│ │ │ ├── mainWindow.js # Main window preload
│ │ │ ├── mainView.js # Main view preload
│ │ │ └── ...
│ │ └── renderer/
│ │ └── ...
│ ├── node_modules/
│ │ └── @ant/claude-native/ # Native bindings (stubs)
│ └── package.json
├── tray-icons/
│ ├── TrayIconTemplate.png # Black icon (for light panels)
│ ├── TrayIconTemplate-Dark.png # White icon (for dark panels)
│ └── ...
└── nupkg-contents/ # Optional: full extracted nupkg
When adding support for new distribution formats (e.g., RPM, Flatpak, Snap) or package repositories, follow these guidelines to avoid iterative debugging in CI.
-
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.
-
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 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.
| 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 |
# 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-nameclaude-desktop-VERSION-amd64.deb- Debian package for x86_64claude-desktop-VERSION-amd64.AppImage- AppImage for x86_64claude-desktop-VERSION-arm64.deb- Debian package for ARM64claude-desktop-VERSION-arm64.AppImage- AppImage for ARM64result/- Nix build output (symlink, gitignored)
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.
./build.sh --build appimage --clean nonix build .#claude-desktop
nix build .#claude-desktop-fhs# Run with logging
./test-build/claude-desktop-*.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log# Find the mounted AppImage path
mount | grep claude
# Example: /tmp/.mount_claudeXXXXXX
# Extract the running app's asar for inspection
npx asar extract /tmp/.mount_claudeXXXXXX/usr/lib/node_modules/electron/dist/resources/app.asar /tmp/claude-inspect
# Search for patterns in the extracted code
grep -n "pattern" /tmp/claude-inspect/.vite/build/index.js# 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"- 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
- App data:
~/.config/Claude/ - Logs:
~/.config/Claude/logs/ - SingletonLock:
~/.config/Claude/SingletonLock - Launcher log:
~/.cache/claude-desktop-debian/launcher.log
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.
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 automatically updates CLAUDE_DESKTOP_VERSION, patches the URLs in scripts/setup/detect-host.sh, and creates a new tag — no manual intervention needed.
.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
- Nix hashes - When Claude Desktop version changes, both the URLs in
scripts/setup/detect-host.shandnix/claude-desktop.nix(version, URLs, SRI hashes) must be updated. The CI handles this automatically. - Claude Desktop version - A GitHub Action automatically updates the
CLAUDE_DESKTOP_VERSIONrepo variable and the URLs inscripts/setup/detect-host.shon main when a new version is detected. Before committingscripts/setup/detect-host.sh, ensure your branch has the latest URLs:Update both amd64 and arm64 URLs in# Check repo variable (source of truth) gh variable get CLAUDE_DESKTOP_VERSION # Check current version in the detect_architecture case statement grep -oP 'x64/\K[0-9]+\.[0-9]+\.[0-9]+' scripts/setup/detect-host.sh | head -1 # If outdated, pull URLs from main branch gh api repos/aaddrick/claude-desktop-debian/contents/scripts/setup/detect-host.sh?ref=main \ --jq '.content' | base64 -d | grep -E "claude_download_url="
detect_architecture()to match main