A CLAUDE.md is just a markdown file at the root of your repo. Copy the content below into your own project's CLAUDE.md to give your agent the same context.
npx versuz@latest install aaddrick-claude-desktop-debian --kind=claude-mdcurl -o CLAUDE.md https://raw.githubusercontent.com/aaddrick/claude-desktop-debian/HEAD/CLAUDE.md# Claude Desktop Debian - Development Notes
## Project Overview
This project repackages Claude Desktop (Electron app) for Debian/Ubuntu Linux, applying necessary patches for Linux compatibility.
## Learnings
The [`docs/learnings/`](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`](docs/learnings/nix.md) — NixOS packaging, Electron resource path resolution, testing without NixOS
- [`cowork-vm-daemon.md`](docs/learnings/cowork-vm-daemon.md) — Cowork VM daemon lifecycle, respawn logic, crash diagnosis
- [`plugin-install.md`](docs/learnings/plugin-install.md) — Anthropic & Partners plugin install flow, gate logic, backend endpoints, and DevTools recipes
- [`apt-worker-architecture.md`](docs/learnings/apt-worker-architecture.md) — APT/DNF binary distribution via Cloudflare Worker + GitHub Releases, redirect chain, credential ownership, heartbeat runbook
- [`tray-rebuild-race.md`](docs/learnings/tray-rebuild-race.md) — why destroy + recreate on `nativeTheme` updates briefly duplicates the tray icon on KDE Plasma, and the in-place `setImage` + `setContextMenu` fast-path that avoids the SNI re-registration race
- [`mcp-double-spawn.md`](docs/learnings/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
- [`linux-topbar-shim.md`](docs/learnings/linux-topbar-shim.md) — why claude.ai's in-app topbar is missing on Linux, the four gates that hide it, why the upstream `frame: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)
- [`test-harness-electron-hooks.md`](docs/learnings/test-harness-electron-hooks.md) — why constructor-level `BrowserWindow` wraps are silently bypassed by `frame-fix-wrapper`'s Proxy, and the prototype-method hook pattern that works (used by the Quick Entry test runners)
- [`test-harness-ax-tree-walker.md`](docs/learnings/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
- [`patching-minified-js.md`](docs/learnings/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
## Code Style
All shell scripts in this project must follow the [Bash Style Guide](STYLEGUIDE.md). 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`
### 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
## 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:
```bash
# 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
The README Acknowledgments section credits external contributors in chronological order (by merge date or fix date). Update it when:
1. **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.
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/*.sh` (one file per subsystem: `tray.sh`, `cowork.sh`, `claude-code.sh`, etc.); `build.sh` is only an orchestrator that sources them. Variable and function names are minified and **change between releases**.
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. Shared extraction helpers live in `scripts/patches/_common.sh`. Example:
```bash
# Extract function name from a known pattern
TRAY_FUNC=$(grep -oP 'on\("menuBarEnabled",\(\)=>\{\K[$\w]+(?=\(\)\})' app.asar.contents/.vite/build/index.js)
```
5. **Handle optional whitespace** in regex patterns:
```bash
# 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
## Frame Fix Wrapper
The app uses a wrapper system to intercept and fix Electron behavior for Linux:
- **`frame-fix-wrapper.js`** - Intercepts `require('electron')` to patch BrowserWindow defaults (e.g., `frame: true` for 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.
## Setting Up build-reference
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.
### Prerequisites
```bash
# Install required tools
sudo apt install p7zip-full wget nodejs npm
# Install asar and prettier globally (or use npx)
npm install -g @electron/asar prettier
```
### Step 1: Download the Windows Installer
The Windows installer contains the app.asar which has the full Electron app source.
```bash
# 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"
```
### Step 2: Extract the Installer
```bash
# 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
```
### Step 3: Extract app.asar
```bash
# Extract the asar archive
asar extract app.asar app-extracted
```
### Step 4: Beautify the JavaScript Files
The extracted JS files are minified. Use prettier to make them readable:
```bash
# 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
```
### Step 5: Clean Up (Optional)
```bash
# 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-extracted
```
### Final Structure
```
build-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
```
## 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
```bash
# 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-VERSION-amd64.deb` - Debian package for x86_64
- `claude-desktop-VERSION-amd64.AppImage` - AppImage for x86_64
- `claude-desktop-VERSION-arm64.deb` - Debian package for ARM64
- `claude-desktop-VERSION-arm64.AppImage` - AppImage for ARM64
- `result/` - Nix build output (symlink, gitignored)
## 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`](docs/learnings/apt-worker-architecture.md).
## Testing
### Local Build
```bash
./build.sh --build appimage --clean no
```
### Nix Build
```bash
nix build .#claude-desktop
nix build .#claude-desktop-fhs
```
### Testing AppImage
```bash
# 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
```bash
# 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
```
### Checking DBus/Tray Status
```bash
# 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.
```bash
# 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.
## 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:
```bash
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.sh` and `nix/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_VERSION` repo variable and the URLs in `scripts/setup/detect-host.sh` on main when a new version is detected. Before committing `scripts/setup/detect-host.sh`, ensure your branch has the latest URLs:
```bash
# 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="
```
Update both amd64 and arm64 URLs in `detect_architecture()` to match main