┌─ NIX ──────────────────────────────────────────────────────────────────────┐
│ # adapted from github:aaddrick/claude-desktop-debian (nix/claude-desktop.nix) │
│ # build scripts are pulled via fetchFromGitHub rather than vendoring the repo │
│ # to bump: update repoSrc rev/hash + version + the windows exe srcs below │
│ { │
│ lib, │
│ stdenvNoCC, │
│ fetchurl, │
│ fetchFromGitHub, │
│ electron, │
│ p7zip, │
│ icoutils, │
│ imagemagick, │
│ nodejs, │
│ asar, │
│ makeDesktopItem, │
│ python3, │
│ bash, │
│ getent, │
│ node-pty, │
│ }: let │
│ pname = "claude-desktop"; │
│ version = "1.12603.1"; │
│ │
│ # upstream repo providing build.sh + scripts/ (the actual packaging logic) │
│ repoSrc = fetchFromGitHub { │
│ owner = "aaddrick"; │
│ repo = "claude-desktop-debian"; │
│ rev = "d2ce0466315033938ea6c2066d424239feffabf0"; # v2.0.20+claude1.12603.1 │
│ hash = "sha256-vMMjROGD/huIbrvTV3okyMPseleQ3dsFOabJo9TtfoI="; │
│ }; │
│ │
│ srcs = { │
│ x86_64-linux = fetchurl { │
│ url = "https://downloads.claude.ai/releases/win32/x64/1.12603.1/Claude-3df4f │
│ d263723119bc45f0af2d784afd5055e2ba9.exe"; │
│ hash = "sha256-Fym+uhmkDkkA8+32jk3B7BxFJ8TbM1sTOADJlkUMl6g="; │
│ }; │
│ aarch64-linux = fetchurl { │
│ url = "https://downloads.claude.ai/releases/win32/arm64/1.12603.1/Claude-3df │
│ 4fd263723119bc45f0af2d784afd5055e2ba9.exe"; │
│ hash = "sha256-jthQmwDvYBV89iR9xdrUccCQOTaEEgyfjxFoVIZEcKw="; │
│ }; │
│ }; │
│ │
│ src = srcs.${stdenvNoCC.hostPlatform.system} or (throw "Unsupported system: ${st │
│ denvNoCC.hostPlatform.system}"); │
│ │
│ # The unwrapped electron derivation - contains the real ELF binary │
│ # and Chromium resources (.pak files, locales/, etc.). │
│ electronUnwrapped = electron.passthru.unwrapped or electron; │
│ electronDir = "${electronUnwrapped}/libexec/electron"; │
│ │
│ desktopItem = makeDesktopItem { │
│ name = "claude-desktop"; │
│ exec = "claude-desktop %u"; │
│ icon = "claude-desktop"; │
│ type = "Application"; │
│ terminal = false; │
│ desktopName = "Claude"; │
│ genericName = "Claude Desktop"; │
│ startupWMClass = "Claude"; │
│ categories = ["Office" "Utility"]; │
│ mimeTypes = ["x-scheme-handler/claude"]; │
│ }; │
│ in │
│ stdenvNoCC.mkDerivation { │
│ inherit pname version src; │
│ │
│ nativeBuildInputs = [ │
│ p7zip │
│ nodejs │
│ asar │
│ icoutils │
│ imagemagick │
│ bash │
│ python3 │
│ getent │
│ ]; │
│ │
│ # The exe is not a standard archive - use manual unpack │
│ dontUnpack = true; │
│ │
│ buildPhase = '' │
│ runHook preBuild │
│ │
│ export HOME=$TMPDIR │
│ │
│ # Writable copy of upstream so we can fix its #649 add-dir patch. │
│ # v2.0.20's patch asserts the minified --add-dir dispatch snippet │
│ # appears exactly once and patches only the first hit, but claude │
│ # 1.12603.1 carries it twice and the assertion fatals. The two hits │
│ # are byte-identical (upstream counts an escaped literal), so the │
│ # filtered replacement it computes is correct for both - rewrite the │
│ # patch to neutralise the count guard and replace all occurrences. │
│ cp -r --no-preserve=mode,ownership ${repoSrc} ./upstream │
│ substituteInPlace ./upstream/scripts/patches/config.sh \ │
│ --replace-fail 'allMatches && allMatches.length > 1' 'false' \ │
│ --replace-fail 'code = code.replace(match[0], filtered);' 'code = code.spl │
│ it(match[0]).join(filtered);' │
│ │
│ # Copy exe to a writable location for build.sh │
│ cp $src Claude-Setup.exe │
│ │
│ # Run build.sh in nix mode - it handles extraction, patching, icon │
│ # extraction, and asar repacking. --source-dir points at our patched │
│ # copy so build.sh finds scripts/. │
│ bash ./upstream/build.sh \ │
│ --exe "$(pwd)/Claude-Setup.exe" \ │
│ --source-dir "$(pwd)/upstream" \ │
│ --node-pty-dir "${node-pty}/lib/node_modules/node-pty" \ │
│ --build nix \ │
│ --clean no │
│ │
│ runHook postBuild │
│ ''; │
│ │
│ installPhase = '' │
│ runHook preInstall │
│ │
│ #===================================================================== │
│ ===== │
│ # Create a custom Electron tree with app resources co-located. │
│ # │
│ # On NixOS, the stock electron-unwrapped lives in a read-only store │
│ # path. Chromium computes process.resourcesPath from /proc/self/exe, │
│ # so it always points to electron-unwrapped's resources/ dir - which │
│ # doesn't contain the app's locale JSONs, tray icons, etc. When │
│ # ELECTRON_FORCE_IS_PACKAGED=true, the app reads en-US.json from │
│ # resourcesPath at module load time (before frame-fix-wrapper.js can │
│ # correct the path), causing an ENOENT crash. │
│ # │
│ # Solution: copy the Electron ELF binary into our own tree so that │
│ # /proc/self/exe resolves here, then merge both Electron's and the │
│ # app's resources into resources/. Everything else (shared libs, │
│ # .pak files, locales/) is symlinked to avoid duplication. │
│ #===================================================================== │
│ ===== │
│ electron_tree=$out/lib/claude-desktop/electron │
│ │
│ mkdir -p $electron_tree/resources │
│ │
│ # Copy the ELF binary - MUST be a real copy (not symlink) so that │
│ # /proc/self/exe resolves to our tree │
│ cp ${electronDir}/electron $electron_tree/electron │
│ chmod +x $electron_tree/electron │
│ │
│ # Symlink everything else from electron-unwrapped │
│ for item in ${electronDir}/*; do │
│ name=$(basename "$item") │
│ [[ "$name" = "electron" ]] && continue │
│ [[ "$name" = "resources" ]] && continue │
│ ln -s "$item" "$electron_tree/$name" │
│ done │
│ │
│ # Populate resources/ - start with Electron's own (default_app.asar) │
│ for item in ${electronDir}/resources/*; do │
│ ln -s "$item" "$electron_tree/resources/$(basename "$item")" │
│ done │
│ │
│ # Install app.asar and unpacked resources into the merged tree │
│ cp build/electron-app/app.asar $electron_tree/resources/ │
│ cp -r build/electron-app/app.asar.unpacked $electron_tree/resources/ │
│ │
│ # Install tray icons into resources │
│ for tray_icon in build/electron-app/nix-resources/Tray*; do │
│ [[ -f "$tray_icon" ]] && cp "$tray_icon" $electron_tree/resources/ │
│ done │
│ │
│ # Install SSH helpers into resources │
│ if [[ -d build/electron-app/nix-resources/claude-ssh ]]; then │
│ cp -r build/electron-app/nix-resources/claude-ssh \ │
│ $electron_tree/resources/ │
│ fi │
│ │
│ # Install cowork resources (smol-bin, plugin shim) │
│ for cowork_res in build/electron-app/nix-resources/smol-bin.*.vhdx \ │
│ build/electron-app/nix-resources/cowork-plugin-shim. │
│ sh; do │
│ if [[ -f "$cowork_res" ]]; then │
│ cp "$cowork_res" $electron_tree/resources/ │
│ echo "Installed cowork resource: $(basename "$cowork_res")" │
│ fi │
│ done │
│ │
│ # Install ion-dist static assets (app:// protocol handler root for │
│ # Third-Party Inference setup - see issue #488) │
│ if [[ -d build/electron-app/nix-resources/ion-dist ]]; then │
│ cp -r build/electron-app/nix-resources/ion-dist \ │
│ $electron_tree/resources/ │
│ echo "Installed cowork resource: ion-dist" │
│ fi │
│ │
│ # Install locale JSON files into resources │
│ for locale_json in build/claude-extract/lib/net45/resources/*-*.json; │
│ do │
│ [[ -f "$locale_json" ]] \ │
│ && cp "$locale_json" $electron_tree/resources/ │
│ done │
│ │
│ # Create the electron wrapper - replicates the env setup from the │
│ # stock electron wrapper (GIO, GTK, GDK_PIXBUF, XDG_DATA_DIRS) but │
│ # execs our custom binary. We extract everything except the final │
│ # exec line from the stock wrapper, then append our own exec. │
│ head -n -1 ${electron}/bin/electron > $electron_tree/electron-wrapper │
│ echo "exec \"$electron_tree/electron\" \"\$@\"" >> $electron_tree/elec │
│ tron-wrapper │
│ chmod +x $electron_tree/electron-wrapper │
│ │
│ # Update CHROME_DEVEL_SANDBOX to point to our tree's chrome-sandbox │
│ substituteInPlace $electron_tree/electron-wrapper \ │
│ --replace-quiet "${electron}/libexec/electron/chrome-sandbox" \ │
│ "$electron_tree/chrome-sandbox" │
│ │
│ #===================================================================== │
│ ===== │
│ # Standard install (icons, desktop file, launcher) │
│ #===================================================================== │
│ ===== │
│ │
│ # Convenience symlink for resources dir (used by launcher, FHS, etc.) │
│ ln -s $electron_tree/resources $out/lib/claude-desktop/resources │
│ │
│ # Install icons │
│ for size in 16 24 32 48 64 256; do │
│ icon_dir=$out/share/icons/hicolor/"$size"x"$size"/apps │
│ mkdir -p "$icon_dir" │
│ icon=$(find build/ -name "claude_*''${size}x''${size}x32.png" 2>/dev │
│ /null | head -1) │
│ if [[ -n "$icon" ]]; then │
│ install -Dm644 "$icon" "$icon_dir/claude-desktop.png" │
│ fi │
│ done │
│ │
│ # Install shared launcher library + doctor (launcher-common.sh │
│ # sources doctor.sh at runtime, so both must live in the same dir) │
│ install -Dm755 ${repoSrc}/scripts/launcher-common.sh \ │
│ $out/lib/claude-desktop/launcher-common.sh │
│ install -Dm755 ${repoSrc}/scripts/doctor.sh \ │
│ $out/lib/claude-desktop/doctor.sh │
│ │
│ # Install .desktop file │
│ mkdir -p $out/share/applications │
│ install -Dm644 ${desktopItem}/share/applications/* $out/share/applicat │
│ ions/ │
│ │
│ # Create launcher script │
│ mkdir -p $out/bin │
│ cat > $out/bin/claude-desktop <<'LAUNCHER' │
│ #!/usr/bin/env bash │
│ # Claude Desktop launcher for NixOS │
│ │
│ electron_exec="ELECTRON_PLACEHOLDER" │
│ app_path="RESOURCES_PLACEHOLDER/app.asar" │
│ │
│ source "LAUNCHER_LIB_PLACEHOLDER" │
│ │
│ # Handle --doctor flag before anything else │
│ if [[ "''${1:-}" == '--doctor' ]]; then │
│ run_doctor "$electron_exec" │
│ exit $? │
│ fi │
│ │
│ # Setup logging and environment │
│ setup_logging || exit 1 │
│ setup_electron_env │
│ cleanup_orphaned_cowork_daemon │
│ cleanup_stale_desktop_helpers │
│ cleanup_stale_lock │
│ cleanup_stale_cowork_socket │
│ │
│ # Log startup info │
│ log_message '--- Claude Desktop Launcher Start (NixOS) ---' │
│ log_message "Timestamp: $(date)" │
│ log_message "Arguments: $@" │
│ log_session_env │
│ │
│ # Check for display │
│ if ! check_display; then │
│ log_message 'No display detected (TTY session)' │
│ echo 'Error: Claude Desktop requires a graphical desktop environment.' >&2 │
│ echo 'Please run from within an X11 or Wayland session, not from a TTY.' >& │
│ 2 │
│ exit 1 │
│ fi │
│ │
│ # Detect display backend (handles CLAUDE_USE_WAYLAND) │
│ detect_display_backend │
│ │
│ # Build Electron arguments │
│ build_electron_args 'nix' │
│ │
│ # Intentionally NOT appended: app.asar sits in Electron's default │
│ # resources/ dir next to the binary, so Electron auto-loads it. Passing │
│ # the path again makes Electron treat it as a file-to-open, which the │
│ # app forwards to its file-drop handler, producing a spurious │
│ # "Attach app.asar?" prompt on launch and on every taskbar reopen │
│ # (the second-instance argv path). Omitting it is the root-cause fix. │
│ # See issue #696. │
│ log_message "App (auto-loaded by Electron): $app_path" │
│ │
│ # Execute Electron and keep the launcher alive so explicit quit can │
│ # clean up Desktop-owned helpers that outlive the Electron main process. │
│ log_message "Executing: $electron_exec ''${electron_args[*]} $*" │
│ run_electron_and_cleanup "$electron_exec" "''${electron_args[@]}" "$@" │
│ exit $? │
│ LAUNCHER │
│ # Substitute placeholders - electron_exec points to our custom │
│ # wrapper (which sets GTK/GIO env then execs our merged binary) │
│ substituteInPlace $out/bin/claude-desktop \ │
│ --replace-fail "ELECTRON_PLACEHOLDER" "$electron_tree/electron-wrapp │
│ er" \ │
│ --replace-fail "RESOURCES_PLACEHOLDER" "$electron_tree/resources" \ │
│ --replace-fail "LAUNCHER_LIB_PLACEHOLDER" "$out/lib/claude-desktop/l │
│ auncher-common.sh" │
│ chmod +x $out/bin/claude-desktop │
│ │
│ runHook postInstall │
│ ''; │
│ │
│ meta = with lib; { │
│ description = "Claude Desktop for Linux"; │
│ homepage = "https://github.com/aaddrick/claude-desktop-debian"; │
│ license = licenses.unfree; │
│ platforms = ["x86_64-linux" "aarch64-linux"]; │
│ sourceProvenance = with sourceTypes; [binaryNativeCode]; │
│ mainProgram = "claude-desktop"; │
│ }; │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ NIX ────────────────────────────────┐
│ # adapted from github:aaddrick/claude-deskto │
│ p-debian (nix/claude-desktop.nix) │
│ # build scripts are pulled via fetchFromGitH │
│ ub rather than vendoring the repo │
│ # to bump: update repoSrc rev/hash + version │
│ + the windows exe srcs below │
│ { │
│ lib, │
│ stdenvNoCC, │
│ fetchurl, │
│ fetchFromGitHub, │
│ electron, │
│ p7zip, │
│ icoutils, │
│ imagemagick, │
│ nodejs, │
│ asar, │
│ makeDesktopItem, │
│ python3, │
│ bash, │
│ getent, │
│ node-pty, │
│ }: let │
│ pname = "claude-desktop"; │
│ version = "1.12603.1"; │
│ │
│ # upstream repo providing build.sh + scrip │
│ ts/ (the actual packaging logic) │
│ repoSrc = fetchFromGitHub { │
│ owner = "aaddrick"; │
│ repo = "claude-desktop-debian"; │
│ rev = "d2ce0466315033938ea6c2066d424239f │
│ effabf0"; # v2.0.20+claude1.12603.1 │
│ hash = "sha256-vMMjROGD/huIbrvTV3okyMPse │
│ leQ3dsFOabJo9TtfoI="; │
│ }; │
│ │
│ srcs = { │
│ x86_64-linux = fetchurl { │
│ url = "https://downloads.claude.ai/rel │
│ eases/win32/x64/1.12603.1/Claude-3df4fd26372 │
│ 3119bc45f0af2d784afd5055e2ba9.exe"; │
│ hash = "sha256-Fym+uhmkDkkA8+32jk3B7Bx │
│ FJ8TbM1sTOADJlkUMl6g="; │
│ }; │
│ aarch64-linux = fetchurl { │
│ url = "https://downloads.claude.ai/rel │
│ eases/win32/arm64/1.12603.1/Claude-3df4fd263 │
│ 723119bc45f0af2d784afd5055e2ba9.exe"; │
│ hash = "sha256-jthQmwDvYBV89iR9xdrUccC │
│ QOTaEEgyfjxFoVIZEcKw="; │
│ }; │
│ }; │
│ │
│ src = srcs.${stdenvNoCC.hostPlatform.syste │
│ m} or (throw "Unsupported system: ${stdenvNo │
│ CC.hostPlatform.system}"); │
│ │
│ # The unwrapped electron derivation - cont │
│ ains the real ELF binary │
│ # and Chromium resources (.pak files, loca │
│ les/, etc.). │
│ electronUnwrapped = electron.passthru.unwr │
│ apped or electron; │
│ electronDir = "${electronUnwrapped}/libexe │
│ c/electron"; │
│ │
│ desktopItem = makeDesktopItem { │
│ name = "claude-desktop"; │
│ exec = "claude-desktop %u"; │
│ icon = "claude-desktop"; │
│ type = "Application"; │
│ terminal = false; │
│ desktopName = "Claude"; │
│ genericName = "Claude Desktop"; │
│ startupWMClass = "Claude"; │
│ categories = ["Office" "Utility"]; │
│ mimeTypes = ["x-scheme-handler/claude"]; │
│ }; │
│ in │
│ stdenvNoCC.mkDerivation { │
│ inherit pname version src; │
│ │
│ nativeBuildInputs = [ │
│ p7zip │
│ nodejs │
│ asar │
│ icoutils │
│ imagemagick │
│ bash │
│ python3 │
│ getent │
│ ]; │
│ │
│ # The exe is not a standard archive - us │
│ e manual unpack │
│ dontUnpack = true; │
│ │
│ buildPhase = '' │
│ runHook preBuild │
│ │
│ export HOME=$TMPDIR │
│ │
│ # Writable copy of upstream so we can │
│ fix its #649 add-dir patch. │
│ # v2.0.20's patch asserts the minified │
│ --add-dir dispatch snippet │
│ # appears exactly once and patches onl │
│ y the first hit, but claude │
│ # 1.12603.1 carries it twice and the a │
│ ssertion fatals. The two hits │
│ # are byte-identical (upstream counts │
│ an escaped literal), so the │
│ # filtered replacement it computes is │
│ correct for both - rewrite the │
│ # patch to neutralise the count guard │
│ and replace all occurrences. │
│ cp -r --no-preserve=mode,ownership ${r │
│ epoSrc} ./upstream │
│ substituteInPlace ./upstream/scripts/p │
│ atches/config.sh \ │
│ --replace-fail 'allMatches && allMat │
│ ches.length > 1' 'false' \ │
│ --replace-fail 'code = code.replace( │
│ match[0], filtered);' 'code = code.split(mat │
│ ch[0]).join(filtered);' │
│ │
│ # Copy exe to a writable location for │
│ build.sh │
│ cp $src Claude-Setup.exe │
│ │
│ # Run build.sh in nix mode - it handle │
│ s extraction, patching, icon │
│ # extraction, and asar repacking. --so │
│ urce-dir points at our patched │
│ # copy so build.sh finds scripts/. │
│ bash ./upstream/build.sh \ │
│ --exe "$(pwd)/Claude-Setup.exe" \ │
│ --source-dir "$(pwd)/upstream" \ │
│ --node-pty-dir "${node-pty}/lib/node │
│ _modules/node-pty" \ │
│ --build nix \ │
│ --clean no │
│ │
│ runHook postBuild │
│ ''; │
│ │
│ installPhase = '' │
│ runHook preInstall │
│ │
│ #=============================== │
│ =========================================== │
│ # Create a custom Electron tree │
│ with app resources co-located. │
│ # │
│ # On NixOS, the stock electron-u │
│ nwrapped lives in a read-only store │
│ # path. Chromium computes proce │
│ ss.resourcesPath from /proc/self/exe, │
│ # so it always points to electro │
│ n-unwrapped's resources/ dir - which │
│ # doesn't contain the app's loca │
│ le JSONs, tray icons, etc. When │
│ # ELECTRON_FORCE_IS_PACKAGED=tru │
│ e, the app reads en-US.json from │
│ # resourcesPath at module load t │
│ ime (before frame-fix-wrapper.js can │
│ # correct the path), causing an │
│ ENOENT crash. │
│ # │
│ # Solution: copy the Electron EL │
│ F binary into our own tree so that │
│ # /proc/self/exe resolves here, │
│ then merge both Electron's and the │
│ # app's resources into resources │
│ /. Everything else (shared libs, │
│ # .pak files, locales/) is symli │
│ nked to avoid duplication. │
│ #=============================== │
│ =========================================== │
│ electron_tree=$out/lib/claude-de │
│ sktop/electron │
│ │
│ mkdir -p $electron_tree/resource │
│ s │
│ │
│ # Copy the ELF binary - MUST be │
│ a real copy (not symlink) so that │
│ # /proc/self/exe resolves to our │
│ tree │
│ cp ${electronDir}/electron $elec │
│ tron_tree/electron │
│ chmod +x $electron_tree/electron │
│ │
│ # Symlink everything else from e │
│ lectron-unwrapped │
│ for item in ${electronDir}/*; do │
│ name=$(basename "$item") │
│ [[ "$name" = "electron" ]] && │
│ continue │
│ [[ "$name" = "resources" ]] && │
│ continue │
│ ln -s "$item" "$electron_tree/ │
│ $name" │
│ done │
│ │
│ # Populate resources/ - start wi │
│ th Electron's own (default_app.asar) │
│ for item in ${electronDir}/resou │
│ rces/*; do │
│ ln -s "$item" "$electron_tree/ │
│ resources/$(basename "$item")" │
│ done │
│ │
│ # Install app.asar and unpacked │
│ resources into the merged tree │
│ cp build/electron-app/app.asar $ │
│ electron_tree/resources/ │
│ cp -r build/electron-app/app.asa │
│ r.unpacked $electron_tree/resources/ │
│ │
│ # Install tray icons into resour │
│ ces │
│ for tray_icon in build/electron- │
│ app/nix-resources/Tray*; do │
│ [[ -f "$tray_icon" ]] && cp "$ │
│ tray_icon" $electron_tree/resources/ │
│ done │
│ │
│ # Install SSH helpers into resou │
│ rces │
│ if [[ -d build/electron-app/nix- │
│ resources/claude-ssh ]]; then │
│ cp -r build/electron-app/nix-r │
│ esources/claude-ssh \ │
│ $electron_tree/resources/ │
│ fi │
│ │
│ # Install cowork resources (smol │
│ -bin, plugin shim) │
│ for cowork_res in build/electron │
│ -app/nix-resources/smol-bin.*.vhdx \ │
│ build/electron │
│ -app/nix-resources/cowork-plugin-shim.sh; do │
│ if [[ -f "$cowork_res" ]]; the │
│ n │
│ cp "$cowork_res" $electron_t │
│ ree/resources/ │
│ echo "Installed cowork resou │
│ rce: $(basename "$cowork_res")" │
│ fi │
│ done │
│ │
│ # Install ion-dist static assets │
│ (app:// protocol handler root for │
│ # Third-Party Inference setup - │
│ see issue #488) │
│ if [[ -d build/electron-app/nix- │
│ resources/ion-dist ]]; then │
│ cp -r build/electron-app/nix-r │
│ esources/ion-dist \ │
│ $electron_tree/resources/ │
│ echo "Installed cowork resourc │
│ e: ion-dist" │
│ fi │
│ │
│ # Install locale JSON files into │
│ resources │
│ for locale_json in build/claude- │
│ extract/lib/net45/resources/*-*.json; do │
│ [[ -f "$locale_json" ]] \ │
│ && cp "$locale_json" $electr │
│ on_tree/resources/ │
│ done │
│ │
│ # Create the electron wrapper - │
│ replicates the env setup from the │
│ # stock electron wrapper (GIO, G │
│ TK, GDK_PIXBUF, XDG_DATA_DIRS) but │
│ # execs our custom binary. We e │
│ xtract everything except the final │
│ # exec line from the stock wrapp │
│ er, then append our own exec. │
│ head -n -1 ${electron}/bin/elect │
│ ron > $electron_tree/electron-wrapper │
│ echo "exec \"$electron_tree/elec │
│ tron\" \"\$@\"" >> $electron_tree/electron-w │
│ rapper │
│ chmod +x $electron_tree/electron │
│ -wrapper │
│ │
│ # Update CHROME_DEVEL_SANDBOX to │
│ point to our tree's chrome-sandbox │
│ substituteInPlace $electron_tree │
│ /electron-wrapper \ │
│ --replace-quiet "${electron}/l │
│ ibexec/electron/chrome-sandbox" \ │
│ "$electron_tree/chrome-sandb │
│ ox" │
│ │
│ #=============================== │
│ =========================================== │
│ # Standard install (icons, deskt │
│ op file, launcher) │
│ #=============================== │
│ =========================================== │
│ │
│ # Convenience symlink for resour │
│ ces dir (used by launcher, FHS, etc.) │
│ ln -s $electron_tree/resources $ │
│ out/lib/claude-desktop/resources │
│ │
│ # Install icons │
│ for size in 16 24 32 48 64 256; │
│ do │
│ icon_dir=$out/share/icons/hico │
│ lor/"$size"x"$size"/apps │
│ mkdir -p "$icon_dir" │
│ icon=$(find build/ -name "clau │
│ de_*''${size}x''${size}x32.png" 2>/dev/null │
│ | head -1) │
│ if [[ -n "$icon" ]]; then │
│ install -Dm644 "$icon" "$ico │
│ n_dir/claude-desktop.png" │
│ fi │
│ done │
│ │
│ # Install shared launcher librar │
│ y + doctor (launcher-common.sh │
│ # sources doctor.sh at runtime, │
│ so both must live in the same dir) │
│ install -Dm755 ${repoSrc}/script │
│ s/launcher-common.sh \ │
│ $out/lib/claude-desktop/launch │
│ er-common.sh │
│ install -Dm755 ${repoSrc}/script │
│ s/doctor.sh \ │
│ $out/lib/claude-desktop/doctor │
│ .sh │
│ │
│ # Install .desktop file │
│ mkdir -p $out/share/applications │
│ install -Dm644 ${desktopItem}/sh │
│ are/applications/* $out/share/applications/ │
│ │
│ # Create launcher script │
│ mkdir -p $out/bin │
│ cat > $out/bin/claude-desktop << │
│ 'LAUNCHER' │
│ #!/usr/bin/env bash │
│ # Claude Desktop launcher for NixOS │
│ │
│ electron_exec="ELECTRON_PLACEHOLDER" │
│ app_path="RESOURCES_PLACEHOLDER/app.as │
│ ar" │
│ │
│ source "LAUNCHER_LIB_PLACEHOLDER" │
│ │
│ # Handle --doctor flag before anything │
│ else │
│ if [[ "''${1:-}" == '--doctor' ]]; the │
│ n │
│ run_doctor "$electron_exec" │
│ exit $? │
│ fi │
│ │
│ # Setup logging and environment │
│ setup_logging || exit 1 │
│ setup_electron_env │
│ cleanup_orphaned_cowork_daemon │
│ cleanup_stale_desktop_helpers │
│ cleanup_stale_lock │
│ cleanup_stale_cowork_socket │
│ │
│ # Log startup info │
│ log_message '--- Claude Desktop Launch │
│ er Start (NixOS) ---' │
│ log_message "Timestamp: $(date)" │
│ log_message "Arguments: $@" │
│ log_session_env │
│ │
│ # Check for display │
│ if ! check_display; then │
│ log_message 'No display detected (TTY │
│ session)' │
│ echo 'Error: Claude Desktop requires │
│ a graphical desktop environment.' >&2 │
│ echo 'Please run from within an X11 o │
│ r Wayland session, not from a TTY.' >&2 │
│ exit 1 │
│ fi │
│ │
│ # Detect display backend (handles CLAU │
│ DE_USE_WAYLAND) │
│ detect_display_backend │
│ │
│ # Build Electron arguments │
│ build_electron_args 'nix' │
│ │
│ # Intentionally NOT appended: app.asar │
│ sits in Electron's default │
│ # resources/ dir next to the binary, s │
│ o Electron auto-loads it. Passing │
│ # the path again makes Electron treat │
│ it as a file-to-open, which the │
│ # app forwards to its file-drop handle │
│ r, producing a spurious │
│ # "Attach app.asar?" prompt on launch │
│ and on every taskbar reopen │
│ # (the second-instance argv path). Omi │
│ tting it is the root-cause fix. │
│ # See issue #696. │
│ log_message "App (auto-loaded by Elect │
│ ron): $app_path" │
│ │
│ # Execute Electron and keep the launch │
│ er alive so explicit quit can │
│ # clean up Desktop-owned helpers that │
│ outlive the Electron main process. │
│ log_message "Executing: $electron_exec │
│ ''${electron_args[*]} $*" │
│ run_electron_and_cleanup "$electron_ex │
│ ec" "''${electron_args[@]}" "$@" │
│ exit $? │
│ LAUNCHER │
│ # Substitute placeholders - elec │
│ tron_exec points to our custom │
│ # wrapper (which sets GTK/GIO en │
│ v then execs our merged binary) │
│ substituteInPlace $out/bin/claud │
│ e-desktop \ │
│ --replace-fail "ELECTRON_PLACE │
│ HOLDER" "$electron_tree/electron-wrapper" \ │
│ --replace-fail "RESOURCES_PLAC │
│ EHOLDER" "$electron_tree/resources" \ │
│ --replace-fail "LAUNCHER_LIB_P │
│ LACEHOLDER" "$out/lib/claude-desktop/launche │
│ r-common.sh" │
│ chmod +x $out/bin/claude-desktop │
│ │
│ runHook postInstall │
│ ''; │
│ │
│ meta = with lib; { │
│ description = "Claude Desktop for Linu │
│ x"; │
│ homepage = "https://github.com/aaddric │
│ k/claude-desktop-debian"; │
│ license = licenses.unfree; │
│ platforms = ["x86_64-linux" "aarch64-l │
│ inux"]; │
│ sourceProvenance = with sourceTypes; [ │
│ binaryNativeCode]; │
│ mainProgram = "claude-desktop"; │
│ }; │
│ } │
└──────────────────────────────────────────────┘