{"owner":"aaddrick","repo":"claude-desktop-debian","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md","AGENTS.md"],"skills":{"CLAUDE.md":"# Claude Desktop Debian - Development Notes\n\n<!--\n  This file is read by Claude Code. The content below is duplicated in\n  AGENTS.md (read by other AI tools per the agents.md standard) so that\n  contributors using either receive the same instructions without needing\n  to cross-reference. Keep CLAUDE.md and AGENTS.md byte-identical below\n  the H1 title (the sync-policy comment above is the one place they\n  intentionally differ) — if you edit one, edit the other.\n-->\n\n## Required reading\n\nThese 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.\n\n- [`CONTRIBUTING.md`](CONTRIBUTING.md) — what we accept, what goes upstream, subsystem owners, AI-attribution policy.\n- [`docs/styleguides/bash_styleguide.md`](docs/styleguides/bash_styleguide.md) — shell-script conventions (forked from YSAP). Tabs, 80 cols, `[[ ]]`, no `set -e`, no `eval`.\n- [`docs/styleguides/docs_styleguide.md`](docs/styleguides/docs_styleguide.md) — page anatomy, naming, antipatterns for the `docs/` tree.\n- [`docs/index.md`](docs/index.md) — entry point for the rest of the repo docs.\n- [`SECURITY.md`](SECURITY.md) — vulnerability reporting; what's in scope vs. upstream.\n\nThis 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.\n\n## Project Overview\n\nThis 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](docs/decisions.md)) 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.\n\n## Learnings\n\nThe [`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. Docs whose subject no longer ships live in [`docs/archive/`](docs/archive/) with an obsolescence header — they stay findable as diagnosis records.\n\n- [`official-deb-rebase-verification.md`](docs/learnings/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\n- [`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 — still load-bearing for the two survivor patches\n- [`cross-build-host-vs-target.md`](docs/learnings/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\n- [`packaging-permissions.md`](docs/learnings/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\n- [`nix.md`](docs/learnings/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\n- [`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\n- [`wayland-global-shortcuts-portal.md`](docs/learnings/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](https://github.com/electron/electron/issues/51875)); wlroots (Niri/Sway/Hyprland) lack a portal GlobalShortcuts backend entirely\n- [`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; now first-party-reproducible → upstream report drafted\n- [`plugin-install.md`](docs/learnings/plugin-install.md) — Anthropic & Partners plugin install flow, gate logic, backend endpoints, and DevTools recipes\n- [`tray-rebuild-race.md`](docs/learnings/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\n- [`cowork-vm-daemon.md`](docs/learnings/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\n- [`test-harness-electron-hooks.md`](docs/learnings/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\n- [`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\n- [`config-wipe-guard.md`](docs/learnings/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-state` → `persisted.*` 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\n- [`quit-cleanup-scope-fence.md`](docs/learnings/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)\n- [`test-methodology-and-coverage.md`](docs/learnings/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)\n\nArchived (still useful as diagnosis records): [`docs/archive/linux-topbar-shim.md`](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/upstream-reports/)); [`docs/archive/cowork-linux-handover.md`](docs/archive/cowork-linux-handover.md) — the 2.x patch-based Cowork stack handover.\n\n## Code Style\n\nAll shell scripts in this project must follow the [Bash Style Guide](docs/styleguides/bash_styleguide.md). Key points:\n\n- Tabs for indentation, lines under 80 characters (exception: URLs and regex patterns)\n- Use `[[ ]]` for conditionals, `$(...)` for command substitution\n- Single quotes for literals, double quotes for expansions\n- Lowercase variables; UPPERCASE only for constants/exports\n- Use `local` in functions, avoid `set -e` and `eval`\n\n### Anti-patterns\n\n- **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`.\n- **Don't `eval`.** Use arrays for argv composition (`cmd \"${args[@]}\"`). `eval` defeats every parser and is a permanent SC2046 magnet.\n- **Don't use POSIX `[ ... ]`.** Always `[[ ... ]]`. POSIX `[` mis-parses unquoted expansions in ways `[[` does not.\n- **Don't backtick.** Always `$(...)`. Backticks don't nest cleanly and conflict with markdown when patches are pasted into PR comments.\n- **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.\n- **Don't wrap commands in `if cmd; then true; else false; fi`-style scaffolding.** Just `cmd` — the exit code is already there.\n- **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.\n\n### Linting\n\nShell scripts are checked with `shellcheck` and GitHub Actions workflows with `actionlint` before pushing. When lint issues are found:\n\n1. **Fix the code** - Correct the underlying issue rather than suppressing the warning\n2. **Disable directives are a last resort** - Only use `# shellcheck disable=SCXXXX` when:\n   - The warning is a false positive\n   - The pattern is intentional and unavoidable\n   - Always add a comment explaining why the disable is needed\n3. **Run `/lint` to check manually** - Use this skill to check for issues before pushing\n\n## Docs\n\n- **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`](docs/styleguides/docs_styleguide.md).\n- **Lowercase kebab-case filenames** for everything in `docs/`. Order belongs in [`docs/index.md`](docs/index.md), not filenames or numeric prefixes.\n- **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`.\n- **Subsystem deep-dives go under [`docs/learnings/`](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.\n- **Decisions go in [`docs/decisions.md`](docs/decisions.md) (ADR format).** Don't relitigate a settled direction inside a how-to page; link the decision instead.\n- **Troubleshooting headings are the literal symptom**, not editorialized prose. `## Black screen on Fedora KDE under Wayland`, not `## Troubles with Wayland`. Search ranks headings.\n- **CHANGELOG follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/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`](CHANGELOG.md) for the current state and [`RELEASING.md`](RELEASING.md) for when entries get promoted from `[Unreleased]`.\n\n## GitHub Workflow\n\n### General Approach\n\n- Use `gh` CLI for all GitHub interactions\n- Create branches based on issue numbers: `fix/123-description` or `feature/123-description`\n- Reference issues in commits and PRs with `#123` or `Fixes #123`\n- After creating a PR, add a comment to the related issue with a summary and link to the PR\n\n### Investigating Issues\n\nFor older issues, review the state of the code when the issue was raised - it may have already been addressed:\n\n```bash\n# Get issue creation date\ngh issue view 123 --json createdAt\n\n# Find the commit just before the issue was created\ngit log --oneline --until=\"2025-08-23T08:48:35Z\" -1\n\n# View a file at that point in time\ngit show <commit>:path/to/file.sh\n\n# Search for relevant changes since the issue was created\ngit log --oneline --after=\"2025-08-23\" -- path/to/file.sh\n\n# View a specific commit that may have fixed the issue\ngit show <commit>\n```\n\nThis helps identify if the issue was already fixed, and allows referencing the specific commit in the response.\n\n### Attribution\n\n**For PR descriptions**, include full attribution:\n\n```\n---\nGenerated with [Claude Code](https://claude.ai/code)\nCo-Authored-By: Claude <model-name> <noreply@anthropic.com>\n<XX>% AI / <YY>% Human\nClaude: <what AI did>\nHuman: <what human did>\n```\n\n- Use the actual model name (e.g., `Claude Opus 4.5`, `Claude Sonnet 4`)\n- The percentage split should honestly reflect the contribution balance for that specific work\n- This provides a trackable record of AI-assisted development over time\n\n**For issues and comments**, use simplified attribution:\n\n```\n---\nWritten by Claude <model-name> via [Claude Code](https://claude.ai/code)\n```\n\n**For commits**, include a Co-Authored-By trailer:\n\n```\nCo-Authored-By: Claude <claude@anthropic.com>\n```\n\n### Contributor Credits\n\n[`ACKNOWLEDGMENTS.md`](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:\n\n1. **Merging an external PR** — Add the author to the list with a link to their GitHub profile and a brief description of their contribution.\n2. **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.\n\nContributors are listed in chronological order: inspirational projects first (k3d3, emsi, leobuskin), then contributors ordered by when their contribution was merged or implemented.\n\n## Working with Minified JavaScript\n\n### Important Guidelines\n\n1. **Always use regex patterns** when modifying the source JavaScript. Patches live in `scripts/patches/*.sh` — `app-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`](docs/learnings/patching-minified-js.md).\n\n2. **The beautified code in `build-reference/` has different spacing** than the actual minified code in the app. Patterns must handle both:\n   - Minified: `oe.nativeTheme.on(\"updated\",()=>{`\n   - Beautified: `oe.nativeTheme.on(\"updated\", () => {`\n\n3. **Use `-E` flag with sed** for extended regex support when patterns need grouping or alternation.\n\n4. **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:\n   ```bash\n   # The minified Quick Entry window var, anchored on a stable literal\n   quick_var=$(grep -oP '[$\\w]+(?=\\.setAlwaysOnTop\\(\\s*!0\\s*,\\s*\"pop-up-menu\"\\))' \\\n       \"$index_js\")\n   ```\n\n5. **Handle optional whitespace** in regex patterns:\n   ```bash\n   # Bad: assumes no spaces\n   sed -i 's/oe.nativeTheme.on(\"updated\",()=>{/...'\n\n   # Good: handles optional whitespace\n   sed -i -E 's/(oe\\.nativeTheme\\.on\\(\\s*\"updated\"\\s*,\\s*\\(\\)\\s*=>\\s*\\{)/...'\n   ```\n\n### Reference Files\n\n- `build-reference/app-extracted/` - Extracted and beautified source for analysis\n- `build-reference/tray-icons/` - Tray icon assets for reference\n\n## Patch Orchestration (patch-zero)\n\n`scripts/patches/app-asar.sh` owns the asar patch stage:\n\n- **`active_patches` array** — the only place a patch gets wired in. Empty array ⇒ no extract, no repack, official `app.asar` ships byte-identical.\n- **productName guard** — the build fails if upstream's `productName` stops matching `WM_CLASS` (breaks `StartupWMClass` in every `.desktop` file).\n- **Upstream tripwires (AU-1/MB-1)** — the build fails if the official bundle stops shipping `apt_channel_pending` (autoupdater still pending, see [D-001](docs/decisions.md)) or `menuBarEnabled:!0` (menu-bar default). These replace the per-patch WARNINGs that left with the v3.0.0 deletions.\n- **Config-wipe recovery is launcher-side, not an asar patch** — `backup_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`](docs/learnings/config-wipe-guard.md).\n- **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.\n\nThe 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](docs/decisions.md).\n\n## Setting Up build-reference\n\nIf `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).\n\n### Prerequisites\n\n```bash\n# Install required tools (ar comes from binutils)\nsudo apt install binutils wget xz-utils zstd nodejs npm\n\n# Install asar and prettier globally (or use npx)\nnpm install -g @electron/asar prettier\n```\n\n### Step 1: Download the official .deb\n\nThe pinned version, pool path, and SHA-256 live in `scripts/setup/official-deb.sh` (`OFFICIAL_DEB_*`). To fetch the pinned amd64 build:\n\n```bash\nmkdir -p build-reference && cd build-reference\n\n# Read the current pin\nsource ../scripts/setup/official-deb.sh 2>/dev/null || true\nwget -O claude-desktop.deb \\\n  \"https://downloads.claude.ai/claude-desktop/apt/stable/$OFFICIAL_DEB_POOL_AMD64\"\necho \"$OFFICIAL_DEB_SHA256_AMD64  claude-desktop.deb\" | sha256sum -c\n```\n\nTo inspect the newest pool entry instead, resolve it from the Packages index (`resolve_official_deb` in `official-deb.sh` does the same thing):\n\n```bash\ncurl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n  | awk -v RS='' '/claude-desktop/' | grep -E '^(Version|Filename|SHA256):'\n```\n\n### Step 2: Extract the .deb\n\nNo dpkg required — `ar` + `tar` handle every member (the data member has shipped as both `.tar.zst` and `.tar.xz`; check with `ar t`):\n\n```bash\nar t claude-desktop.deb                     # list members\nar p claude-desktop.deb data.tar.xz | tar -J -x   # or --zstd for .tar.zst\n\n# The app tree lands at usr/lib/claude-desktop/\ncp usr/lib/claude-desktop/resources/app.asar .\ncp -a usr/lib/claude-desktop/resources/app.asar.unpacked .\n\n# Optional: hicolor icons for reference\ncp -a usr/share/icons/hicolor tray-icons\n```\n\n### Step 3: Extract app.asar\n\n```bash\nasar extract app.asar app-extracted\n```\n\n### Step 4: Beautify the JavaScript Files\n\nThe extracted JS files are minified. Use prettier to make them readable:\n\n```bash\n# Beautify all JS files in the build directory. Since 1.19367.0 the main\n# process is code-split, so index.js is a tiny stub and the real main\n# code lives in index.chunk-<hash>.js — the glob covers every chunk.\nnpx prettier --write \"app-extracted/.vite/build/*.js\"\n\n# The main-process chunk is the biggest .vite/build/*.js (index.js just\n# require()s it). Resolve it from the stub if you want to beautify only it:\nmain_chunk=$(grep -oP 'require\\(\"\\./\\Kindex\\.chunk-[^\"]+\\.js(?=\"\\))' \\\n    app-extracted/.vite/build/index.js)\nnpx prettier --write \"app-extracted/.vite/build/$main_chunk\"\n```\n\n### Step 5: Clean Up (Optional)\n\n```bash\n# Keep only what's needed for reference\nrm -rf usr claude-desktop.deb\nrm -rf app.asar app.asar.unpacked  # Keep only app-extracted\n```\n\n### Final Structure\n\n```\nbuild-reference/\n├── app-extracted/\n│   ├── .vite/\n│   │   ├── build/\n│   │   │   ├── index.js                 # Main-process entry stub\n│   │   │   ├── index.chunk-<hash>.js     # Main process (code-split, 1.19367.0+)\n│   │   │   ├── mainWindow.js             # Main window preload\n│   │   │   ├── mainView.js               # Main view preload\n│   │   │   └── ...\n│   │   └── renderer/\n│   │       └── ...\n│   ├── node_modules/\n│   │   └── @ant/claude-native/   # Rust native binding (real on Linux)\n│   └── package.json\n└── tray-icons/                   # Official hicolor icons (optional)\n```\n\nRemember that patterns verified against beautified output need the whitespace-tolerant form when applied to the shipped minified bytes (see the guidelines above).\n\n## Adding New Package Formats or Repositories\n\nWhen adding support for new distribution formats (e.g., RPM, Flatpak, Snap) or package repositories, follow these guidelines to avoid iterative debugging in CI.\n\n### Research Before Implementing\n\n1. **Understand the target system's constraints** - Each package format has specific rules:\n   - Version string formats (e.g., RPM cannot have hyphens in Version field)\n   - Required metadata fields\n   - Signing requirements and tools\n\n2. **Search for existing CI implementations** - Look for \"GitHub Actions [format] signing\" or similar. Existing workflows reveal required flags, environment setup, and common pitfalls.\n\n3. **Check tool behavior in non-interactive environments** - CI has no TTY. Tools like GPG need flags like `--batch` and `--yes` to work without prompts.\n\n### Consider Concurrency\n\n1. **Multiple jobs writing to the same branch will race** - If APT and DNF repos both push to `gh-pages`, add:\n   - Job dependencies (`needs: [other-job]`), or\n   - Retry loops with `git pull --rebase` before push\n\n2. **External processes may also modify branches** - GitHub Pages deployment runs automatically and can cause push conflicts.\n\n### Test the Full Pipeline\n\n1. **Test CI steps locally first** - Run the signing/packaging commands manually to catch errors before committing.\n\n2. **Use a test tag for new infrastructure** - Create a non-release tag to validate the full CI pipeline before merging to main.\n\n3. **Verify the end-user experience** - After CI succeeds, actually test the install commands from the README on a clean system.\n\n### Common CI Pitfalls\n\n| Issue | Solution |\n|-------|----------|\n| GPG \"cannot open /dev/tty\" | Add `--batch` flag |\n| GPG \"File exists\" error | Add `--yes` flag to overwrite |\n| Push rejected (ref changed) | Add `git pull --rebase` before push, with retry loop |\n| Version format invalid | Research target format's version constraints upfront |\n| Signing key not found | Ensure key is imported before signing step, check key ID output |\n\n## CI/CD\n\n### Triggering Builds\n\n```bash\n# Trigger CI on a branch\ngh workflow run CI --ref branch-name\n\n# Watch the run\ngh run watch RUN_ID\n\n# Download artifacts\ngh run download RUN_ID -n artifact-name\n```\n\n### Build Artifacts\n\n- `claude-desktop-unofficial_VERSION_amd64.deb` / `claude-desktop-unofficial_VERSION_arm64.deb` - Debian packages\n- `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`\n- `claude-desktop-unofficial-VERSION-1.x86_64.rpm` / `claude-desktop-unofficial-VERSION-1.aarch64.rpm` - RPM packages\n- `claude-desktop-unofficial-VERSION-amd64.AppImage` / `claude-desktop-unofficial-VERSION-arm64.AppImage` - AppImages (+ `.zsync` in CI)\n- `result/` - Nix build output (symlink, gitignored; the derivation is a stub until the @typedrat rework lands)\n\nOne cross-building `build.yml` produces all of these from `ubuntu-latest` via the `--arch` input (see [`docs/learnings/cross-build-host-vs-target.md`](docs/learnings/cross-build-host-vs-target.md) for the host-vs-target trap).\n\n## Distribution\n\nAPT 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.\n\nKey files:\n- `worker/src/worker.js` — Worker source\n- `worker/wrangler.toml` — Worker config (route, `custom_domain = true`)\n- `.github/workflows/deploy-worker.yml` — deploys on push to `main` when `worker/**` changes\n- `.github/workflows/apt-repo-heartbeat.yml` — daily chain validation, auto-opens tracking issue on failure\n- `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\n\nRepo secrets: `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`. Token scoped to the \"Edit Cloudflare Workers\" template.\n\nFull 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).\n\n## Testing\n\n### Local Build\n\n```bash\n./build.sh --build appimage --clean no\n```\n\n### Nix Build\n\n```bash\nnix build .#claude-desktop\nnix build .#claude-desktop-fhs\n```\n\nThe 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`](docs/learnings/nix.md)).\n\n### Testing AppImage\n\n```bash\n# Run with logging\n./test-build/claude-desktop-*.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log\n```\n\n## Debugging Workflow\n\n### Inspecting the Running App's Code\n\n```bash\n# Find the mounted AppImage path\nmount | grep claude\n# Example: /tmp/.mount_claudeXXXXXX\n\n# Extract the running app's asar for inspection (official bare\n# co-located layout: ELF + chrome-sandbox + resources/ side by side)\nnpx asar extract /tmp/.mount_claudeXXXXXX/usr/lib/claude-desktop/resources/app.asar /tmp/claude-inspect\n\n# Search for patterns in the extracted code. Since 1.19367.0 the main\n# process is code-split, so grep across all chunks (index.js is a stub);\n# main-process anchors live in index.chunk-<hash>.js.\ngrep -rn \"pattern\" /tmp/claude-inspect/.vite/build/\n```\n\n### Checking DBus/Tray Status\n\n```bash\n# List registered tray icons\ngdbus call --session --dest=org.kde.StatusNotifierWatcher \\\n  --object-path=/StatusNotifierWatcher \\\n  --method=org.freedesktop.DBus.Properties.Get \\\n  org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems\n\n# Find which process owns a DBus connection\ngdbus call --session --dest=org.freedesktop.DBus \\\n  --object-path=/org/freedesktop/DBus \\\n  --method=org.freedesktop.DBus.GetConnectionUnixProcessID \":1.XXXX\"\n```\n\n### Log Locations\n\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n- App logs: `~/.config/Claude/logs/`\n- Run with logging: `./app.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log`\n\n## Useful Locations\n\n- App data: `~/.config/Claude/`\n- Logs: `~/.config/Claude/logs/`\n- SingletonLock: `~/.config/Claude/SingletonLock`\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n\n## Versioning\n\nRelease versions are managed via two GitHub Actions repository variables (not files):\n\n- **`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.\n- **`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.\n\n### Tag format\n\nTags 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.\n\n```bash\n# Check current values\ngh variable get REPO_VERSION\ngh variable get CLAUDE_DESKTOP_VERSION\n\n# Bump repo version and tag a release\ngh variable set REPO_VERSION --body \"1.3.24\"\ngit tag \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\ngit push origin \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\n```\n\nWhen 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.\n\n## Common Gotchas\n\n- **`.zsync` files** - Used for delta updates, can be ignored/deleted\n- **AppImage mount points** - Running AppImages mount to `/tmp/.mount_claude*`; check with `mount | grep claude`\n- **Killing the app** - Must kill all electron child processes, not just the main one:\n  ```bash\n  pkill -9 -f \"mount_claude\"\n  ```\n- **SingletonLock** - If app won't start, check for stale lock: `~/.config/Claude/SingletonLock`\n- **Node version** - Build requires Node.js; the script downloads its own if needed (keyed to the HOST arch — see the cross-build learning)\n- **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:\n  ```bash\n  # Check repo variable (source of truth)\n  gh variable get CLAUDE_DESKTOP_VERSION\n\n  # Check the pinned version on your branch\n  grep -oP \"^OFFICIAL_DEB_VERSION='\\K[^']+\" scripts/setup/official-deb.sh\n\n  # What the official pool currently serves\n  curl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n    | grep -E '^Version:' | sort -V | tail -1\n  ```\n- **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\n","AGENTS.md":"# AGENTS.md\n\n<!--\n  This file is read by AI tools that support the agents.md vendor-neutral\n  standard. The content below is duplicated in CLAUDE.md (read by Claude\n  Code) so that contributors using either receive the same instructions\n  without needing to cross-reference. Keep CLAUDE.md and AGENTS.md\n  byte-identical below the H1 title (the sync-policy comment above is the\n  one place they intentionally differ) — if you edit one, edit the other.\n-->\n\n## Required reading\n\nThese 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.\n\n- [`CONTRIBUTING.md`](CONTRIBUTING.md) — what we accept, what goes upstream, subsystem owners, AI-attribution policy.\n- [`docs/styleguides/bash_styleguide.md`](docs/styleguides/bash_styleguide.md) — shell-script conventions (forked from YSAP). Tabs, 80 cols, `[[ ]]`, no `set -e`, no `eval`.\n- [`docs/styleguides/docs_styleguide.md`](docs/styleguides/docs_styleguide.md) — page anatomy, naming, antipatterns for the `docs/` tree.\n- [`docs/index.md`](docs/index.md) — entry point for the rest of the repo docs.\n- [`SECURITY.md`](SECURITY.md) — vulnerability reporting; what's in scope vs. upstream.\n\nThis 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.\n\n## Project Overview\n\nThis 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](docs/decisions.md)) 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.\n\n## Learnings\n\nThe [`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. Docs whose subject no longer ships live in [`docs/archive/`](docs/archive/) with an obsolescence header — they stay findable as diagnosis records.\n\n- [`official-deb-rebase-verification.md`](docs/learnings/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\n- [`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 — still load-bearing for the two survivor patches\n- [`cross-build-host-vs-target.md`](docs/learnings/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\n- [`packaging-permissions.md`](docs/learnings/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\n- [`nix.md`](docs/learnings/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\n- [`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\n- [`wayland-global-shortcuts-portal.md`](docs/learnings/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](https://github.com/electron/electron/issues/51875)); wlroots (Niri/Sway/Hyprland) lack a portal GlobalShortcuts backend entirely\n- [`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; now first-party-reproducible → upstream report drafted\n- [`plugin-install.md`](docs/learnings/plugin-install.md) — Anthropic & Partners plugin install flow, gate logic, backend endpoints, and DevTools recipes\n- [`tray-rebuild-race.md`](docs/learnings/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\n- [`cowork-vm-daemon.md`](docs/learnings/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\n- [`test-harness-electron-hooks.md`](docs/learnings/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\n- [`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\n- [`config-wipe-guard.md`](docs/learnings/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-state` → `persisted.*` 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\n- [`quit-cleanup-scope-fence.md`](docs/learnings/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)\n- [`test-methodology-and-coverage.md`](docs/learnings/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)\n\nArchived (still useful as diagnosis records): [`docs/archive/linux-topbar-shim.md`](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/upstream-reports/)); [`docs/archive/cowork-linux-handover.md`](docs/archive/cowork-linux-handover.md) — the 2.x patch-based Cowork stack handover.\n\n## Code Style\n\nAll shell scripts in this project must follow the [Bash Style Guide](docs/styleguides/bash_styleguide.md). Key points:\n\n- Tabs for indentation, lines under 80 characters (exception: URLs and regex patterns)\n- Use `[[ ]]` for conditionals, `$(...)` for command substitution\n- Single quotes for literals, double quotes for expansions\n- Lowercase variables; UPPERCASE only for constants/exports\n- Use `local` in functions, avoid `set -e` and `eval`\n\n### Anti-patterns\n\n- **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`.\n- **Don't `eval`.** Use arrays for argv composition (`cmd \"${args[@]}\"`). `eval` defeats every parser and is a permanent SC2046 magnet.\n- **Don't use POSIX `[ ... ]`.** Always `[[ ... ]]`. POSIX `[` mis-parses unquoted expansions in ways `[[` does not.\n- **Don't backtick.** Always `$(...)`. Backticks don't nest cleanly and conflict with markdown when patches are pasted into PR comments.\n- **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.\n- **Don't wrap commands in `if cmd; then true; else false; fi`-style scaffolding.** Just `cmd` — the exit code is already there.\n- **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.\n\n### Linting\n\nShell scripts are checked with `shellcheck` and GitHub Actions workflows with `actionlint` before pushing. When lint issues are found:\n\n1. **Fix the code** - Correct the underlying issue rather than suppressing the warning\n2. **Disable directives are a last resort** - Only use `# shellcheck disable=SCXXXX` when:\n   - The warning is a false positive\n   - The pattern is intentional and unavoidable\n   - Always add a comment explaining why the disable is needed\n3. **Run `/lint` to check manually** - Use this skill to check for issues before pushing\n\n## Docs\n\n- **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`](docs/styleguides/docs_styleguide.md).\n- **Lowercase kebab-case filenames** for everything in `docs/`. Order belongs in [`docs/index.md`](docs/index.md), not filenames or numeric prefixes.\n- **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`.\n- **Subsystem deep-dives go under [`docs/learnings/`](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.\n- **Decisions go in [`docs/decisions.md`](docs/decisions.md) (ADR format).** Don't relitigate a settled direction inside a how-to page; link the decision instead.\n- **Troubleshooting headings are the literal symptom**, not editorialized prose. `## Black screen on Fedora KDE under Wayland`, not `## Troubles with Wayland`. Search ranks headings.\n- **CHANGELOG follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/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`](CHANGELOG.md) for the current state and [`RELEASING.md`](RELEASING.md) for when entries get promoted from `[Unreleased]`.\n\n## GitHub Workflow\n\n### General Approach\n\n- Use `gh` CLI for all GitHub interactions\n- Create branches based on issue numbers: `fix/123-description` or `feature/123-description`\n- Reference issues in commits and PRs with `#123` or `Fixes #123`\n- After creating a PR, add a comment to the related issue with a summary and link to the PR\n\n### Investigating Issues\n\nFor older issues, review the state of the code when the issue was raised - it may have already been addressed:\n\n```bash\n# Get issue creation date\ngh issue view 123 --json createdAt\n\n# Find the commit just before the issue was created\ngit log --oneline --until=\"2025-08-23T08:48:35Z\" -1\n\n# View a file at that point in time\ngit show <commit>:path/to/file.sh\n\n# Search for relevant changes since the issue was created\ngit log --oneline --after=\"2025-08-23\" -- path/to/file.sh\n\n# View a specific commit that may have fixed the issue\ngit show <commit>\n```\n\nThis helps identify if the issue was already fixed, and allows referencing the specific commit in the response.\n\n### Attribution\n\n**For PR descriptions**, include full attribution:\n\n```\n---\nGenerated with [Claude Code](https://claude.ai/code)\nCo-Authored-By: Claude <model-name> <noreply@anthropic.com>\n<XX>% AI / <YY>% Human\nClaude: <what AI did>\nHuman: <what human did>\n```\n\n- Use the actual model name (e.g., `Claude Opus 4.5`, `Claude Sonnet 4`)\n- The percentage split should honestly reflect the contribution balance for that specific work\n- This provides a trackable record of AI-assisted development over time\n\n**For issues and comments**, use simplified attribution:\n\n```\n---\nWritten by Claude <model-name> via [Claude Code](https://claude.ai/code)\n```\n\n**For commits**, include a Co-Authored-By trailer:\n\n```\nCo-Authored-By: Claude <claude@anthropic.com>\n```\n\n### Contributor Credits\n\n[`ACKNOWLEDGMENTS.md`](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:\n\n1. **Merging an external PR** — Add the author to the list with a link to their GitHub profile and a brief description of their contribution.\n2. **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.\n\nContributors are listed in chronological order: inspirational projects first (k3d3, emsi, leobuskin), then contributors ordered by when their contribution was merged or implemented.\n\n## Working with Minified JavaScript\n\n### Important Guidelines\n\n1. **Always use regex patterns** when modifying the source JavaScript. Patches live in `scripts/patches/*.sh` — `app-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`](docs/learnings/patching-minified-js.md).\n\n2. **The beautified code in `build-reference/` has different spacing** than the actual minified code in the app. Patterns must handle both:\n   - Minified: `oe.nativeTheme.on(\"updated\",()=>{`\n   - Beautified: `oe.nativeTheme.on(\"updated\", () => {`\n\n3. **Use `-E` flag with sed** for extended regex support when patterns need grouping or alternation.\n\n4. **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:\n   ```bash\n   # The minified Quick Entry window var, anchored on a stable literal\n   quick_var=$(grep -oP '[$\\w]+(?=\\.setAlwaysOnTop\\(\\s*!0\\s*,\\s*\"pop-up-menu\"\\))' \\\n       \"$index_js\")\n   ```\n\n5. **Handle optional whitespace** in regex patterns:\n   ```bash\n   # Bad: assumes no spaces\n   sed -i 's/oe.nativeTheme.on(\"updated\",()=>{/...'\n\n   # Good: handles optional whitespace\n   sed -i -E 's/(oe\\.nativeTheme\\.on\\(\\s*\"updated\"\\s*,\\s*\\(\\)\\s*=>\\s*\\{)/...'\n   ```\n\n### Reference Files\n\n- `build-reference/app-extracted/` - Extracted and beautified source for analysis\n- `build-reference/tray-icons/` - Tray icon assets for reference\n\n## Patch Orchestration (patch-zero)\n\n`scripts/patches/app-asar.sh` owns the asar patch stage:\n\n- **`active_patches` array** — the only place a patch gets wired in. Empty array ⇒ no extract, no repack, official `app.asar` ships byte-identical.\n- **productName guard** — the build fails if upstream's `productName` stops matching `WM_CLASS` (breaks `StartupWMClass` in every `.desktop` file).\n- **Upstream tripwires (AU-1/MB-1)** — the build fails if the official bundle stops shipping `apt_channel_pending` (autoupdater still pending, see [D-001](docs/decisions.md)) or `menuBarEnabled:!0` (menu-bar default). These replace the per-patch WARNINGs that left with the v3.0.0 deletions.\n- **Config-wipe recovery is launcher-side, not an asar patch** — `backup_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`](docs/learnings/config-wipe-guard.md).\n- **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.\n\nThe 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](docs/decisions.md).\n\n## Setting Up build-reference\n\nIf `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).\n\n### Prerequisites\n\n```bash\n# Install required tools (ar comes from binutils)\nsudo apt install binutils wget xz-utils zstd nodejs npm\n\n# Install asar and prettier globally (or use npx)\nnpm install -g @electron/asar prettier\n```\n\n### Step 1: Download the official .deb\n\nThe pinned version, pool path, and SHA-256 live in `scripts/setup/official-deb.sh` (`OFFICIAL_DEB_*`). To fetch the pinned amd64 build:\n\n```bash\nmkdir -p build-reference && cd build-reference\n\n# Read the current pin\nsource ../scripts/setup/official-deb.sh 2>/dev/null || true\nwget -O claude-desktop.deb \\\n  \"https://downloads.claude.ai/claude-desktop/apt/stable/$OFFICIAL_DEB_POOL_AMD64\"\necho \"$OFFICIAL_DEB_SHA256_AMD64  claude-desktop.deb\" | sha256sum -c\n```\n\nTo inspect the newest pool entry instead, resolve it from the Packages index (`resolve_official_deb` in `official-deb.sh` does the same thing):\n\n```bash\ncurl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n  | awk -v RS='' '/claude-desktop/' | grep -E '^(Version|Filename|SHA256):'\n```\n\n### Step 2: Extract the .deb\n\nNo dpkg required — `ar` + `tar` handle every member (the data member has shipped as both `.tar.zst` and `.tar.xz`; check with `ar t`):\n\n```bash\nar t claude-desktop.deb                     # list members\nar p claude-desktop.deb data.tar.xz | tar -J -x   # or --zstd for .tar.zst\n\n# The app tree lands at usr/lib/claude-desktop/\ncp usr/lib/claude-desktop/resources/app.asar .\ncp -a usr/lib/claude-desktop/resources/app.asar.unpacked .\n\n# Optional: hicolor icons for reference\ncp -a usr/share/icons/hicolor tray-icons\n```\n\n### Step 3: Extract app.asar\n\n```bash\nasar extract app.asar app-extracted\n```\n\n### Step 4: Beautify the JavaScript Files\n\nThe extracted JS files are minified. Use prettier to make them readable:\n\n```bash\n# Beautify all JS files in the build directory. Since 1.19367.0 the main\n# process is code-split, so index.js is a tiny stub and the real main\n# code lives in index.chunk-<hash>.js — the glob covers every chunk.\nnpx prettier --write \"app-extracted/.vite/build/*.js\"\n\n# The main-process chunk is the biggest .vite/build/*.js (index.js just\n# require()s it). Resolve it from the stub if you want to beautify only it:\nmain_chunk=$(grep -oP 'require\\(\"\\./\\Kindex\\.chunk-[^\"]+\\.js(?=\"\\))' \\\n    app-extracted/.vite/build/index.js)\nnpx prettier --write \"app-extracted/.vite/build/$main_chunk\"\n```\n\n### Step 5: Clean Up (Optional)\n\n```bash\n# Keep only what's needed for reference\nrm -rf usr claude-desktop.deb\nrm -rf app.asar app.asar.unpacked  # Keep only app-extracted\n```\n\n### Final Structure\n\n```\nbuild-reference/\n├── app-extracted/\n│   ├── .vite/\n│   │   ├── build/\n│   │   │   ├── index.js                 # Main-process entry stub\n│   │   │   ├── index.chunk-<hash>.js     # Main process (code-split, 1.19367.0+)\n│   │   │   ├── mainWindow.js             # Main window preload\n│   │   │   ├── mainView.js               # Main view preload\n│   │   │   └── ...\n│   │   └── renderer/\n│   │       └── ...\n│   ├── node_modules/\n│   │   └── @ant/claude-native/   # Rust native binding (real on Linux)\n│   └── package.json\n└── tray-icons/                   # Official hicolor icons (optional)\n```\n\nRemember that patterns verified against beautified output need the whitespace-tolerant form when applied to the shipped minified bytes (see the guidelines above).\n\n## Adding New Package Formats or Repositories\n\nWhen adding support for new distribution formats (e.g., RPM, Flatpak, Snap) or package repositories, follow these guidelines to avoid iterative debugging in CI.\n\n### Research Before Implementing\n\n1. **Understand the target system's constraints** - Each package format has specific rules:\n   - Version string formats (e.g., RPM cannot have hyphens in Version field)\n   - Required metadata fields\n   - Signing requirements and tools\n\n2. **Search for existing CI implementations** - Look for \"GitHub Actions [format] signing\" or similar. Existing workflows reveal required flags, environment setup, and common pitfalls.\n\n3. **Check tool behavior in non-interactive environments** - CI has no TTY. Tools like GPG need flags like `--batch` and `--yes` to work without prompts.\n\n### Consider Concurrency\n\n1. **Multiple jobs writing to the same branch will race** - If APT and DNF repos both push to `gh-pages`, add:\n   - Job dependencies (`needs: [other-job]`), or\n   - Retry loops with `git pull --rebase` before push\n\n2. **External processes may also modify branches** - GitHub Pages deployment runs automatically and can cause push conflicts.\n\n### Test the Full Pipeline\n\n1. **Test CI steps locally first** - Run the signing/packaging commands manually to catch errors before committing.\n\n2. **Use a test tag for new infrastructure** - Create a non-release tag to validate the full CI pipeline before merging to main.\n\n3. **Verify the end-user experience** - After CI succeeds, actually test the install commands from the README on a clean system.\n\n### Common CI Pitfalls\n\n| Issue | Solution |\n|-------|----------|\n| GPG \"cannot open /dev/tty\" | Add `--batch` flag |\n| GPG \"File exists\" error | Add `--yes` flag to overwrite |\n| Push rejected (ref changed) | Add `git pull --rebase` before push, with retry loop |\n| Version format invalid | Research target format's version constraints upfront |\n| Signing key not found | Ensure key is imported before signing step, check key ID output |\n\n## CI/CD\n\n### Triggering Builds\n\n```bash\n# Trigger CI on a branch\ngh workflow run CI --ref branch-name\n\n# Watch the run\ngh run watch RUN_ID\n\n# Download artifacts\ngh run download RUN_ID -n artifact-name\n```\n\n### Build Artifacts\n\n- `claude-desktop-unofficial_VERSION_amd64.deb` / `claude-desktop-unofficial_VERSION_arm64.deb` - Debian packages\n- `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`\n- `claude-desktop-unofficial-VERSION-1.x86_64.rpm` / `claude-desktop-unofficial-VERSION-1.aarch64.rpm` - RPM packages\n- `claude-desktop-unofficial-VERSION-amd64.AppImage` / `claude-desktop-unofficial-VERSION-arm64.AppImage` - AppImages (+ `.zsync` in CI)\n- `result/` - Nix build output (symlink, gitignored; the derivation is a stub until the @typedrat rework lands)\n\nOne cross-building `build.yml` produces all of these from `ubuntu-latest` via the `--arch` input (see [`docs/learnings/cross-build-host-vs-target.md`](docs/learnings/cross-build-host-vs-target.md) for the host-vs-target trap).\n\n## Distribution\n\nAPT 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.\n\nKey files:\n- `worker/src/worker.js` — Worker source\n- `worker/wrangler.toml` — Worker config (route, `custom_domain = true`)\n- `.github/workflows/deploy-worker.yml` — deploys on push to `main` when `worker/**` changes\n- `.github/workflows/apt-repo-heartbeat.yml` — daily chain validation, auto-opens tracking issue on failure\n- `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\n\nRepo secrets: `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`. Token scoped to the \"Edit Cloudflare Workers\" template.\n\nFull 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).\n\n## Testing\n\n### Local Build\n\n```bash\n./build.sh --build appimage --clean no\n```\n\n### Nix Build\n\n```bash\nnix build .#claude-desktop\nnix build .#claude-desktop-fhs\n```\n\nThe 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`](docs/learnings/nix.md)).\n\n### Testing AppImage\n\n```bash\n# Run with logging\n./test-build/claude-desktop-*.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log\n```\n\n## Debugging Workflow\n\n### Inspecting the Running App's Code\n\n```bash\n# Find the mounted AppImage path\nmount | grep claude\n# Example: /tmp/.mount_claudeXXXXXX\n\n# Extract the running app's asar for inspection (official bare\n# co-located layout: ELF + chrome-sandbox + resources/ side by side)\nnpx asar extract /tmp/.mount_claudeXXXXXX/usr/lib/claude-desktop/resources/app.asar /tmp/claude-inspect\n\n# Search for patterns in the extracted code. Since 1.19367.0 the main\n# process is code-split, so grep across all chunks (index.js is a stub);\n# main-process anchors live in index.chunk-<hash>.js.\ngrep -rn \"pattern\" /tmp/claude-inspect/.vite/build/\n```\n\n### Checking DBus/Tray Status\n\n```bash\n# List registered tray icons\ngdbus call --session --dest=org.kde.StatusNotifierWatcher \\\n  --object-path=/StatusNotifierWatcher \\\n  --method=org.freedesktop.DBus.Properties.Get \\\n  org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems\n\n# Find which process owns a DBus connection\ngdbus call --session --dest=org.freedesktop.DBus \\\n  --object-path=/org/freedesktop/DBus \\\n  --method=org.freedesktop.DBus.GetConnectionUnixProcessID \":1.XXXX\"\n```\n\n### Log Locations\n\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n- App logs: `~/.config/Claude/logs/`\n- Run with logging: `./app.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log`\n\n## Useful Locations\n\n- App data: `~/.config/Claude/`\n- Logs: `~/.config/Claude/logs/`\n- SingletonLock: `~/.config/Claude/SingletonLock`\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n\n## Versioning\n\nRelease versions are managed via two GitHub Actions repository variables (not files):\n\n- **`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.\n- **`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.\n\n### Tag format\n\nTags 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.\n\n```bash\n# Check current values\ngh variable get REPO_VERSION\ngh variable get CLAUDE_DESKTOP_VERSION\n\n# Bump repo version and tag a release\ngh variable set REPO_VERSION --body \"1.3.24\"\ngit tag \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\ngit push origin \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\n```\n\nWhen 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.\n\n## Common Gotchas\n\n- **`.zsync` files** - Used for delta updates, can be ignored/deleted\n- **AppImage mount points** - Running AppImages mount to `/tmp/.mount_claude*`; check with `mount | grep claude`\n- **Killing the app** - Must kill all electron child processes, not just the main one:\n  ```bash\n  pkill -9 -f \"mount_claude\"\n  ```\n- **SingletonLock** - If app won't start, check for stale lock: `~/.config/Claude/SingletonLock`\n- **Node version** - Build requires Node.js; the script downloads its own if needed (keyed to the HOST arch — see the cross-build learning)\n- **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:\n  ```bash\n  # Check repo variable (source of truth)\n  gh variable get CLAUDE_DESKTOP_VERSION\n\n  # Check the pinned version on your branch\n  grep -oP \"^OFFICIAL_DEB_VERSION='\\K[^']+\" scripts/setup/official-deb.sh\n\n  # What the official pool currently serves\n  curl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n    | grep -E '^Version:' | sort -V | tail -1\n  ```\n- **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\n"},"files":{"CLAUDE.md":"# Claude Desktop Debian - Development Notes\n\n<!--\n  This file is read by Claude Code. The content below is duplicated in\n  AGENTS.md (read by other AI tools per the agents.md standard) so that\n  contributors using either receive the same instructions without needing\n  to cross-reference. Keep CLAUDE.md and AGENTS.md byte-identical below\n  the H1 title (the sync-policy comment above is the one place they\n  intentionally differ) — if you edit one, edit the other.\n-->\n\n## Required reading\n\nThese 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.\n\n- [`CONTRIBUTING.md`](CONTRIBUTING.md) — what we accept, what goes upstream, subsystem owners, AI-attribution policy.\n- [`docs/styleguides/bash_styleguide.md`](docs/styleguides/bash_styleguide.md) — shell-script conventions (forked from YSAP). Tabs, 80 cols, `[[ ]]`, no `set -e`, no `eval`.\n- [`docs/styleguides/docs_styleguide.md`](docs/styleguides/docs_styleguide.md) — page anatomy, naming, antipatterns for the `docs/` tree.\n- [`docs/index.md`](docs/index.md) — entry point for the rest of the repo docs.\n- [`SECURITY.md`](SECURITY.md) — vulnerability reporting; what's in scope vs. upstream.\n\nThis 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.\n\n## Project Overview\n\nThis 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](docs/decisions.md)) 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.\n\n## Learnings\n\nThe [`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. Docs whose subject no longer ships live in [`docs/archive/`](docs/archive/) with an obsolescence header — they stay findable as diagnosis records.\n\n- [`official-deb-rebase-verification.md`](docs/learnings/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\n- [`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 — still load-bearing for the two survivor patches\n- [`cross-build-host-vs-target.md`](docs/learnings/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\n- [`packaging-permissions.md`](docs/learnings/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\n- [`nix.md`](docs/learnings/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\n- [`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\n- [`wayland-global-shortcuts-portal.md`](docs/learnings/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](https://github.com/electron/electron/issues/51875)); wlroots (Niri/Sway/Hyprland) lack a portal GlobalShortcuts backend entirely\n- [`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; now first-party-reproducible → upstream report drafted\n- [`plugin-install.md`](docs/learnings/plugin-install.md) — Anthropic & Partners plugin install flow, gate logic, backend endpoints, and DevTools recipes\n- [`tray-rebuild-race.md`](docs/learnings/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\n- [`cowork-vm-daemon.md`](docs/learnings/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\n- [`test-harness-electron-hooks.md`](docs/learnings/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\n- [`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\n- [`config-wipe-guard.md`](docs/learnings/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-state` → `persisted.*` 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\n- [`quit-cleanup-scope-fence.md`](docs/learnings/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)\n- [`test-methodology-and-coverage.md`](docs/learnings/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)\n\nArchived (still useful as diagnosis records): [`docs/archive/linux-topbar-shim.md`](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/upstream-reports/)); [`docs/archive/cowork-linux-handover.md`](docs/archive/cowork-linux-handover.md) — the 2.x patch-based Cowork stack handover.\n\n## Code Style\n\nAll shell scripts in this project must follow the [Bash Style Guide](docs/styleguides/bash_styleguide.md). Key points:\n\n- Tabs for indentation, lines under 80 characters (exception: URLs and regex patterns)\n- Use `[[ ]]` for conditionals, `$(...)` for command substitution\n- Single quotes for literals, double quotes for expansions\n- Lowercase variables; UPPERCASE only for constants/exports\n- Use `local` in functions, avoid `set -e` and `eval`\n\n### Anti-patterns\n\n- **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`.\n- **Don't `eval`.** Use arrays for argv composition (`cmd \"${args[@]}\"`). `eval` defeats every parser and is a permanent SC2046 magnet.\n- **Don't use POSIX `[ ... ]`.** Always `[[ ... ]]`. POSIX `[` mis-parses unquoted expansions in ways `[[` does not.\n- **Don't backtick.** Always `$(...)`. Backticks don't nest cleanly and conflict with markdown when patches are pasted into PR comments.\n- **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.\n- **Don't wrap commands in `if cmd; then true; else false; fi`-style scaffolding.** Just `cmd` — the exit code is already there.\n- **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.\n\n### Linting\n\nShell scripts are checked with `shellcheck` and GitHub Actions workflows with `actionlint` before pushing. When lint issues are found:\n\n1. **Fix the code** - Correct the underlying issue rather than suppressing the warning\n2. **Disable directives are a last resort** - Only use `# shellcheck disable=SCXXXX` when:\n   - The warning is a false positive\n   - The pattern is intentional and unavoidable\n   - Always add a comment explaining why the disable is needed\n3. **Run `/lint` to check manually** - Use this skill to check for issues before pushing\n\n## Docs\n\n- **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`](docs/styleguides/docs_styleguide.md).\n- **Lowercase kebab-case filenames** for everything in `docs/`. Order belongs in [`docs/index.md`](docs/index.md), not filenames or numeric prefixes.\n- **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`.\n- **Subsystem deep-dives go under [`docs/learnings/`](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.\n- **Decisions go in [`docs/decisions.md`](docs/decisions.md) (ADR format).** Don't relitigate a settled direction inside a how-to page; link the decision instead.\n- **Troubleshooting headings are the literal symptom**, not editorialized prose. `## Black screen on Fedora KDE under Wayland`, not `## Troubles with Wayland`. Search ranks headings.\n- **CHANGELOG follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/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`](CHANGELOG.md) for the current state and [`RELEASING.md`](RELEASING.md) for when entries get promoted from `[Unreleased]`.\n\n## GitHub Workflow\n\n### General Approach\n\n- Use `gh` CLI for all GitHub interactions\n- Create branches based on issue numbers: `fix/123-description` or `feature/123-description`\n- Reference issues in commits and PRs with `#123` or `Fixes #123`\n- After creating a PR, add a comment to the related issue with a summary and link to the PR\n\n### Investigating Issues\n\nFor older issues, review the state of the code when the issue was raised - it may have already been addressed:\n\n```bash\n# Get issue creation date\ngh issue view 123 --json createdAt\n\n# Find the commit just before the issue was created\ngit log --oneline --until=\"2025-08-23T08:48:35Z\" -1\n\n# View a file at that point in time\ngit show <commit>:path/to/file.sh\n\n# Search for relevant changes since the issue was created\ngit log --oneline --after=\"2025-08-23\" -- path/to/file.sh\n\n# View a specific commit that may have fixed the issue\ngit show <commit>\n```\n\nThis helps identify if the issue was already fixed, and allows referencing the specific commit in the response.\n\n### Attribution\n\n**For PR descriptions**, include full attribution:\n\n```\n---\nGenerated with [Claude Code](https://claude.ai/code)\nCo-Authored-By: Claude <model-name> <noreply@anthropic.com>\n<XX>% AI / <YY>% Human\nClaude: <what AI did>\nHuman: <what human did>\n```\n\n- Use the actual model name (e.g., `Claude Opus 4.5`, `Claude Sonnet 4`)\n- The percentage split should honestly reflect the contribution balance for that specific work\n- This provides a trackable record of AI-assisted development over time\n\n**For issues and comments**, use simplified attribution:\n\n```\n---\nWritten by Claude <model-name> via [Claude Code](https://claude.ai/code)\n```\n\n**For commits**, include a Co-Authored-By trailer:\n\n```\nCo-Authored-By: Claude <claude@anthropic.com>\n```\n\n### Contributor Credits\n\n[`ACKNOWLEDGMENTS.md`](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:\n\n1. **Merging an external PR** — Add the author to the list with a link to their GitHub profile and a brief description of their contribution.\n2. **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.\n\nContributors are listed in chronological order: inspirational projects first (k3d3, emsi, leobuskin), then contributors ordered by when their contribution was merged or implemented.\n\n## Working with Minified JavaScript\n\n### Important Guidelines\n\n1. **Always use regex patterns** when modifying the source JavaScript. Patches live in `scripts/patches/*.sh` — `app-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`](docs/learnings/patching-minified-js.md).\n\n2. **The beautified code in `build-reference/` has different spacing** than the actual minified code in the app. Patterns must handle both:\n   - Minified: `oe.nativeTheme.on(\"updated\",()=>{`\n   - Beautified: `oe.nativeTheme.on(\"updated\", () => {`\n\n3. **Use `-E` flag with sed** for extended regex support when patterns need grouping or alternation.\n\n4. **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:\n   ```bash\n   # The minified Quick Entry window var, anchored on a stable literal\n   quick_var=$(grep -oP '[$\\w]+(?=\\.setAlwaysOnTop\\(\\s*!0\\s*,\\s*\"pop-up-menu\"\\))' \\\n       \"$index_js\")\n   ```\n\n5. **Handle optional whitespace** in regex patterns:\n   ```bash\n   # Bad: assumes no spaces\n   sed -i 's/oe.nativeTheme.on(\"updated\",()=>{/...'\n\n   # Good: handles optional whitespace\n   sed -i -E 's/(oe\\.nativeTheme\\.on\\(\\s*\"updated\"\\s*,\\s*\\(\\)\\s*=>\\s*\\{)/...'\n   ```\n\n### Reference Files\n\n- `build-reference/app-extracted/` - Extracted and beautified source for analysis\n- `build-reference/tray-icons/` - Tray icon assets for reference\n\n## Patch Orchestration (patch-zero)\n\n`scripts/patches/app-asar.sh` owns the asar patch stage:\n\n- **`active_patches` array** — the only place a patch gets wired in. Empty array ⇒ no extract, no repack, official `app.asar` ships byte-identical.\n- **productName guard** — the build fails if upstream's `productName` stops matching `WM_CLASS` (breaks `StartupWMClass` in every `.desktop` file).\n- **Upstream tripwires (AU-1/MB-1)** — the build fails if the official bundle stops shipping `apt_channel_pending` (autoupdater still pending, see [D-001](docs/decisions.md)) or `menuBarEnabled:!0` (menu-bar default). These replace the per-patch WARNINGs that left with the v3.0.0 deletions.\n- **Config-wipe recovery is launcher-side, not an asar patch** — `backup_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`](docs/learnings/config-wipe-guard.md).\n- **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.\n\nThe 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](docs/decisions.md).\n\n## Setting Up build-reference\n\nIf `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).\n\n### Prerequisites\n\n```bash\n# Install required tools (ar comes from binutils)\nsudo apt install binutils wget xz-utils zstd nodejs npm\n\n# Install asar and prettier globally (or use npx)\nnpm install -g @electron/asar prettier\n```\n\n### Step 1: Download the official .deb\n\nThe pinned version, pool path, and SHA-256 live in `scripts/setup/official-deb.sh` (`OFFICIAL_DEB_*`). To fetch the pinned amd64 build:\n\n```bash\nmkdir -p build-reference && cd build-reference\n\n# Read the current pin\nsource ../scripts/setup/official-deb.sh 2>/dev/null || true\nwget -O claude-desktop.deb \\\n  \"https://downloads.claude.ai/claude-desktop/apt/stable/$OFFICIAL_DEB_POOL_AMD64\"\necho \"$OFFICIAL_DEB_SHA256_AMD64  claude-desktop.deb\" | sha256sum -c\n```\n\nTo inspect the newest pool entry instead, resolve it from the Packages index (`resolve_official_deb` in `official-deb.sh` does the same thing):\n\n```bash\ncurl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n  | awk -v RS='' '/claude-desktop/' | grep -E '^(Version|Filename|SHA256):'\n```\n\n### Step 2: Extract the .deb\n\nNo dpkg required — `ar` + `tar` handle every member (the data member has shipped as both `.tar.zst` and `.tar.xz`; check with `ar t`):\n\n```bash\nar t claude-desktop.deb                     # list members\nar p claude-desktop.deb data.tar.xz | tar -J -x   # or --zstd for .tar.zst\n\n# The app tree lands at usr/lib/claude-desktop/\ncp usr/lib/claude-desktop/resources/app.asar .\ncp -a usr/lib/claude-desktop/resources/app.asar.unpacked .\n\n# Optional: hicolor icons for reference\ncp -a usr/share/icons/hicolor tray-icons\n```\n\n### Step 3: Extract app.asar\n\n```bash\nasar extract app.asar app-extracted\n```\n\n### Step 4: Beautify the JavaScript Files\n\nThe extracted JS files are minified. Use prettier to make them readable:\n\n```bash\n# Beautify all JS files in the build directory. Since 1.19367.0 the main\n# process is code-split, so index.js is a tiny stub and the real main\n# code lives in index.chunk-<hash>.js — the glob covers every chunk.\nnpx prettier --write \"app-extracted/.vite/build/*.js\"\n\n# The main-process chunk is the biggest .vite/build/*.js (index.js just\n# require()s it). Resolve it from the stub if you want to beautify only it:\nmain_chunk=$(grep -oP 'require\\(\"\\./\\Kindex\\.chunk-[^\"]+\\.js(?=\"\\))' \\\n    app-extracted/.vite/build/index.js)\nnpx prettier --write \"app-extracted/.vite/build/$main_chunk\"\n```\n\n### Step 5: Clean Up (Optional)\n\n```bash\n# Keep only what's needed for reference\nrm -rf usr claude-desktop.deb\nrm -rf app.asar app.asar.unpacked  # Keep only app-extracted\n```\n\n### Final Structure\n\n```\nbuild-reference/\n├── app-extracted/\n│   ├── .vite/\n│   │   ├── build/\n│   │   │   ├── index.js                 # Main-process entry stub\n│   │   │   ├── index.chunk-<hash>.js     # Main process (code-split, 1.19367.0+)\n│   │   │   ├── mainWindow.js             # Main window preload\n│   │   │   ├── mainView.js               # Main view preload\n│   │   │   └── ...\n│   │   └── renderer/\n│   │       └── ...\n│   ├── node_modules/\n│   │   └── @ant/claude-native/   # Rust native binding (real on Linux)\n│   └── package.json\n└── tray-icons/                   # Official hicolor icons (optional)\n```\n\nRemember that patterns verified against beautified output need the whitespace-tolerant form when applied to the shipped minified bytes (see the guidelines above).\n\n## Adding New Package Formats or Repositories\n\nWhen adding support for new distribution formats (e.g., RPM, Flatpak, Snap) or package repositories, follow these guidelines to avoid iterative debugging in CI.\n\n### Research Before Implementing\n\n1. **Understand the target system's constraints** - Each package format has specific rules:\n   - Version string formats (e.g., RPM cannot have hyphens in Version field)\n   - Required metadata fields\n   - Signing requirements and tools\n\n2. **Search for existing CI implementations** - Look for \"GitHub Actions [format] signing\" or similar. Existing workflows reveal required flags, environment setup, and common pitfalls.\n\n3. **Check tool behavior in non-interactive environments** - CI has no TTY. Tools like GPG need flags like `--batch` and `--yes` to work without prompts.\n\n### Consider Concurrency\n\n1. **Multiple jobs writing to the same branch will race** - If APT and DNF repos both push to `gh-pages`, add:\n   - Job dependencies (`needs: [other-job]`), or\n   - Retry loops with `git pull --rebase` before push\n\n2. **External processes may also modify branches** - GitHub Pages deployment runs automatically and can cause push conflicts.\n\n### Test the Full Pipeline\n\n1. **Test CI steps locally first** - Run the signing/packaging commands manually to catch errors before committing.\n\n2. **Use a test tag for new infrastructure** - Create a non-release tag to validate the full CI pipeline before merging to main.\n\n3. **Verify the end-user experience** - After CI succeeds, actually test the install commands from the README on a clean system.\n\n### Common CI Pitfalls\n\n| Issue | Solution |\n|-------|----------|\n| GPG \"cannot open /dev/tty\" | Add `--batch` flag |\n| GPG \"File exists\" error | Add `--yes` flag to overwrite |\n| Push rejected (ref changed) | Add `git pull --rebase` before push, with retry loop |\n| Version format invalid | Research target format's version constraints upfront |\n| Signing key not found | Ensure key is imported before signing step, check key ID output |\n\n## CI/CD\n\n### Triggering Builds\n\n```bash\n# Trigger CI on a branch\ngh workflow run CI --ref branch-name\n\n# Watch the run\ngh run watch RUN_ID\n\n# Download artifacts\ngh run download RUN_ID -n artifact-name\n```\n\n### Build Artifacts\n\n- `claude-desktop-unofficial_VERSION_amd64.deb` / `claude-desktop-unofficial_VERSION_arm64.deb` - Debian packages\n- `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`\n- `claude-desktop-unofficial-VERSION-1.x86_64.rpm` / `claude-desktop-unofficial-VERSION-1.aarch64.rpm` - RPM packages\n- `claude-desktop-unofficial-VERSION-amd64.AppImage` / `claude-desktop-unofficial-VERSION-arm64.AppImage` - AppImages (+ `.zsync` in CI)\n- `result/` - Nix build output (symlink, gitignored; the derivation is a stub until the @typedrat rework lands)\n\nOne cross-building `build.yml` produces all of these from `ubuntu-latest` via the `--arch` input (see [`docs/learnings/cross-build-host-vs-target.md`](docs/learnings/cross-build-host-vs-target.md) for the host-vs-target trap).\n\n## Distribution\n\nAPT 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.\n\nKey files:\n- `worker/src/worker.js` — Worker source\n- `worker/wrangler.toml` — Worker config (route, `custom_domain = true`)\n- `.github/workflows/deploy-worker.yml` — deploys on push to `main` when `worker/**` changes\n- `.github/workflows/apt-repo-heartbeat.yml` — daily chain validation, auto-opens tracking issue on failure\n- `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\n\nRepo secrets: `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`. Token scoped to the \"Edit Cloudflare Workers\" template.\n\nFull 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).\n\n## Testing\n\n### Local Build\n\n```bash\n./build.sh --build appimage --clean no\n```\n\n### Nix Build\n\n```bash\nnix build .#claude-desktop\nnix build .#claude-desktop-fhs\n```\n\nThe 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`](docs/learnings/nix.md)).\n\n### Testing AppImage\n\n```bash\n# Run with logging\n./test-build/claude-desktop-*.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log\n```\n\n## Debugging Workflow\n\n### Inspecting the Running App's Code\n\n```bash\n# Find the mounted AppImage path\nmount | grep claude\n# Example: /tmp/.mount_claudeXXXXXX\n\n# Extract the running app's asar for inspection (official bare\n# co-located layout: ELF + chrome-sandbox + resources/ side by side)\nnpx asar extract /tmp/.mount_claudeXXXXXX/usr/lib/claude-desktop/resources/app.asar /tmp/claude-inspect\n\n# Search for patterns in the extracted code. Since 1.19367.0 the main\n# process is code-split, so grep across all chunks (index.js is a stub);\n# main-process anchors live in index.chunk-<hash>.js.\ngrep -rn \"pattern\" /tmp/claude-inspect/.vite/build/\n```\n\n### Checking DBus/Tray Status\n\n```bash\n# List registered tray icons\ngdbus call --session --dest=org.kde.StatusNotifierWatcher \\\n  --object-path=/StatusNotifierWatcher \\\n  --method=org.freedesktop.DBus.Properties.Get \\\n  org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems\n\n# Find which process owns a DBus connection\ngdbus call --session --dest=org.freedesktop.DBus \\\n  --object-path=/org/freedesktop/DBus \\\n  --method=org.freedesktop.DBus.GetConnectionUnixProcessID \":1.XXXX\"\n```\n\n### Log Locations\n\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n- App logs: `~/.config/Claude/logs/`\n- Run with logging: `./app.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log`\n\n## Useful Locations\n\n- App data: `~/.config/Claude/`\n- Logs: `~/.config/Claude/logs/`\n- SingletonLock: `~/.config/Claude/SingletonLock`\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n\n## Versioning\n\nRelease versions are managed via two GitHub Actions repository variables (not files):\n\n- **`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.\n- **`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.\n\n### Tag format\n\nTags 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.\n\n```bash\n# Check current values\ngh variable get REPO_VERSION\ngh variable get CLAUDE_DESKTOP_VERSION\n\n# Bump repo version and tag a release\ngh variable set REPO_VERSION --body \"1.3.24\"\ngit tag \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\ngit push origin \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\n```\n\nWhen 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.\n\n## Common Gotchas\n\n- **`.zsync` files** - Used for delta updates, can be ignored/deleted\n- **AppImage mount points** - Running AppImages mount to `/tmp/.mount_claude*`; check with `mount | grep claude`\n- **Killing the app** - Must kill all electron child processes, not just the main one:\n  ```bash\n  pkill -9 -f \"mount_claude\"\n  ```\n- **SingletonLock** - If app won't start, check for stale lock: `~/.config/Claude/SingletonLock`\n- **Node version** - Build requires Node.js; the script downloads its own if needed (keyed to the HOST arch — see the cross-build learning)\n- **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:\n  ```bash\n  # Check repo variable (source of truth)\n  gh variable get CLAUDE_DESKTOP_VERSION\n\n  # Check the pinned version on your branch\n  grep -oP \"^OFFICIAL_DEB_VERSION='\\K[^']+\" scripts/setup/official-deb.sh\n\n  # What the official pool currently serves\n  curl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n    | grep -E '^Version:' | sort -V | tail -1\n  ```\n- **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\n","AGENTS.md":"# AGENTS.md\n\n<!--\n  This file is read by AI tools that support the agents.md vendor-neutral\n  standard. The content below is duplicated in CLAUDE.md (read by Claude\n  Code) so that contributors using either receive the same instructions\n  without needing to cross-reference. Keep CLAUDE.md and AGENTS.md\n  byte-identical below the H1 title (the sync-policy comment above is the\n  one place they intentionally differ) — if you edit one, edit the other.\n-->\n\n## Required reading\n\nThese 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.\n\n- [`CONTRIBUTING.md`](CONTRIBUTING.md) — what we accept, what goes upstream, subsystem owners, AI-attribution policy.\n- [`docs/styleguides/bash_styleguide.md`](docs/styleguides/bash_styleguide.md) — shell-script conventions (forked from YSAP). Tabs, 80 cols, `[[ ]]`, no `set -e`, no `eval`.\n- [`docs/styleguides/docs_styleguide.md`](docs/styleguides/docs_styleguide.md) — page anatomy, naming, antipatterns for the `docs/` tree.\n- [`docs/index.md`](docs/index.md) — entry point for the rest of the repo docs.\n- [`SECURITY.md`](SECURITY.md) — vulnerability reporting; what's in scope vs. upstream.\n\nThis 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.\n\n## Project Overview\n\nThis 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](docs/decisions.md)) 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.\n\n## Learnings\n\nThe [`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. Docs whose subject no longer ships live in [`docs/archive/`](docs/archive/) with an obsolescence header — they stay findable as diagnosis records.\n\n- [`official-deb-rebase-verification.md`](docs/learnings/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\n- [`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 — still load-bearing for the two survivor patches\n- [`cross-build-host-vs-target.md`](docs/learnings/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\n- [`packaging-permissions.md`](docs/learnings/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\n- [`nix.md`](docs/learnings/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\n- [`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\n- [`wayland-global-shortcuts-portal.md`](docs/learnings/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](https://github.com/electron/electron/issues/51875)); wlroots (Niri/Sway/Hyprland) lack a portal GlobalShortcuts backend entirely\n- [`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; now first-party-reproducible → upstream report drafted\n- [`plugin-install.md`](docs/learnings/plugin-install.md) — Anthropic & Partners plugin install flow, gate logic, backend endpoints, and DevTools recipes\n- [`tray-rebuild-race.md`](docs/learnings/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\n- [`cowork-vm-daemon.md`](docs/learnings/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\n- [`test-harness-electron-hooks.md`](docs/learnings/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\n- [`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\n- [`config-wipe-guard.md`](docs/learnings/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-state` → `persisted.*` 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\n- [`quit-cleanup-scope-fence.md`](docs/learnings/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)\n- [`test-methodology-and-coverage.md`](docs/learnings/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)\n\nArchived (still useful as diagnosis records): [`docs/archive/linux-topbar-shim.md`](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/upstream-reports/)); [`docs/archive/cowork-linux-handover.md`](docs/archive/cowork-linux-handover.md) — the 2.x patch-based Cowork stack handover.\n\n## Code Style\n\nAll shell scripts in this project must follow the [Bash Style Guide](docs/styleguides/bash_styleguide.md). Key points:\n\n- Tabs for indentation, lines under 80 characters (exception: URLs and regex patterns)\n- Use `[[ ]]` for conditionals, `$(...)` for command substitution\n- Single quotes for literals, double quotes for expansions\n- Lowercase variables; UPPERCASE only for constants/exports\n- Use `local` in functions, avoid `set -e` and `eval`\n\n### Anti-patterns\n\n- **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`.\n- **Don't `eval`.** Use arrays for argv composition (`cmd \"${args[@]}\"`). `eval` defeats every parser and is a permanent SC2046 magnet.\n- **Don't use POSIX `[ ... ]`.** Always `[[ ... ]]`. POSIX `[` mis-parses unquoted expansions in ways `[[` does not.\n- **Don't backtick.** Always `$(...)`. Backticks don't nest cleanly and conflict with markdown when patches are pasted into PR comments.\n- **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.\n- **Don't wrap commands in `if cmd; then true; else false; fi`-style scaffolding.** Just `cmd` — the exit code is already there.\n- **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.\n\n### Linting\n\nShell scripts are checked with `shellcheck` and GitHub Actions workflows with `actionlint` before pushing. When lint issues are found:\n\n1. **Fix the code** - Correct the underlying issue rather than suppressing the warning\n2. **Disable directives are a last resort** - Only use `# shellcheck disable=SCXXXX` when:\n   - The warning is a false positive\n   - The pattern is intentional and unavoidable\n   - Always add a comment explaining why the disable is needed\n3. **Run `/lint` to check manually** - Use this skill to check for issues before pushing\n\n## Docs\n\n- **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`](docs/styleguides/docs_styleguide.md).\n- **Lowercase kebab-case filenames** for everything in `docs/`. Order belongs in [`docs/index.md`](docs/index.md), not filenames or numeric prefixes.\n- **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`.\n- **Subsystem deep-dives go under [`docs/learnings/`](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.\n- **Decisions go in [`docs/decisions.md`](docs/decisions.md) (ADR format).** Don't relitigate a settled direction inside a how-to page; link the decision instead.\n- **Troubleshooting headings are the literal symptom**, not editorialized prose. `## Black screen on Fedora KDE under Wayland`, not `## Troubles with Wayland`. Search ranks headings.\n- **CHANGELOG follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/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`](CHANGELOG.md) for the current state and [`RELEASING.md`](RELEASING.md) for when entries get promoted from `[Unreleased]`.\n\n## GitHub Workflow\n\n### General Approach\n\n- Use `gh` CLI for all GitHub interactions\n- Create branches based on issue numbers: `fix/123-description` or `feature/123-description`\n- Reference issues in commits and PRs with `#123` or `Fixes #123`\n- After creating a PR, add a comment to the related issue with a summary and link to the PR\n\n### Investigating Issues\n\nFor older issues, review the state of the code when the issue was raised - it may have already been addressed:\n\n```bash\n# Get issue creation date\ngh issue view 123 --json createdAt\n\n# Find the commit just before the issue was created\ngit log --oneline --until=\"2025-08-23T08:48:35Z\" -1\n\n# View a file at that point in time\ngit show <commit>:path/to/file.sh\n\n# Search for relevant changes since the issue was created\ngit log --oneline --after=\"2025-08-23\" -- path/to/file.sh\n\n# View a specific commit that may have fixed the issue\ngit show <commit>\n```\n\nThis helps identify if the issue was already fixed, and allows referencing the specific commit in the response.\n\n### Attribution\n\n**For PR descriptions**, include full attribution:\n\n```\n---\nGenerated with [Claude Code](https://claude.ai/code)\nCo-Authored-By: Claude <model-name> <noreply@anthropic.com>\n<XX>% AI / <YY>% Human\nClaude: <what AI did>\nHuman: <what human did>\n```\n\n- Use the actual model name (e.g., `Claude Opus 4.5`, `Claude Sonnet 4`)\n- The percentage split should honestly reflect the contribution balance for that specific work\n- This provides a trackable record of AI-assisted development over time\n\n**For issues and comments**, use simplified attribution:\n\n```\n---\nWritten by Claude <model-name> via [Claude Code](https://claude.ai/code)\n```\n\n**For commits**, include a Co-Authored-By trailer:\n\n```\nCo-Authored-By: Claude <claude@anthropic.com>\n```\n\n### Contributor Credits\n\n[`ACKNOWLEDGMENTS.md`](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:\n\n1. **Merging an external PR** — Add the author to the list with a link to their GitHub profile and a brief description of their contribution.\n2. **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.\n\nContributors are listed in chronological order: inspirational projects first (k3d3, emsi, leobuskin), then contributors ordered by when their contribution was merged or implemented.\n\n## Working with Minified JavaScript\n\n### Important Guidelines\n\n1. **Always use regex patterns** when modifying the source JavaScript. Patches live in `scripts/patches/*.sh` — `app-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`](docs/learnings/patching-minified-js.md).\n\n2. **The beautified code in `build-reference/` has different spacing** than the actual minified code in the app. Patterns must handle both:\n   - Minified: `oe.nativeTheme.on(\"updated\",()=>{`\n   - Beautified: `oe.nativeTheme.on(\"updated\", () => {`\n\n3. **Use `-E` flag with sed** for extended regex support when patterns need grouping or alternation.\n\n4. **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:\n   ```bash\n   # The minified Quick Entry window var, anchored on a stable literal\n   quick_var=$(grep -oP '[$\\w]+(?=\\.setAlwaysOnTop\\(\\s*!0\\s*,\\s*\"pop-up-menu\"\\))' \\\n       \"$index_js\")\n   ```\n\n5. **Handle optional whitespace** in regex patterns:\n   ```bash\n   # Bad: assumes no spaces\n   sed -i 's/oe.nativeTheme.on(\"updated\",()=>{/...'\n\n   # Good: handles optional whitespace\n   sed -i -E 's/(oe\\.nativeTheme\\.on\\(\\s*\"updated\"\\s*,\\s*\\(\\)\\s*=>\\s*\\{)/...'\n   ```\n\n### Reference Files\n\n- `build-reference/app-extracted/` - Extracted and beautified source for analysis\n- `build-reference/tray-icons/` - Tray icon assets for reference\n\n## Patch Orchestration (patch-zero)\n\n`scripts/patches/app-asar.sh` owns the asar patch stage:\n\n- **`active_patches` array** — the only place a patch gets wired in. Empty array ⇒ no extract, no repack, official `app.asar` ships byte-identical.\n- **productName guard** — the build fails if upstream's `productName` stops matching `WM_CLASS` (breaks `StartupWMClass` in every `.desktop` file).\n- **Upstream tripwires (AU-1/MB-1)** — the build fails if the official bundle stops shipping `apt_channel_pending` (autoupdater still pending, see [D-001](docs/decisions.md)) or `menuBarEnabled:!0` (menu-bar default). These replace the per-patch WARNINGs that left with the v3.0.0 deletions.\n- **Config-wipe recovery is launcher-side, not an asar patch** — `backup_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`](docs/learnings/config-wipe-guard.md).\n- **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.\n\nThe 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](docs/decisions.md).\n\n## Setting Up build-reference\n\nIf `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).\n\n### Prerequisites\n\n```bash\n# Install required tools (ar comes from binutils)\nsudo apt install binutils wget xz-utils zstd nodejs npm\n\n# Install asar and prettier globally (or use npx)\nnpm install -g @electron/asar prettier\n```\n\n### Step 1: Download the official .deb\n\nThe pinned version, pool path, and SHA-256 live in `scripts/setup/official-deb.sh` (`OFFICIAL_DEB_*`). To fetch the pinned amd64 build:\n\n```bash\nmkdir -p build-reference && cd build-reference\n\n# Read the current pin\nsource ../scripts/setup/official-deb.sh 2>/dev/null || true\nwget -O claude-desktop.deb \\\n  \"https://downloads.claude.ai/claude-desktop/apt/stable/$OFFICIAL_DEB_POOL_AMD64\"\necho \"$OFFICIAL_DEB_SHA256_AMD64  claude-desktop.deb\" | sha256sum -c\n```\n\nTo inspect the newest pool entry instead, resolve it from the Packages index (`resolve_official_deb` in `official-deb.sh` does the same thing):\n\n```bash\ncurl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n  | awk -v RS='' '/claude-desktop/' | grep -E '^(Version|Filename|SHA256):'\n```\n\n### Step 2: Extract the .deb\n\nNo dpkg required — `ar` + `tar` handle every member (the data member has shipped as both `.tar.zst` and `.tar.xz`; check with `ar t`):\n\n```bash\nar t claude-desktop.deb                     # list members\nar p claude-desktop.deb data.tar.xz | tar -J -x   # or --zstd for .tar.zst\n\n# The app tree lands at usr/lib/claude-desktop/\ncp usr/lib/claude-desktop/resources/app.asar .\ncp -a usr/lib/claude-desktop/resources/app.asar.unpacked .\n\n# Optional: hicolor icons for reference\ncp -a usr/share/icons/hicolor tray-icons\n```\n\n### Step 3: Extract app.asar\n\n```bash\nasar extract app.asar app-extracted\n```\n\n### Step 4: Beautify the JavaScript Files\n\nThe extracted JS files are minified. Use prettier to make them readable:\n\n```bash\n# Beautify all JS files in the build directory. Since 1.19367.0 the main\n# process is code-split, so index.js is a tiny stub and the real main\n# code lives in index.chunk-<hash>.js — the glob covers every chunk.\nnpx prettier --write \"app-extracted/.vite/build/*.js\"\n\n# The main-process chunk is the biggest .vite/build/*.js (index.js just\n# require()s it). Resolve it from the stub if you want to beautify only it:\nmain_chunk=$(grep -oP 'require\\(\"\\./\\Kindex\\.chunk-[^\"]+\\.js(?=\"\\))' \\\n    app-extracted/.vite/build/index.js)\nnpx prettier --write \"app-extracted/.vite/build/$main_chunk\"\n```\n\n### Step 5: Clean Up (Optional)\n\n```bash\n# Keep only what's needed for reference\nrm -rf usr claude-desktop.deb\nrm -rf app.asar app.asar.unpacked  # Keep only app-extracted\n```\n\n### Final Structure\n\n```\nbuild-reference/\n├── app-extracted/\n│   ├── .vite/\n│   │   ├── build/\n│   │   │   ├── index.js                 # Main-process entry stub\n│   │   │   ├── index.chunk-<hash>.js     # Main process (code-split, 1.19367.0+)\n│   │   │   ├── mainWindow.js             # Main window preload\n│   │   │   ├── mainView.js               # Main view preload\n│   │   │   └── ...\n│   │   └── renderer/\n│   │       └── ...\n│   ├── node_modules/\n│   │   └── @ant/claude-native/   # Rust native binding (real on Linux)\n│   └── package.json\n└── tray-icons/                   # Official hicolor icons (optional)\n```\n\nRemember that patterns verified against beautified output need the whitespace-tolerant form when applied to the shipped minified bytes (see the guidelines above).\n\n## Adding New Package Formats or Repositories\n\nWhen adding support for new distribution formats (e.g., RPM, Flatpak, Snap) or package repositories, follow these guidelines to avoid iterative debugging in CI.\n\n### Research Before Implementing\n\n1. **Understand the target system's constraints** - Each package format has specific rules:\n   - Version string formats (e.g., RPM cannot have hyphens in Version field)\n   - Required metadata fields\n   - Signing requirements and tools\n\n2. **Search for existing CI implementations** - Look for \"GitHub Actions [format] signing\" or similar. Existing workflows reveal required flags, environment setup, and common pitfalls.\n\n3. **Check tool behavior in non-interactive environments** - CI has no TTY. Tools like GPG need flags like `--batch` and `--yes` to work without prompts.\n\n### Consider Concurrency\n\n1. **Multiple jobs writing to the same branch will race** - If APT and DNF repos both push to `gh-pages`, add:\n   - Job dependencies (`needs: [other-job]`), or\n   - Retry loops with `git pull --rebase` before push\n\n2. **External processes may also modify branches** - GitHub Pages deployment runs automatically and can cause push conflicts.\n\n### Test the Full Pipeline\n\n1. **Test CI steps locally first** - Run the signing/packaging commands manually to catch errors before committing.\n\n2. **Use a test tag for new infrastructure** - Create a non-release tag to validate the full CI pipeline before merging to main.\n\n3. **Verify the end-user experience** - After CI succeeds, actually test the install commands from the README on a clean system.\n\n### Common CI Pitfalls\n\n| Issue | Solution |\n|-------|----------|\n| GPG \"cannot open /dev/tty\" | Add `--batch` flag |\n| GPG \"File exists\" error | Add `--yes` flag to overwrite |\n| Push rejected (ref changed) | Add `git pull --rebase` before push, with retry loop |\n| Version format invalid | Research target format's version constraints upfront |\n| Signing key not found | Ensure key is imported before signing step, check key ID output |\n\n## CI/CD\n\n### Triggering Builds\n\n```bash\n# Trigger CI on a branch\ngh workflow run CI --ref branch-name\n\n# Watch the run\ngh run watch RUN_ID\n\n# Download artifacts\ngh run download RUN_ID -n artifact-name\n```\n\n### Build Artifacts\n\n- `claude-desktop-unofficial_VERSION_amd64.deb` / `claude-desktop-unofficial_VERSION_arm64.deb` - Debian packages\n- `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`\n- `claude-desktop-unofficial-VERSION-1.x86_64.rpm` / `claude-desktop-unofficial-VERSION-1.aarch64.rpm` - RPM packages\n- `claude-desktop-unofficial-VERSION-amd64.AppImage` / `claude-desktop-unofficial-VERSION-arm64.AppImage` - AppImages (+ `.zsync` in CI)\n- `result/` - Nix build output (symlink, gitignored; the derivation is a stub until the @typedrat rework lands)\n\nOne cross-building `build.yml` produces all of these from `ubuntu-latest` via the `--arch` input (see [`docs/learnings/cross-build-host-vs-target.md`](docs/learnings/cross-build-host-vs-target.md) for the host-vs-target trap).\n\n## Distribution\n\nAPT 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.\n\nKey files:\n- `worker/src/worker.js` — Worker source\n- `worker/wrangler.toml` — Worker config (route, `custom_domain = true`)\n- `.github/workflows/deploy-worker.yml` — deploys on push to `main` when `worker/**` changes\n- `.github/workflows/apt-repo-heartbeat.yml` — daily chain validation, auto-opens tracking issue on failure\n- `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\n\nRepo secrets: `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`. Token scoped to the \"Edit Cloudflare Workers\" template.\n\nFull 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).\n\n## Testing\n\n### Local Build\n\n```bash\n./build.sh --build appimage --clean no\n```\n\n### Nix Build\n\n```bash\nnix build .#claude-desktop\nnix build .#claude-desktop-fhs\n```\n\nThe 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`](docs/learnings/nix.md)).\n\n### Testing AppImage\n\n```bash\n# Run with logging\n./test-build/claude-desktop-*.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log\n```\n\n## Debugging Workflow\n\n### Inspecting the Running App's Code\n\n```bash\n# Find the mounted AppImage path\nmount | grep claude\n# Example: /tmp/.mount_claudeXXXXXX\n\n# Extract the running app's asar for inspection (official bare\n# co-located layout: ELF + chrome-sandbox + resources/ side by side)\nnpx asar extract /tmp/.mount_claudeXXXXXX/usr/lib/claude-desktop/resources/app.asar /tmp/claude-inspect\n\n# Search for patterns in the extracted code. Since 1.19367.0 the main\n# process is code-split, so grep across all chunks (index.js is a stub);\n# main-process anchors live in index.chunk-<hash>.js.\ngrep -rn \"pattern\" /tmp/claude-inspect/.vite/build/\n```\n\n### Checking DBus/Tray Status\n\n```bash\n# List registered tray icons\ngdbus call --session --dest=org.kde.StatusNotifierWatcher \\\n  --object-path=/StatusNotifierWatcher \\\n  --method=org.freedesktop.DBus.Properties.Get \\\n  org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems\n\n# Find which process owns a DBus connection\ngdbus call --session --dest=org.freedesktop.DBus \\\n  --object-path=/org/freedesktop/DBus \\\n  --method=org.freedesktop.DBus.GetConnectionUnixProcessID \":1.XXXX\"\n```\n\n### Log Locations\n\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n- App logs: `~/.config/Claude/logs/`\n- Run with logging: `./app.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log`\n\n## Useful Locations\n\n- App data: `~/.config/Claude/`\n- Logs: `~/.config/Claude/logs/`\n- SingletonLock: `~/.config/Claude/SingletonLock`\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n\n## Versioning\n\nRelease versions are managed via two GitHub Actions repository variables (not files):\n\n- **`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.\n- **`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.\n\n### Tag format\n\nTags 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.\n\n```bash\n# Check current values\ngh variable get REPO_VERSION\ngh variable get CLAUDE_DESKTOP_VERSION\n\n# Bump repo version and tag a release\ngh variable set REPO_VERSION --body \"1.3.24\"\ngit tag \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\ngit push origin \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\n```\n\nWhen 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.\n\n## Common Gotchas\n\n- **`.zsync` files** - Used for delta updates, can be ignored/deleted\n- **AppImage mount points** - Running AppImages mount to `/tmp/.mount_claude*`; check with `mount | grep claude`\n- **Killing the app** - Must kill all electron child processes, not just the main one:\n  ```bash\n  pkill -9 -f \"mount_claude\"\n  ```\n- **SingletonLock** - If app won't start, check for stale lock: `~/.config/Claude/SingletonLock`\n- **Node version** - Build requires Node.js; the script downloads its own if needed (keyed to the HOST arch — see the cross-build learning)\n- **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:\n  ```bash\n  # Check repo variable (source of truth)\n  gh variable get CLAUDE_DESKTOP_VERSION\n\n  # Check the pinned version on your branch\n  grep -oP \"^OFFICIAL_DEB_VERSION='\\K[^']+\" scripts/setup/official-deb.sh\n\n  # What the official pool currently serves\n  curl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n    | grep -E '^Version:' | sort -V | tail -1\n  ```\n- **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\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# Claude Desktop Debian - Development Notes\n\n<!--\n  This file is read by Claude Code. The content below is duplicated in\n  AGENTS.md (read by other AI tools per the agents.md standard) so that\n  contributors using either receive the same instructions without needing\n  to cross-reference. Keep CLAUDE.md and AGENTS.md byte-identical below\n  the H1 title (the sync-policy comment above is the one place they\n  intentionally differ) — if you edit one, edit the other.\n-->\n\n## Required reading\n\nThese 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.\n\n- [`CONTRIBUTING.md`](CONTRIBUTING.md) — what we accept, what goes upstream, subsystem owners, AI-attribution policy.\n- [`docs/styleguides/bash_styleguide.md`](docs/styleguides/bash_styleguide.md) — shell-script conventions (forked from YSAP). Tabs, 80 cols, `[[ ]]`, no `set -e`, no `eval`.\n- [`docs/styleguides/docs_styleguide.md`](docs/styleguides/docs_styleguide.md) — page anatomy, naming, antipatterns for the `docs/` tree.\n- [`docs/index.md`](docs/index.md) — entry point for the rest of the repo docs.\n- [`SECURITY.md`](SECURITY.md) — vulnerability reporting; what's in scope vs. upstream.\n\nThis 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.\n\n## Project Overview\n\nThis 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](docs/decisions.md)) 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.\n\n## Learnings\n\nThe [`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. Docs whose subject no longer ships live in [`docs/archive/`](docs/archive/) with an obsolescence header — they stay findable as diagnosis records.\n\n- [`official-deb-rebase-verification.md`](docs/learnings/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\n- [`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 — still load-bearing for the two survivor patches\n- [`cross-build-host-vs-target.md`](docs/learnings/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\n- [`packaging-permissions.md`](docs/learnings/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\n- [`nix.md`](docs/learnings/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\n- [`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\n- [`wayland-global-shortcuts-portal.md`](docs/learnings/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](https://github.com/electron/electron/issues/51875)); wlroots (Niri/Sway/Hyprland) lack a portal GlobalShortcuts backend entirely\n- [`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; now first-party-reproducible → upstream report drafted\n- [`plugin-install.md`](docs/learnings/plugin-install.md) — Anthropic & Partners plugin install flow, gate logic, backend endpoints, and DevTools recipes\n- [`tray-rebuild-race.md`](docs/learnings/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\n- [`cowork-vm-daemon.md`](docs/learnings/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\n- [`test-harness-electron-hooks.md`](docs/learnings/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\n- [`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\n- [`config-wipe-guard.md`](docs/learnings/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-state` → `persisted.*` 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\n- [`quit-cleanup-scope-fence.md`](docs/learnings/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)\n- [`test-methodology-and-coverage.md`](docs/learnings/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)\n\nArchived (still useful as diagnosis records): [`docs/archive/linux-topbar-shim.md`](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/upstream-reports/)); [`docs/archive/cowork-linux-handover.md`](docs/archive/cowork-linux-handover.md) — the 2.x patch-based Cowork stack handover.\n\n## Code Style\n\nAll shell scripts in this project must follow the [Bash Style Guide](docs/styleguides/bash_styleguide.md). Key points:\n\n- Tabs for indentation, lines under 80 characters (exception: URLs and regex patterns)\n- Use `[[ ]]` for conditionals, `$(...)` for command substitution\n- Single quotes for literals, double quotes for expansions\n- Lowercase variables; UPPERCASE only for constants/exports\n- Use `local` in functions, avoid `set -e` and `eval`\n\n### Anti-patterns\n\n- **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`.\n- **Don't `eval`.** Use arrays for argv composition (`cmd \"${args[@]}\"`). `eval` defeats every parser and is a permanent SC2046 magnet.\n- **Don't use POSIX `[ ... ]`.** Always `[[ ... ]]`. POSIX `[` mis-parses unquoted expansions in ways `[[` does not.\n- **Don't backtick.** Always `$(...)`. Backticks don't nest cleanly and conflict with markdown when patches are pasted into PR comments.\n- **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.\n- **Don't wrap commands in `if cmd; then true; else false; fi`-style scaffolding.** Just `cmd` — the exit code is already there.\n- **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.\n\n### Linting\n\nShell scripts are checked with `shellcheck` and GitHub Actions workflows with `actionlint` before pushing. When lint issues are found:\n\n1. **Fix the code** - Correct the underlying issue rather than suppressing the warning\n2. **Disable directives are a last resort** - Only use `# shellcheck disable=SCXXXX` when:\n   - The warning is a false positive\n   - The pattern is intentional and unavoidable\n   - Always add a comment explaining why the disable is needed\n3. **Run `/lint` to check manually** - Use this skill to check for issues before pushing\n\n## Docs\n\n- **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`](docs/styleguides/docs_styleguide.md).\n- **Lowercase kebab-case filenames** for everything in `docs/`. Order belongs in [`docs/index.md`](docs/index.md), not filenames or numeric prefixes.\n- **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`.\n- **Subsystem deep-dives go under [`docs/learnings/`](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.\n- **Decisions go in [`docs/decisions.md`](docs/decisions.md) (ADR format).** Don't relitigate a settled direction inside a how-to page; link the decision instead.\n- **Troubleshooting headings are the literal symptom**, not editorialized prose. `## Black screen on Fedora KDE under Wayland`, not `## Troubles with Wayland`. Search ranks headings.\n- **CHANGELOG follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/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`](CHANGELOG.md) for the current state and [`RELEASING.md`](RELEASING.md) for when entries get promoted from `[Unreleased]`.\n\n## GitHub Workflow\n\n### General Approach\n\n- Use `gh` CLI for all GitHub interactions\n- Create branches based on issue numbers: `fix/123-description` or `feature/123-description`\n- Reference issues in commits and PRs with `#123` or `Fixes #123`\n- After creating a PR, add a comment to the related issue with a summary and link to the PR\n\n### Investigating Issues\n\nFor older issues, review the state of the code when the issue was raised - it may have already been addressed:\n\n```bash\n# Get issue creation date\ngh issue view 123 --json createdAt\n\n# Find the commit just before the issue was created\ngit log --oneline --until=\"2025-08-23T08:48:35Z\" -1\n\n# View a file at that point in time\ngit show <commit>:path/to/file.sh\n\n# Search for relevant changes since the issue was created\ngit log --oneline --after=\"2025-08-23\" -- path/to/file.sh\n\n# View a specific commit that may have fixed the issue\ngit show <commit>\n```\n\nThis helps identify if the issue was already fixed, and allows referencing the specific commit in the response.\n\n### Attribution\n\n**For PR descriptions**, include full attribution:\n\n```\n---\nGenerated with [Claude Code](https://claude.ai/code)\nCo-Authored-By: Claude <model-name> <noreply@anthropic.com>\n<XX>% AI / <YY>% Human\nClaude: <what AI did>\nHuman: <what human did>\n```\n\n- Use the actual model name (e.g., `Claude Opus 4.5`, `Claude Sonnet 4`)\n- The percentage split should honestly reflect the contribution balance for that specific work\n- This provides a trackable record of AI-assisted development over time\n\n**For issues and comments**, use simplified attribution:\n\n```\n---\nWritten by Claude <model-name> via [Claude Code](https://claude.ai/code)\n```\n\n**For commits**, include a Co-Authored-By trailer:\n\n```\nCo-Authored-By: Claude <claude@anthropic.com>\n```\n\n### Contributor Credits\n\n[`ACKNOWLEDGMENTS.md`](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:\n\n1. **Merging an external PR** — Add the author to the list with a link to their GitHub profile and a brief description of their contribution.\n2. **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.\n\nContributors are listed in chronological order: inspirational projects first (k3d3, emsi, leobuskin), then contributors ordered by when their contribution was merged or implemented.\n\n## Working with Minified JavaScript\n\n### Important Guidelines\n\n1. **Always use regex patterns** when modifying the source JavaScript. Patches live in `scripts/patches/*.sh` — `app-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`](docs/learnings/patching-minified-js.md).\n\n2. **The beautified code in `build-reference/` has different spacing** than the actual minified code in the app. Patterns must handle both:\n   - Minified: `oe.nativeTheme.on(\"updated\",()=>{`\n   - Beautified: `oe.nativeTheme.on(\"updated\", () => {`\n\n3. **Use `-E` flag with sed** for extended regex support when patterns need grouping or alternation.\n\n4. **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:\n   ```bash\n   # The minified Quick Entry window var, anchored on a stable literal\n   quick_var=$(grep -oP '[$\\w]+(?=\\.setAlwaysOnTop\\(\\s*!0\\s*,\\s*\"pop-up-menu\"\\))' \\\n       \"$index_js\")\n   ```\n\n5. **Handle optional whitespace** in regex patterns:\n   ```bash\n   # Bad: assumes no spaces\n   sed -i 's/oe.nativeTheme.on(\"updated\",()=>{/...'\n\n   # Good: handles optional whitespace\n   sed -i -E 's/(oe\\.nativeTheme\\.on\\(\\s*\"updated\"\\s*,\\s*\\(\\)\\s*=>\\s*\\{)/...'\n   ```\n\n### Reference Files\n\n- `build-reference/app-extracted/` - Extracted and beautified source for analysis\n- `build-reference/tray-icons/` - Tray icon assets for reference\n\n## Patch Orchestration (patch-zero)\n\n`scripts/patches/app-asar.sh` owns the asar patch stage:\n\n- **`active_patches` array** — the only place a patch gets wired in. Empty array ⇒ no extract, no repack, official `app.asar` ships byte-identical.\n- **productName guard** — the build fails if upstream's `productName` stops matching `WM_CLASS` (breaks `StartupWMClass` in every `.desktop` file).\n- **Upstream tripwires (AU-1/MB-1)** — the build fails if the official bundle stops shipping `apt_channel_pending` (autoupdater still pending, see [D-001](docs/decisions.md)) or `menuBarEnabled:!0` (menu-bar default). These replace the per-patch WARNINGs that left with the v3.0.0 deletions.\n- **Config-wipe recovery is launcher-side, not an asar patch** — `backup_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`](docs/learnings/config-wipe-guard.md).\n- **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.\n\nThe 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](docs/decisions.md).\n\n## Setting Up build-reference\n\nIf `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).\n\n### Prerequisites\n\n```bash\n# Install required tools (ar comes from binutils)\nsudo apt install binutils wget xz-utils zstd nodejs npm\n\n# Install asar and prettier globally (or use npx)\nnpm install -g @electron/asar prettier\n```\n\n### Step 1: Download the official .deb\n\nThe pinned version, pool path, and SHA-256 live in `scripts/setup/official-deb.sh` (`OFFICIAL_DEB_*`). To fetch the pinned amd64 build:\n\n```bash\nmkdir -p build-reference && cd build-reference\n\n# Read the current pin\nsource ../scripts/setup/official-deb.sh 2>/dev/null || true\nwget -O claude-desktop.deb \\\n  \"https://downloads.claude.ai/claude-desktop/apt/stable/$OFFICIAL_DEB_POOL_AMD64\"\necho \"$OFFICIAL_DEB_SHA256_AMD64  claude-desktop.deb\" | sha256sum -c\n```\n\nTo inspect the newest pool entry instead, resolve it from the Packages index (`resolve_official_deb` in `official-deb.sh` does the same thing):\n\n```bash\ncurl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n  | awk -v RS='' '/claude-desktop/' | grep -E '^(Version|Filename|SHA256):'\n```\n\n### Step 2: Extract the .deb\n\nNo dpkg required — `ar` + `tar` handle every member (the data member has shipped as both `.tar.zst` and `.tar.xz`; check with `ar t`):\n\n```bash\nar t claude-desktop.deb                     # list members\nar p claude-desktop.deb data.tar.xz | tar -J -x   # or --zstd for .tar.zst\n\n# The app tree lands at usr/lib/claude-desktop/\ncp usr/lib/claude-desktop/resources/app.asar .\ncp -a usr/lib/claude-desktop/resources/app.asar.unpacked .\n\n# Optional: hicolor icons for reference\ncp -a usr/share/icons/hicolor tray-icons\n```\n\n### Step 3: Extract app.asar\n\n```bash\nasar extract app.asar app-extracted\n```\n\n### Step 4: Beautify the JavaScript Files\n\nThe extracted JS files are minified. Use prettier to make them readable:\n\n```bash\n# Beautify all JS files in the build directory. Since 1.19367.0 the main\n# process is code-split, so index.js is a tiny stub and the real main\n# code lives in index.chunk-<hash>.js — the glob covers every chunk.\nnpx prettier --write \"app-extracted/.vite/build/*.js\"\n\n# The main-process chunk is the biggest .vite/build/*.js (index.js just\n# require()s it). Resolve it from the stub if you want to beautify only it:\nmain_chunk=$(grep -oP 'require\\(\"\\./\\Kindex\\.chunk-[^\"]+\\.js(?=\"\\))' \\\n    app-extracted/.vite/build/index.js)\nnpx prettier --write \"app-extracted/.vite/build/$main_chunk\"\n```\n\n### Step 5: Clean Up (Optional)\n\n```bash\n# Keep only what's needed for reference\nrm -rf usr claude-desktop.deb\nrm -rf app.asar app.asar.unpacked  # Keep only app-extracted\n```\n\n### Final Structure\n\n```\nbuild-reference/\n├── app-extracted/\n│   ├── .vite/\n│   │   ├── build/\n│   │   │   ├── index.js                 # Main-process entry stub\n│   │   │   ├── index.chunk-<hash>.js     # Main process (code-split, 1.19367.0+)\n│   │   │   ├── mainWindow.js             # Main window preload\n│   │   │   ├── mainView.js               # Main view preload\n│   │   │   └── ...\n│   │   └── renderer/\n│   │       └── ...\n│   ├── node_modules/\n│   │   └── @ant/claude-native/   # Rust native binding (real on Linux)\n│   └── package.json\n└── tray-icons/                   # Official hicolor icons (optional)\n```\n\nRemember that patterns verified against beautified output need the whitespace-tolerant form when applied to the shipped minified bytes (see the guidelines above).\n\n## Adding New Package Formats or Repositories\n\nWhen adding support for new distribution formats (e.g., RPM, Flatpak, Snap) or package repositories, follow these guidelines to avoid iterative debugging in CI.\n\n### Research Before Implementing\n\n1. **Understand the target system's constraints** - Each package format has specific rules:\n   - Version string formats (e.g., RPM cannot have hyphens in Version field)\n   - Required metadata fields\n   - Signing requirements and tools\n\n2. **Search for existing CI implementations** - Look for \"GitHub Actions [format] signing\" or similar. Existing workflows reveal required flags, environment setup, and common pitfalls.\n\n3. **Check tool behavior in non-interactive environments** - CI has no TTY. Tools like GPG need flags like `--batch` and `--yes` to work without prompts.\n\n### Consider Concurrency\n\n1. **Multiple jobs writing to the same branch will race** - If APT and DNF repos both push to `gh-pages`, add:\n   - Job dependencies (`needs: [other-job]`), or\n   - Retry loops with `git pull --rebase` before push\n\n2. **External processes may also modify branches** - GitHub Pages deployment runs automatically and can cause push conflicts.\n\n### Test the Full Pipeline\n\n1. **Test CI steps locally first** - Run the signing/packaging commands manually to catch errors before committing.\n\n2. **Use a test tag for new infrastructure** - Create a non-release tag to validate the full CI pipeline before merging to main.\n\n3. **Verify the end-user experience** - After CI succeeds, actually test the install commands from the README on a clean system.\n\n### Common CI Pitfalls\n\n| Issue | Solution |\n|-------|----------|\n| GPG \"cannot open /dev/tty\" | Add `--batch` flag |\n| GPG \"File exists\" error | Add `--yes` flag to overwrite |\n| Push rejected (ref changed) | Add `git pull --rebase` before push, with retry loop |\n| Version format invalid | Research target format's version constraints upfront |\n| Signing key not found | Ensure key is imported before signing step, check key ID output |\n\n## CI/CD\n\n### Triggering Builds\n\n```bash\n# Trigger CI on a branch\ngh workflow run CI --ref branch-name\n\n# Watch the run\ngh run watch RUN_ID\n\n# Download artifacts\ngh run download RUN_ID -n artifact-name\n```\n\n### Build Artifacts\n\n- `claude-desktop-unofficial_VERSION_amd64.deb` / `claude-desktop-unofficial_VERSION_arm64.deb` - Debian packages\n- `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`\n- `claude-desktop-unofficial-VERSION-1.x86_64.rpm` / `claude-desktop-unofficial-VERSION-1.aarch64.rpm` - RPM packages\n- `claude-desktop-unofficial-VERSION-amd64.AppImage` / `claude-desktop-unofficial-VERSION-arm64.AppImage` - AppImages (+ `.zsync` in CI)\n- `result/` - Nix build output (symlink, gitignored; the derivation is a stub until the @typedrat rework lands)\n\nOne cross-building `build.yml` produces all of these from `ubuntu-latest` via the `--arch` input (see [`docs/learnings/cross-build-host-vs-target.md`](docs/learnings/cross-build-host-vs-target.md) for the host-vs-target trap).\n\n## Distribution\n\nAPT 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.\n\nKey files:\n- `worker/src/worker.js` — Worker source\n- `worker/wrangler.toml` — Worker config (route, `custom_domain = true`)\n- `.github/workflows/deploy-worker.yml` — deploys on push to `main` when `worker/**` changes\n- `.github/workflows/apt-repo-heartbeat.yml` — daily chain validation, auto-opens tracking issue on failure\n- `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\n\nRepo secrets: `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`. Token scoped to the \"Edit Cloudflare Workers\" template.\n\nFull 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).\n\n## Testing\n\n### Local Build\n\n```bash\n./build.sh --build appimage --clean no\n```\n\n### Nix Build\n\n```bash\nnix build .#claude-desktop\nnix build .#claude-desktop-fhs\n```\n\nThe 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`](docs/learnings/nix.md)).\n\n### Testing AppImage\n\n```bash\n# Run with logging\n./test-build/claude-desktop-*.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log\n```\n\n## Debugging Workflow\n\n### Inspecting the Running App's Code\n\n```bash\n# Find the mounted AppImage path\nmount | grep claude\n# Example: /tmp/.mount_claudeXXXXXX\n\n# Extract the running app's asar for inspection (official bare\n# co-located layout: ELF + chrome-sandbox + resources/ side by side)\nnpx asar extract /tmp/.mount_claudeXXXXXX/usr/lib/claude-desktop/resources/app.asar /tmp/claude-inspect\n\n# Search for patterns in the extracted code. Since 1.19367.0 the main\n# process is code-split, so grep across all chunks (index.js is a stub);\n# main-process anchors live in index.chunk-<hash>.js.\ngrep -rn \"pattern\" /tmp/claude-inspect/.vite/build/\n```\n\n### Checking DBus/Tray Status\n\n```bash\n# List registered tray icons\ngdbus call --session --dest=org.kde.StatusNotifierWatcher \\\n  --object-path=/StatusNotifierWatcher \\\n  --method=org.freedesktop.DBus.Properties.Get \\\n  org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems\n\n# Find which process owns a DBus connection\ngdbus call --session --dest=org.freedesktop.DBus \\\n  --object-path=/org/freedesktop/DBus \\\n  --method=org.freedesktop.DBus.GetConnectionUnixProcessID \":1.XXXX\"\n```\n\n### Log Locations\n\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n- App logs: `~/.config/Claude/logs/`\n- Run with logging: `./app.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log`\n\n## Useful Locations\n\n- App data: `~/.config/Claude/`\n- Logs: `~/.config/Claude/logs/`\n- SingletonLock: `~/.config/Claude/SingletonLock`\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n\n## Versioning\n\nRelease versions are managed via two GitHub Actions repository variables (not files):\n\n- **`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.\n- **`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.\n\n### Tag format\n\nTags 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.\n\n```bash\n# Check current values\ngh variable get REPO_VERSION\ngh variable get CLAUDE_DESKTOP_VERSION\n\n# Bump repo version and tag a release\ngh variable set REPO_VERSION --body \"1.3.24\"\ngit tag \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\ngit push origin \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\n```\n\nWhen 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.\n\n## Common Gotchas\n\n- **`.zsync` files** - Used for delta updates, can be ignored/deleted\n- **AppImage mount points** - Running AppImages mount to `/tmp/.mount_claude*`; check with `mount | grep claude`\n- **Killing the app** - Must kill all electron child processes, not just the main one:\n  ```bash\n  pkill -9 -f \"mount_claude\"\n  ```\n- **SingletonLock** - If app won't start, check for stale lock: `~/.config/Claude/SingletonLock`\n- **Node version** - Build requires Node.js; the script downloads its own if needed (keyed to the HOST arch — see the cross-build learning)\n- **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:\n  ```bash\n  # Check repo variable (source of truth)\n  gh variable get CLAUDE_DESKTOP_VERSION\n\n  # Check the pinned version on your branch\n  grep -oP \"^OFFICIAL_DEB_VERSION='\\K[^']+\" scripts/setup/official-deb.sh\n\n  # What the official pool currently serves\n  curl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n    | grep -E '^Version:' | sort -V | tail -1\n  ```\n- **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\n","category":"root","tokens":8268},{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md\n\n<!--\n  This file is read by AI tools that support the agents.md vendor-neutral\n  standard. The content below is duplicated in CLAUDE.md (read by Claude\n  Code) so that contributors using either receive the same instructions\n  without needing to cross-reference. Keep CLAUDE.md and AGENTS.md\n  byte-identical below the H1 title (the sync-policy comment above is the\n  one place they intentionally differ) — if you edit one, edit the other.\n-->\n\n## Required reading\n\nThese 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.\n\n- [`CONTRIBUTING.md`](CONTRIBUTING.md) — what we accept, what goes upstream, subsystem owners, AI-attribution policy.\n- [`docs/styleguides/bash_styleguide.md`](docs/styleguides/bash_styleguide.md) — shell-script conventions (forked from YSAP). Tabs, 80 cols, `[[ ]]`, no `set -e`, no `eval`.\n- [`docs/styleguides/docs_styleguide.md`](docs/styleguides/docs_styleguide.md) — page anatomy, naming, antipatterns for the `docs/` tree.\n- [`docs/index.md`](docs/index.md) — entry point for the rest of the repo docs.\n- [`SECURITY.md`](SECURITY.md) — vulnerability reporting; what's in scope vs. upstream.\n\nThis 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.\n\n## Project Overview\n\nThis 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](docs/decisions.md)) 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.\n\n## Learnings\n\nThe [`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. Docs whose subject no longer ships live in [`docs/archive/`](docs/archive/) with an obsolescence header — they stay findable as diagnosis records.\n\n- [`official-deb-rebase-verification.md`](docs/learnings/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\n- [`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 — still load-bearing for the two survivor patches\n- [`cross-build-host-vs-target.md`](docs/learnings/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\n- [`packaging-permissions.md`](docs/learnings/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\n- [`nix.md`](docs/learnings/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\n- [`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\n- [`wayland-global-shortcuts-portal.md`](docs/learnings/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](https://github.com/electron/electron/issues/51875)); wlroots (Niri/Sway/Hyprland) lack a portal GlobalShortcuts backend entirely\n- [`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; now first-party-reproducible → upstream report drafted\n- [`plugin-install.md`](docs/learnings/plugin-install.md) — Anthropic & Partners plugin install flow, gate logic, backend endpoints, and DevTools recipes\n- [`tray-rebuild-race.md`](docs/learnings/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\n- [`cowork-vm-daemon.md`](docs/learnings/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\n- [`test-harness-electron-hooks.md`](docs/learnings/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\n- [`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\n- [`config-wipe-guard.md`](docs/learnings/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-state` → `persisted.*` 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\n- [`quit-cleanup-scope-fence.md`](docs/learnings/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)\n- [`test-methodology-and-coverage.md`](docs/learnings/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)\n\nArchived (still useful as diagnosis records): [`docs/archive/linux-topbar-shim.md`](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/upstream-reports/)); [`docs/archive/cowork-linux-handover.md`](docs/archive/cowork-linux-handover.md) — the 2.x patch-based Cowork stack handover.\n\n## Code Style\n\nAll shell scripts in this project must follow the [Bash Style Guide](docs/styleguides/bash_styleguide.md). Key points:\n\n- Tabs for indentation, lines under 80 characters (exception: URLs and regex patterns)\n- Use `[[ ]]` for conditionals, `$(...)` for command substitution\n- Single quotes for literals, double quotes for expansions\n- Lowercase variables; UPPERCASE only for constants/exports\n- Use `local` in functions, avoid `set -e` and `eval`\n\n### Anti-patterns\n\n- **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`.\n- **Don't `eval`.** Use arrays for argv composition (`cmd \"${args[@]}\"`). `eval` defeats every parser and is a permanent SC2046 magnet.\n- **Don't use POSIX `[ ... ]`.** Always `[[ ... ]]`. POSIX `[` mis-parses unquoted expansions in ways `[[` does not.\n- **Don't backtick.** Always `$(...)`. Backticks don't nest cleanly and conflict with markdown when patches are pasted into PR comments.\n- **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.\n- **Don't wrap commands in `if cmd; then true; else false; fi`-style scaffolding.** Just `cmd` — the exit code is already there.\n- **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.\n\n### Linting\n\nShell scripts are checked with `shellcheck` and GitHub Actions workflows with `actionlint` before pushing. When lint issues are found:\n\n1. **Fix the code** - Correct the underlying issue rather than suppressing the warning\n2. **Disable directives are a last resort** - Only use `# shellcheck disable=SCXXXX` when:\n   - The warning is a false positive\n   - The pattern is intentional and unavoidable\n   - Always add a comment explaining why the disable is needed\n3. **Run `/lint` to check manually** - Use this skill to check for issues before pushing\n\n## Docs\n\n- **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`](docs/styleguides/docs_styleguide.md).\n- **Lowercase kebab-case filenames** for everything in `docs/`. Order belongs in [`docs/index.md`](docs/index.md), not filenames or numeric prefixes.\n- **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`.\n- **Subsystem deep-dives go under [`docs/learnings/`](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.\n- **Decisions go in [`docs/decisions.md`](docs/decisions.md) (ADR format).** Don't relitigate a settled direction inside a how-to page; link the decision instead.\n- **Troubleshooting headings are the literal symptom**, not editorialized prose. `## Black screen on Fedora KDE under Wayland`, not `## Troubles with Wayland`. Search ranks headings.\n- **CHANGELOG follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/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`](CHANGELOG.md) for the current state and [`RELEASING.md`](RELEASING.md) for when entries get promoted from `[Unreleased]`.\n\n## GitHub Workflow\n\n### General Approach\n\n- Use `gh` CLI for all GitHub interactions\n- Create branches based on issue numbers: `fix/123-description` or `feature/123-description`\n- Reference issues in commits and PRs with `#123` or `Fixes #123`\n- After creating a PR, add a comment to the related issue with a summary and link to the PR\n\n### Investigating Issues\n\nFor older issues, review the state of the code when the issue was raised - it may have already been addressed:\n\n```bash\n# Get issue creation date\ngh issue view 123 --json createdAt\n\n# Find the commit just before the issue was created\ngit log --oneline --until=\"2025-08-23T08:48:35Z\" -1\n\n# View a file at that point in time\ngit show <commit>:path/to/file.sh\n\n# Search for relevant changes since the issue was created\ngit log --oneline --after=\"2025-08-23\" -- path/to/file.sh\n\n# View a specific commit that may have fixed the issue\ngit show <commit>\n```\n\nThis helps identify if the issue was already fixed, and allows referencing the specific commit in the response.\n\n### Attribution\n\n**For PR descriptions**, include full attribution:\n\n```\n---\nGenerated with [Claude Code](https://claude.ai/code)\nCo-Authored-By: Claude <model-name> <noreply@anthropic.com>\n<XX>% AI / <YY>% Human\nClaude: <what AI did>\nHuman: <what human did>\n```\n\n- Use the actual model name (e.g., `Claude Opus 4.5`, `Claude Sonnet 4`)\n- The percentage split should honestly reflect the contribution balance for that specific work\n- This provides a trackable record of AI-assisted development over time\n\n**For issues and comments**, use simplified attribution:\n\n```\n---\nWritten by Claude <model-name> via [Claude Code](https://claude.ai/code)\n```\n\n**For commits**, include a Co-Authored-By trailer:\n\n```\nCo-Authored-By: Claude <claude@anthropic.com>\n```\n\n### Contributor Credits\n\n[`ACKNOWLEDGMENTS.md`](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:\n\n1. **Merging an external PR** — Add the author to the list with a link to their GitHub profile and a brief description of their contribution.\n2. **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.\n\nContributors are listed in chronological order: inspirational projects first (k3d3, emsi, leobuskin), then contributors ordered by when their contribution was merged or implemented.\n\n## Working with Minified JavaScript\n\n### Important Guidelines\n\n1. **Always use regex patterns** when modifying the source JavaScript. Patches live in `scripts/patches/*.sh` — `app-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`](docs/learnings/patching-minified-js.md).\n\n2. **The beautified code in `build-reference/` has different spacing** than the actual minified code in the app. Patterns must handle both:\n   - Minified: `oe.nativeTheme.on(\"updated\",()=>{`\n   - Beautified: `oe.nativeTheme.on(\"updated\", () => {`\n\n3. **Use `-E` flag with sed** for extended regex support when patterns need grouping or alternation.\n\n4. **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:\n   ```bash\n   # The minified Quick Entry window var, anchored on a stable literal\n   quick_var=$(grep -oP '[$\\w]+(?=\\.setAlwaysOnTop\\(\\s*!0\\s*,\\s*\"pop-up-menu\"\\))' \\\n       \"$index_js\")\n   ```\n\n5. **Handle optional whitespace** in regex patterns:\n   ```bash\n   # Bad: assumes no spaces\n   sed -i 's/oe.nativeTheme.on(\"updated\",()=>{/...'\n\n   # Good: handles optional whitespace\n   sed -i -E 's/(oe\\.nativeTheme\\.on\\(\\s*\"updated\"\\s*,\\s*\\(\\)\\s*=>\\s*\\{)/...'\n   ```\n\n### Reference Files\n\n- `build-reference/app-extracted/` - Extracted and beautified source for analysis\n- `build-reference/tray-icons/` - Tray icon assets for reference\n\n## Patch Orchestration (patch-zero)\n\n`scripts/patches/app-asar.sh` owns the asar patch stage:\n\n- **`active_patches` array** — the only place a patch gets wired in. Empty array ⇒ no extract, no repack, official `app.asar` ships byte-identical.\n- **productName guard** — the build fails if upstream's `productName` stops matching `WM_CLASS` (breaks `StartupWMClass` in every `.desktop` file).\n- **Upstream tripwires (AU-1/MB-1)** — the build fails if the official bundle stops shipping `apt_channel_pending` (autoupdater still pending, see [D-001](docs/decisions.md)) or `menuBarEnabled:!0` (menu-bar default). These replace the per-patch WARNINGs that left with the v3.0.0 deletions.\n- **Config-wipe recovery is launcher-side, not an asar patch** — `backup_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`](docs/learnings/config-wipe-guard.md).\n- **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.\n\nThe 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](docs/decisions.md).\n\n## Setting Up build-reference\n\nIf `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).\n\n### Prerequisites\n\n```bash\n# Install required tools (ar comes from binutils)\nsudo apt install binutils wget xz-utils zstd nodejs npm\n\n# Install asar and prettier globally (or use npx)\nnpm install -g @electron/asar prettier\n```\n\n### Step 1: Download the official .deb\n\nThe pinned version, pool path, and SHA-256 live in `scripts/setup/official-deb.sh` (`OFFICIAL_DEB_*`). To fetch the pinned amd64 build:\n\n```bash\nmkdir -p build-reference && cd build-reference\n\n# Read the current pin\nsource ../scripts/setup/official-deb.sh 2>/dev/null || true\nwget -O claude-desktop.deb \\\n  \"https://downloads.claude.ai/claude-desktop/apt/stable/$OFFICIAL_DEB_POOL_AMD64\"\necho \"$OFFICIAL_DEB_SHA256_AMD64  claude-desktop.deb\" | sha256sum -c\n```\n\nTo inspect the newest pool entry instead, resolve it from the Packages index (`resolve_official_deb` in `official-deb.sh` does the same thing):\n\n```bash\ncurl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n  | awk -v RS='' '/claude-desktop/' | grep -E '^(Version|Filename|SHA256):'\n```\n\n### Step 2: Extract the .deb\n\nNo dpkg required — `ar` + `tar` handle every member (the data member has shipped as both `.tar.zst` and `.tar.xz`; check with `ar t`):\n\n```bash\nar t claude-desktop.deb                     # list members\nar p claude-desktop.deb data.tar.xz | tar -J -x   # or --zstd for .tar.zst\n\n# The app tree lands at usr/lib/claude-desktop/\ncp usr/lib/claude-desktop/resources/app.asar .\ncp -a usr/lib/claude-desktop/resources/app.asar.unpacked .\n\n# Optional: hicolor icons for reference\ncp -a usr/share/icons/hicolor tray-icons\n```\n\n### Step 3: Extract app.asar\n\n```bash\nasar extract app.asar app-extracted\n```\n\n### Step 4: Beautify the JavaScript Files\n\nThe extracted JS files are minified. Use prettier to make them readable:\n\n```bash\n# Beautify all JS files in the build directory. Since 1.19367.0 the main\n# process is code-split, so index.js is a tiny stub and the real main\n# code lives in index.chunk-<hash>.js — the glob covers every chunk.\nnpx prettier --write \"app-extracted/.vite/build/*.js\"\n\n# The main-process chunk is the biggest .vite/build/*.js (index.js just\n# require()s it). Resolve it from the stub if you want to beautify only it:\nmain_chunk=$(grep -oP 'require\\(\"\\./\\Kindex\\.chunk-[^\"]+\\.js(?=\"\\))' \\\n    app-extracted/.vite/build/index.js)\nnpx prettier --write \"app-extracted/.vite/build/$main_chunk\"\n```\n\n### Step 5: Clean Up (Optional)\n\n```bash\n# Keep only what's needed for reference\nrm -rf usr claude-desktop.deb\nrm -rf app.asar app.asar.unpacked  # Keep only app-extracted\n```\n\n### Final Structure\n\n```\nbuild-reference/\n├── app-extracted/\n│   ├── .vite/\n│   │   ├── build/\n│   │   │   ├── index.js                 # Main-process entry stub\n│   │   │   ├── index.chunk-<hash>.js     # Main process (code-split, 1.19367.0+)\n│   │   │   ├── mainWindow.js             # Main window preload\n│   │   │   ├── mainView.js               # Main view preload\n│   │   │   └── ...\n│   │   └── renderer/\n│   │       └── ...\n│   ├── node_modules/\n│   │   └── @ant/claude-native/   # Rust native binding (real on Linux)\n│   └── package.json\n└── tray-icons/                   # Official hicolor icons (optional)\n```\n\nRemember that patterns verified against beautified output need the whitespace-tolerant form when applied to the shipped minified bytes (see the guidelines above).\n\n## Adding New Package Formats or Repositories\n\nWhen adding support for new distribution formats (e.g., RPM, Flatpak, Snap) or package repositories, follow these guidelines to avoid iterative debugging in CI.\n\n### Research Before Implementing\n\n1. **Understand the target system's constraints** - Each package format has specific rules:\n   - Version string formats (e.g., RPM cannot have hyphens in Version field)\n   - Required metadata fields\n   - Signing requirements and tools\n\n2. **Search for existing CI implementations** - Look for \"GitHub Actions [format] signing\" or similar. Existing workflows reveal required flags, environment setup, and common pitfalls.\n\n3. **Check tool behavior in non-interactive environments** - CI has no TTY. Tools like GPG need flags like `--batch` and `--yes` to work without prompts.\n\n### Consider Concurrency\n\n1. **Multiple jobs writing to the same branch will race** - If APT and DNF repos both push to `gh-pages`, add:\n   - Job dependencies (`needs: [other-job]`), or\n   - Retry loops with `git pull --rebase` before push\n\n2. **External processes may also modify branches** - GitHub Pages deployment runs automatically and can cause push conflicts.\n\n### Test the Full Pipeline\n\n1. **Test CI steps locally first** - Run the signing/packaging commands manually to catch errors before committing.\n\n2. **Use a test tag for new infrastructure** - Create a non-release tag to validate the full CI pipeline before merging to main.\n\n3. **Verify the end-user experience** - After CI succeeds, actually test the install commands from the README on a clean system.\n\n### Common CI Pitfalls\n\n| Issue | Solution |\n|-------|----------|\n| GPG \"cannot open /dev/tty\" | Add `--batch` flag |\n| GPG \"File exists\" error | Add `--yes` flag to overwrite |\n| Push rejected (ref changed) | Add `git pull --rebase` before push, with retry loop |\n| Version format invalid | Research target format's version constraints upfront |\n| Signing key not found | Ensure key is imported before signing step, check key ID output |\n\n## CI/CD\n\n### Triggering Builds\n\n```bash\n# Trigger CI on a branch\ngh workflow run CI --ref branch-name\n\n# Watch the run\ngh run watch RUN_ID\n\n# Download artifacts\ngh run download RUN_ID -n artifact-name\n```\n\n### Build Artifacts\n\n- `claude-desktop-unofficial_VERSION_amd64.deb` / `claude-desktop-unofficial_VERSION_arm64.deb` - Debian packages\n- `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`\n- `claude-desktop-unofficial-VERSION-1.x86_64.rpm` / `claude-desktop-unofficial-VERSION-1.aarch64.rpm` - RPM packages\n- `claude-desktop-unofficial-VERSION-amd64.AppImage` / `claude-desktop-unofficial-VERSION-arm64.AppImage` - AppImages (+ `.zsync` in CI)\n- `result/` - Nix build output (symlink, gitignored; the derivation is a stub until the @typedrat rework lands)\n\nOne cross-building `build.yml` produces all of these from `ubuntu-latest` via the `--arch` input (see [`docs/learnings/cross-build-host-vs-target.md`](docs/learnings/cross-build-host-vs-target.md) for the host-vs-target trap).\n\n## Distribution\n\nAPT 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.\n\nKey files:\n- `worker/src/worker.js` — Worker source\n- `worker/wrangler.toml` — Worker config (route, `custom_domain = true`)\n- `.github/workflows/deploy-worker.yml` — deploys on push to `main` when `worker/**` changes\n- `.github/workflows/apt-repo-heartbeat.yml` — daily chain validation, auto-opens tracking issue on failure\n- `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\n\nRepo secrets: `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`. Token scoped to the \"Edit Cloudflare Workers\" template.\n\nFull 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).\n\n## Testing\n\n### Local Build\n\n```bash\n./build.sh --build appimage --clean no\n```\n\n### Nix Build\n\n```bash\nnix build .#claude-desktop\nnix build .#claude-desktop-fhs\n```\n\nThe 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`](docs/learnings/nix.md)).\n\n### Testing AppImage\n\n```bash\n# Run with logging\n./test-build/claude-desktop-*.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log\n```\n\n## Debugging Workflow\n\n### Inspecting the Running App's Code\n\n```bash\n# Find the mounted AppImage path\nmount | grep claude\n# Example: /tmp/.mount_claudeXXXXXX\n\n# Extract the running app's asar for inspection (official bare\n# co-located layout: ELF + chrome-sandbox + resources/ side by side)\nnpx asar extract /tmp/.mount_claudeXXXXXX/usr/lib/claude-desktop/resources/app.asar /tmp/claude-inspect\n\n# Search for patterns in the extracted code. Since 1.19367.0 the main\n# process is code-split, so grep across all chunks (index.js is a stub);\n# main-process anchors live in index.chunk-<hash>.js.\ngrep -rn \"pattern\" /tmp/claude-inspect/.vite/build/\n```\n\n### Checking DBus/Tray Status\n\n```bash\n# List registered tray icons\ngdbus call --session --dest=org.kde.StatusNotifierWatcher \\\n  --object-path=/StatusNotifierWatcher \\\n  --method=org.freedesktop.DBus.Properties.Get \\\n  org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems\n\n# Find which process owns a DBus connection\ngdbus call --session --dest=org.freedesktop.DBus \\\n  --object-path=/org/freedesktop/DBus \\\n  --method=org.freedesktop.DBus.GetConnectionUnixProcessID \":1.XXXX\"\n```\n\n### Log Locations\n\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n- App logs: `~/.config/Claude/logs/`\n- Run with logging: `./app.AppImage 2>&1 | tee ~/.cache/claude-desktop-debian/launcher.log`\n\n## Useful Locations\n\n- App data: `~/.config/Claude/`\n- Logs: `~/.config/Claude/logs/`\n- SingletonLock: `~/.config/Claude/SingletonLock`\n- Launcher log: `~/.cache/claude-desktop-debian/launcher.log`\n\n## Versioning\n\nRelease versions are managed via two GitHub Actions repository variables (not files):\n\n- **`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.\n- **`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.\n\n### Tag format\n\nTags 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.\n\n```bash\n# Check current values\ngh variable get REPO_VERSION\ngh variable get CLAUDE_DESKTOP_VERSION\n\n# Bump repo version and tag a release\ngh variable set REPO_VERSION --body \"1.3.24\"\ngit tag \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\ngit push origin \"v1.3.24+claude$(gh variable get CLAUDE_DESKTOP_VERSION)\"\n```\n\nWhen 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.\n\n## Common Gotchas\n\n- **`.zsync` files** - Used for delta updates, can be ignored/deleted\n- **AppImage mount points** - Running AppImages mount to `/tmp/.mount_claude*`; check with `mount | grep claude`\n- **Killing the app** - Must kill all electron child processes, not just the main one:\n  ```bash\n  pkill -9 -f \"mount_claude\"\n  ```\n- **SingletonLock** - If app won't start, check for stale lock: `~/.config/Claude/SingletonLock`\n- **Node version** - Build requires Node.js; the script downloads its own if needed (keyed to the HOST arch — see the cross-build learning)\n- **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:\n  ```bash\n  # Check repo variable (source of truth)\n  gh variable get CLAUDE_DESKTOP_VERSION\n\n  # Check the pinned version on your branch\n  grep -oP \"^OFFICIAL_DEB_VERSION='\\K[^']+\" scripts/setup/official-deb.sh\n\n  # What the official pool currently serves\n  curl -fsS \"https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages\" \\\n    | grep -E '^Version:' | sort -V | tail -1\n  ```\n- **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\n","category":"root","tokens":8265}]}