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 NixOS
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 in
build.sh. 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. Example from
build.sh:# 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 build.sh 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)
./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 build.sh URLs, 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
build.shURLs andnix/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 inbuild.shon main when a new version is detected. Before committingbuild.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 build.sh grep -oP 'x64/\K[0-9]+\.[0-9]+\.[0-9]+' build.sh | head -1 # If outdated, pull URLs from main branch gh api repos/aaddrick/claude-desktop-debian/contents/build.sh?ref=main \ --jq '.content' | base64 -d | grep -E "CLAUDE_DOWNLOAD_URL=|claude_download_url="
detect_architecture()to match main