OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      517baeda9f7c
DATE      2026-06-15
SUBJECT   claude: claude desktop + usage monitor
FILES     12 CHANGED
HASH      517baeda9f7c
DATE      2026-06-15
SUBJECT   claude: claude desktop + usage monitor
FILES     12 CHANGED
 

diff --git a/hosts/ooksdesk/default.nix b/hosts/ooksdesk/default.nix
index b8d3647..54f4502 100644
--- a/hosts/ooksdesk/default.nix
+++ b/hosts/ooksdesk/default.nix
@@ -26,7 +26,7 @@
       environment = "hyprland";
       theme = "minimal";
       sunshine.enable = true;
-      profiles = ["creative" "virtualization" "gaming" "media" "communication" "p
roductivity" "infra" "work"];
+      profiles = ["creative" "virtualization" "gaming" "media" "communication" "p
roductivity" "infra" "work" "ai"];
       default = {
         browser = "zen";
         terminal = "foot";

diff --git a/hosts/ooksdesk/default.nix b/ho
sts/ooksdesk/default.nix
index b8d3647..54f4502 100644
--- a/hosts/ooksdesk/default.nix
+++ b/hosts/ooksdesk/default.nix
@@ -26,7 +26,7 @@
       environment = "hyprland";
       theme = "minimal";
       sunshine.enable = true;
-      profiles = ["creative" "virtualizatio
n" "gaming" "media" "communication" "product
ivity" "infra" "work"];
+      profiles = ["creative" "virtualizatio
n" "gaming" "media" "communication" "product
ivity" "infra" "work" "ai"];
       default = {
         browser = "zen";
         terminal = "foot";
 

diff --git a/modules/common/console/profile/standard.nix b/modules/common/console/
profile/standard.nix
index 76085b1..bcec65e 100644
--- a/modules/common/console/profile/standard.nix
+++ b/modules/common/console/profile/standard.nix
@@ -12,6 +12,7 @@ in {
       multiplexer = "zellij";
       tools = {
         codex.enable = true;
+        claude.enable = true;
         bat.enable = true;
         btop.enable = true;
         direnv.enable = true;

diff --git a/modules/common/console/profile/
standard.nix b/modules/common/console/profil
e/standard.nix
index 76085b1..bcec65e 100644
--- a/modules/common/console/profile/standar
d.nix
+++ b/modules/common/console/profile/standar
d.nix
@@ -12,6 +12,7 @@ in {
       multiplexer = "zellij";
       tools = {
         codex.enable = true;
+        claude.enable = true;
         bat.enable = true;
         btop.enable = true;
         direnv.enable = true;
 

diff --git a/modules/home/console/tools/claude.nix b/modules/home/console/tools/cl
aude.nix
new file mode 100644
index 0000000..6a0b781
--- /dev/null
+++ b/modules/home/console/tools/claude.nix
@@ -0,0 +1,163 @@
+{
+  osConfig,
+  lib,
+  pkgs,
+  ...
+}: let
+  inherit (lib) mkIf;
+  inherit (builtins) attrValues;
+  cfg = osConfig.ooknet.console.tools.claude;
+
+  # reusable usage core. fetches the oauth usage endpoint (same data as /usage an
d
+  # the website usage tab), caches with stale-while-revalidate. quickshell reads 
this too.
+  ookusage = pkgs.writeShellApplication {
+    name = "ookusage";
+    runtimeInputs = attrValues {
+      inherit (pkgs) jq curl coreutils util-linux;
+    };
+    text = ''
+      # ookusage - claude subscription usage (% of 5h + weekly limits) from the
+      # oauth usage endpoint. emits the raw endpoint json, cached. token is read
+      # internally, never logged; cache holds only percentages.
+      #   ookusage           print cache, refresh in background if stale (swr)
+      #   ookusage refresh   force fetch, print
+      #   ookusage __bg      internal: non-blocking fetch, silent
+      set -euo pipefail
+
+      endpoint="https://api.anthropic.com/api/oauth/usage"
+      cache_dir="''${XDG_CACHE_HOME:-$HOME/.cache}/ookusage"
+      cache="$cache_dir/usage.json"
+      lock="$cache_dir/.lock"
+      ttl="''${OOKUSAGE_TTL:-60}"
+
+      get_token() {
+        local t=""
+        if [[ -n "''${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then printf '%s' "$CLAUDE_C
ODE_OAUTH_TOKEN"; return; fi
+        # linux: plaintext credentials file
+        if [[ -r "$HOME/.claude/.credentials.json" ]]; then
+          t="$(jq -r '.claudeAiOauth.accessToken // empty' "$HOME/.claude/.creden
tials.json" 2>/dev/null || true)"
+        fi
+        # linux: libsecret keyring
+        if [[ -z "$t" ]] && command -v secret-tool >/dev/null 2>&1; then
+          t="$(secret-tool lookup service "Claude Code-credentials" 2>/dev/null \
+               | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null || true)
"
+        fi
+        # macos: keychain
+        if [[ -z "$t" ]] && command -v security >/dev/null 2>&1; then
+          t="$(security find-generic-password -s "Claude Code-credentials" -w 2>/
dev/null \
+               | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null || true)
"
+        fi
+        printf '%s' "$t"
+      }
+
+      fetch() {
+        local tok; tok="$(get_token)"
+        [[ -z "$tok" ]] && return 1
+        curl -sS --max-time 10 "$endpoint" \
+          -H "Authorization: Bearer $tok" \
+          -H "anthropic-beta: oauth-2025-04-20"
+      }
+
+      refresh() { # $1 = -n for non-blocking bg refresh
+        mkdir -p "$cache_dir"
+        exec 9>"$lock"
+        if [ "''${1:-}" = "-n" ]; then flock -n 9 || return 0; else flock 9 || re
turn 0; fi
+        local tmp; tmp="$(mktemp "$cache.XXXXXX")"
+        # only replace cache if we got a sane response (has five_hour)
+        if fetch >"$tmp" 2>/dev/null && jq -e '.five_hour' "$tmp" >/dev/null 2>&1
; then
+          mv "$tmp" "$cache"
+        else
+          rm -f "$tmp"
+          return 1
+        fi
+      }
+
+      case "''${1:-}" in
+        refresh) refresh || true; cat "$cache" 2>/dev/null || echo '{"error":"fet
ch failed"}' ;;
+        __bg)    refresh -n || true ;;
+        *)
+          if [[ -f "$cache" ]]; then
+            cat "$cache"
+            age=$(( $(date +%s) - $(stat -c %Y "$cache") ))
+            if (( age > ttl )); then
+              setsid -f "$0" __bg >/dev/null 2>&1 || nohup "$0" __bg >/dev/null 2
>&1 &
+            fi
+          else
+            refresh || true
+            cat "$cache" 2>/dev/null || echo '{"error":"fetch failed"}'
+          fi
+          ;;
+      esac
+    '';
+  };
+
+  # claude status line formatter. model/dir/git + usage %
+  claude-statusline = pkgs.writeShellApplication {
+    name = "claude-statusline";
+    runtimeInputs =
+      [ookusage]
+      ++ attrValues {
+        inherit (pkgs) jq git coreutils gawk;
+      };
+    text = ''
+      # claude-statusline - model/dir/git + claude subscription usage % (5h + wee
kly)
+      # pulls live usage from `ookusage` (oauth usage endpoint, matches the websi
te)
+      set -euo pipefail
+
+      input="$(cat)"
+      usage="$(ookusage 2>/dev/null || echo '{}')"
+
+      model="$(jq -r '.model.display_name // .model.id // "claude"' <<<"$input")"
+      dir="$(jq -r '.workspace.current_dir // .cwd // empty' <<<"$input")"
+      [[ -z "$dir" ]] && dir="$PWD"
+      branch="$(git -C "$dir" branch --show-current 2>/dev/null || true)"
+      short="''${dir/#$HOME/\~}"
+
+      # ansi
+      d=$'\e[2m'; x=$'\e[0m'; bold=$'\e[1m'; blue=$'\e[34m'; grn=$'\e[32m'
+      sep="  ''${d}│''${x}  "
+      pctcol() { awk -v p="$1" 'BEGIN{p+=0; if(p>=90)printf"\033[31m";else if(p>=
70)printf"\033[33m";else printf"\033[32m"}'; }
+      fmt_reset() { # iso -> local HH:MM, or "Day HH:MM" if >24h away
+        local iso="$1"; [[ -z "$iso" || "$iso" == "null" ]] && return
+        local e now; e="$(date -d "$iso" +%s 2>/dev/null)" || return; now="$(date
 +%s)"
+        if (( e - now < 86400 )); then date -d "$iso" +%H:%M; else date -d "$iso"
 +'%a %H:%M'; fi
+      }
+      seg() { # label util reset_iso -> "label NN% ?HH:MM" colored, or nothing if
 util empty
+        local label="$1" util="$2" iso="$3"
+        [[ -z "$util" || "$util" == "null" ]] && return
+        local rs; rs="$(fmt_reset "$iso")"
+        printf '%s %s%.0f%%%s' "$label" "$(pctcol "$util")" "$util" "$x"
+        [[ -n "$rs" ]] && printf ' %s?%s%s' "$d" "$rs" "$x"
+      }
+
+      # newline-delimited (mapfile) so empty fields like a null opus slot are pre
served
+      mapfile -t U < <(jq -r '
+        (.five_hour.utilization      // "" | tostring),
+        (.five_hour.resets_at        // ""),
+        (.seven_day.utilization      // "" | tostring),
+        (.seven_day.resets_at        // ""),
+        (.seven_day_opus.utilization // "" | tostring),
+        (.seven_day_opus.resets_at   // "")
+      ' <<<"$usage")
+      fh="''${U[0]:-}"; fh_r="''${U[1]:-}"; sd="''${U[2]:-}"; sd_r="''${U[3]:-}";
 op="''${U[4]:-}"; op_r="''${U[5]:-}"
+
+      # left: context
+      printf '%s%s%s  %s%s%s' "$bold" "$model" "$x" "$blue" "$short" "$x"
+      [[ -n "$branch" ]] && printf '  %s %s%s' "$grn" "$branch" "$x"
+
+      # right: usage. weekly opus only shown when the plan reports it (non-null)
+      if [[ -z "$fh$sd" ]]; then
+        printf '%s%susage n/a%s' "$sep" "$d" "$x"
+      else
+        printf '%s%s' "$sep" "$(seg 5h "$fh" "$fh_r")"
+        printf '%s%s' "$sep" "$(seg wk "$sd" "$sd_r")"
+        [[ -n "$op" && "$op" != "null" ]] && printf '%s%s' "$sep" "$(seg opus "$o
p" "$op_r")"
+      fi
+      printf '\n'
+    '';
+  };
+in {
+  config = mkIf cfg.enable {
+    home.packages = [ookusage claude-statusline];
+  };
+}

diff --git a/modules/home/console/tools/clau
de.nix b/modules/home/console/tools/claude.n
ix
new file mode 100644
index 0000000..6a0b781
--- /dev/null
+++ b/modules/home/console/tools/claude.nix
@@ -0,0 +1,163 @@
+{
+  osConfig,
+  lib,
+  pkgs,
+  ...
+}: let
+  inherit (lib) mkIf;
+  inherit (builtins) attrValues;
+  cfg = osConfig.ooknet.console.tools.claud
e;
+
+  # reusable usage core. fetches the oauth 
usage endpoint (same data as /usage and
+  # the website usage tab), caches with sta
le-while-revalidate. quickshell reads this t
oo.
+  ookusage = pkgs.writeShellApplication {
+    name = "ookusage";
+    runtimeInputs = attrValues {
+      inherit (pkgs) jq curl coreutils util
-linux;
+    };
+    text = ''
+      # ookusage - claude subscription usag
e (% of 5h + weekly limits) from the
+      # oauth usage endpoint. emits the raw
 endpoint json, cached. token is read
+      # internally, never logged; cache hol
ds only percentages.
+      #   ookusage           print cache, r
efresh in background if stale (swr)
+      #   ookusage refresh   force fetch, p
rint
+      #   ookusage __bg      internal: non-
blocking fetch, silent
+      set -euo pipefail
+
+      endpoint="https://api.anthropic.com/a
pi/oauth/usage"
+      cache_dir="''${XDG_CACHE_HOME:-$HOME/
.cache}/ookusage"
+      cache="$cache_dir/usage.json"
+      lock="$cache_dir/.lock"
+      ttl="''${OOKUSAGE_TTL:-60}"
+
+      get_token() {
+        local t=""
+        if [[ -n "''${CLAUDE_CODE_OAUTH_TOK
EN:-}" ]]; then printf '%s' "$CLAUDE_CODE_OA
UTH_TOKEN"; return; fi
+        # linux: plaintext credentials file
+        if [[ -r "$HOME/.claude/.credential
s.json" ]]; then
+          t="$(jq -r '.claudeAiOauth.access
Token // empty' "$HOME/.claude/.credentials.
json" 2>/dev/null || true)"
+        fi
+        # linux: libsecret keyring
+        if [[ -z "$t" ]] && command -v secr
et-tool >/dev/null 2>&1; then
+          t="$(secret-tool lookup service "
Claude Code-credentials" 2>/dev/null \
+               | jq -r '.claudeAiOauth.acce
ssToken // empty' 2>/dev/null || true)"
+        fi
+        # macos: keychain
+        if [[ -z "$t" ]] && command -v secu
rity >/dev/null 2>&1; then
+          t="$(security find-generic-passwo
rd -s "Claude Code-credentials" -w 2>/dev/nu
ll \
+               | jq -r '.claudeAiOauth.acce
ssToken // empty' 2>/dev/null || true)"
+        fi
+        printf '%s' "$t"
+      }
+
+      fetch() {
+        local tok; tok="$(get_token)"
+        [[ -z "$tok" ]] && return 1
+        curl -sS --max-time 10 "$endpoint" 
\
+          -H "Authorization: Bearer $tok" \
+          -H "anthropic-beta: oauth-2025-04
-20"
+      }
+
+      refresh() { # $1 = -n for non-blockin
g bg refresh
+        mkdir -p "$cache_dir"
+        exec 9>"$lock"
+        if [ "''${1:-}" = "-n" ]; then floc
k -n 9 || return 0; else flock 9 || return 0
; fi
+        local tmp; tmp="$(mktemp "$cache.XX
XXXX")"
+        # only replace cache if we got a sa
ne response (has five_hour)
+        if fetch >"$tmp" 2>/dev/null && jq 
-e '.five_hour' "$tmp" >/dev/null 2>&1; then
+          mv "$tmp" "$cache"
+        else
+          rm -f "$tmp"
+          return 1
+        fi
+      }
+
+      case "''${1:-}" in
+        refresh) refresh || true; cat "$cac
he" 2>/dev/null || echo '{"error":"fetch fai
led"}' ;;
+        __bg)    refresh -n || true ;;
+        *)
+          if [[ -f "$cache" ]]; then
+            cat "$cache"
+            age=$(( $(date +%s) - $(stat -c
 %Y "$cache") ))
+            if (( age > ttl )); then
+              setsid -f "$0" __bg >/dev/nul
l 2>&1 || nohup "$0" __bg >/dev/null 2>&1 &
+            fi
+          else
+            refresh || true
+            cat "$cache" 2>/dev/null || ech
o '{"error":"fetch failed"}'
+          fi
+          ;;
+      esac
+    '';
+  };
+
+  # claude status line formatter. model/dir
/git + usage %
+  claude-statusline = pkgs.writeShellApplic
ation {
+    name = "claude-statusline";
+    runtimeInputs =
+      [ookusage]
+      ++ attrValues {
+        inherit (pkgs) jq git coreutils gaw
k;
+      };
+    text = ''
+      # claude-statusline - model/dir/git +
 claude subscription usage % (5h + weekly)
+      # pulls live usage from `ookusage` (o
auth usage endpoint, matches the website)
+      set -euo pipefail
+
+      input="$(cat)"
+      usage="$(ookusage 2>/dev/null || echo
 '{}')"
+
+      model="$(jq -r '.model.display_name /
/ .model.id // "claude"' <<<"$input")"
+      dir="$(jq -r '.workspace.current_dir 
// .cwd // empty' <<<"$input")"
+      [[ -z "$dir" ]] && dir="$PWD"
+      branch="$(git -C "$dir" branch --show
-current 2>/dev/null || true)"
+      short="''${dir/#$HOME/\~}"
+
+      # ansi
+      d=$'\e[2m'; x=$'\e[0m'; bold=$'\e[1m'
; blue=$'\e[34m'; grn=$'\e[32m'
+      sep="  ''${d}│''${x}  "
+      pctcol() { awk -v p="$1" 'BEGIN{p+=0;
 if(p>=90)printf"\033[31m";else if(p>=70)pri
ntf"\033[33m";else printf"\033[32m"}'; }
+      fmt_reset() { # iso -> local HH:MM, o
r "Day HH:MM" if >24h away
+        local iso="$1"; [[ -z "$iso" || "$i
so" == "null" ]] && return
+        local e now; e="$(date -d "$iso" +%
s 2>/dev/null)" || return; now="$(date +%s)"
+        if (( e - now < 86400 )); then date
 -d "$iso" +%H:%M; else date -d "$iso" +'%a 
%H:%M'; fi
+      }
+      seg() { # label util reset_iso -> "la
bel NN% ?HH:MM" colored, or nothing if util 
empty
+        local label="$1" util="$2" iso="$3"
+        [[ -z "$util" || "$util" == "null" 
]] && return
+        local rs; rs="$(fmt_reset "$iso")"
+        printf '%s %s%.0f%%%s' "$label" "$(
pctcol "$util")" "$util" "$x"
+        [[ -n "$rs" ]] && printf ' %s?%s%s'
 "$d" "$rs" "$x"
+      }
+
+      # newline-delimited (mapfile) so empt
y fields like a null opus slot are preserved
+      mapfile -t U < <(jq -r '
+        (.five_hour.utilization      // "" 
| tostring),
+        (.five_hour.resets_at        // "")
,
+        (.seven_day.utilization      // "" 
| tostring),
+        (.seven_day.resets_at        // "")
,
+        (.seven_day_opus.utilization // "" 
| tostring),
+        (.seven_day_opus.resets_at   // "")
+      ' <<<"$usage")
+      fh="''${U[0]:-}"; fh_r="''${U[1]:-}";
 sd="''${U[2]:-}"; sd_r="''${U[3]:-}"; op="'
'${U[4]:-}"; op_r="''${U[5]:-}"
+
+      # left: context
+      printf '%s%s%s  %s%s%s' "$bold" "$mod
el" "$x" "$blue" "$short" "$x"
+      [[ -n "$branch" ]] && printf '  %s %s
%s' "$grn" "$branch" "$x"
+
+      # right: usage. weekly opus only show
n when the plan reports it (non-null)
+      if [[ -z "$fh$sd" ]]; then
+        printf '%s%susage n/a%s' "$sep" "$d
" "$x"
+      else
+        printf '%s%s' "$sep" "$(seg 5h "$fh
" "$fh_r")"
+        printf '%s%s' "$sep" "$(seg wk "$sd
" "$sd_r")"
+        [[ -n "$op" && "$op" != "null" ]] &
& printf '%s%s' "$sep" "$(seg opus "$op" "$o
p_r")"
+      fi
+      printf '\n'
+    '';
+  };
+in {
+  config = mkIf cfg.enable {
+    home.packages = [ookusage claude-status
line];
+  };
+}
 

diff --git a/modules/home/console/tools/default.nix b/modules/home/console/tools/d
efault.nix
index 6bdac39..de143e6 100644
--- a/modules/home/console/tools/default.nix
+++ b/modules/home/console/tools/default.nix
@@ -1,6 +1,7 @@
 {
   imports = [
     ./codex.nix
+    ./claude.nix
     ./bat.nix
     ./vale.nix
     ./networking.nix

diff --git a/modules/home/console/tools/defa
ult.nix b/modules/home/console/tools/default
.nix
index 6bdac39..de143e6 100644
--- a/modules/home/console/tools/default.nix
+++ b/modules/home/console/tools/default.nix
@@ -1,6 +1,7 @@
 {
   imports = [
     ./codex.nix
+    ./claude.nix
     ./bat.nix
     ./vale.nix
     ./networking.nix
 

diff --git a/modules/home/nixos/ai/claude-desktop.nix b/modules/home/nixos/ai/clau
de-desktop.nix
new file mode 100644
index 0000000..4ed4b78
--- /dev/null
+++ b/modules/home/nixos/ai/claude-desktop.nix
@@ -0,0 +1,17 @@
+{
+  lib,
+  osConfig,
+  self',
+  ...
+}: let
+  inherit (lib) elem mkIf;
+  inherit (builtins) attrValues;
+  inherit (osConfig.ooknet.workstation) profiles;
+in {
+  # fhs variant is upstreams recommended default (MCP servers)
+  config = mkIf (elem "ai" profiles) {
+    home.packages = attrValues {
+      inherit (self'.packages) claude-desktop-fhs;
+    };
+  };
+}

diff --git a/modules/home/nixos/ai/claude-de
sktop.nix b/modules/home/nixos/ai/claude-des
ktop.nix
new file mode 100644
index 0000000..4ed4b78
--- /dev/null
+++ b/modules/home/nixos/ai/claude-desktop.n
ix
@@ -0,0 +1,17 @@
+{
+  lib,
+  osConfig,
+  self',
+  ...
+}: let
+  inherit (lib) elem mkIf;
+  inherit (builtins) attrValues;
+  inherit (osConfig.ooknet.workstation) pro
files;
+in {
+  # fhs variant is upstreams recommended de
fault (MCP servers)
+  config = mkIf (elem "ai" profiles) {
+    home.packages = attrValues {
+      inherit (self'.packages) claude-deskt
op-fhs;
+    };
+  };
+}
 

diff --git a/modules/home/nixos/ai/default.nix b/modules/home/nixos/ai/default.nix
new file mode 100644
index 0000000..87bb4db
--- /dev/null
+++ b/modules/home/nixos/ai/default.nix
@@ -0,0 +1,5 @@
+{
+  imports = [
+    ./claude-desktop.nix
+  ];
+}

diff --git a/modules/home/nixos/ai/default.n
ix b/modules/home/nixos/ai/default.nix
new file mode 100644
index 0000000..87bb4db
--- /dev/null
+++ b/modules/home/nixos/ai/default.nix
@@ -0,0 +1,5 @@
+{
+  imports = [
+    ./claude-desktop.nix
+  ];
+}
 

diff --git a/modules/home/nixos/default.nix b/modules/home/nixos/default.nix
index ff46539..e15088b 100644
--- a/modules/home/nixos/default.nix
+++ b/modules/home/nixos/default.nix
@@ -8,6 +8,7 @@
     ./creative
     ./gaming
     ./work
+    ./ai
     ./userDirs.nix
     ./mimeApps.nix
   ];

diff --git a/modules/home/nixos/default.nix 
b/modules/home/nixos/default.nix
index ff46539..e15088b 100644
--- a/modules/home/nixos/default.nix
+++ b/modules/home/nixos/default.nix
@@ -8,6 +8,7 @@
     ./creative
     ./gaming
     ./work
+    ./ai
     ./userDirs.nix
     ./mimeApps.nix
   ];
 

diff --git a/modules/options/console/default.nix b/modules/options/console/default
.nix
index 4b811c0..df05e18 100644
--- a/modules/options/console/default.nix
+++ b/modules/options/console/default.nix
@@ -22,6 +22,7 @@ in {
     };
     tools = {
       codex.enable = mkEnableOption "";
+      claude.enable = mkEnableOption "";
       bat.enable = mkEnableOption "";
       btop.enable = mkEnableOption "";
       direnv.enable = mkEnableOption "";

diff --git a/modules/options/console/default
.nix b/modules/options/console/default.nix
index 4b811c0..df05e18 100644
--- a/modules/options/console/default.nix
+++ b/modules/options/console/default.nix
@@ -22,6 +22,7 @@ in {
     };
     tools = {
       codex.enable = mkEnableOption "";
+      claude.enable = mkEnableOption "";
       bat.enable = mkEnableOption "";
       btop.enable = mkEnableOption "";
       direnv.enable = mkEnableOption "";
 

diff --git a/outputs/pkgs/claude-desktop/default.nix b/outputs/pkgs/claude-desktop
/default.nix
new file mode 100644
index 0000000..3d71035
--- /dev/null
+++ b/outputs/pkgs/claude-desktop/default.nix
@@ -0,0 +1,312 @@
+# 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-3df4
fd263723119bc45f0af2d784afd5055e2ba9.exe";
+      hash = "sha256-Fym+uhmkDkkA8+32jk3B7BxFJ8TbM1sTOADJlkUMl6g=";
+    };
+    aarch64-linux = fetchurl {
+      url = "https://downloads.claude.ai/releases/win32/arm64/1.12603.1/Claude-3d
f4fd263723119bc45f0af2d784afd5055e2ba9.exe";
+      hash = "sha256-jthQmwDvYBV89iR9xdrUccCQOTaEEgyfjxFoVIZEcKw=";
+    };
+  };
+
+  src = srcs.${stdenvNoCC.hostPlatform.system} or (throw "Unsupported system: ${s
tdenvNoCC.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.sp
lit(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/ele
ctron-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>/de
v/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/applica
tions/
+
+            # 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-wrap
per" \
+              --replace-fail "RESOURCES_PLACEHOLDER" "$electron_tree/resources" \
+              --replace-fail "LAUNCHER_LIB_PLACEHOLDER" "$out/lib/claude-desktop/
launcher-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";
+    };
+  }

diff --git a/outputs/pkgs/claude-desktop/def
ault.nix b/outputs/pkgs/claude-desktop/defau
lt.nix
new file mode 100644
index 0000000..3d71035
--- /dev/null
+++ b/outputs/pkgs/claude-desktop/default.ni
x
@@ -0,0 +1,312 @@
+# adapted from github:aaddrick/claude-deskt
op-debian (nix/claude-desktop.nix)
+# build scripts are pulled via fetchFromGit
Hub rather than vendoring the repo
+# to bump: update repoSrc rev/hash + versio
n + 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 + scri
pts/ (the actual packaging logic)
+  repoSrc = fetchFromGitHub {
+    owner = "aaddrick";
+    repo = "claude-desktop-debian";
+    rev = "d2ce0466315033938ea6c2066d424239
feffabf0"; # v2.0.20+claude1.12603.1
+    hash = "sha256-vMMjROGD/huIbrvTV3okyMPs
eleQ3dsFOabJo9TtfoI=";
+  };
+
+  srcs = {
+    x86_64-linux = fetchurl {
+      url = "https://downloads.claude.ai/re
leases/win32/x64/1.12603.1/Claude-3df4fd2637
23119bc45f0af2d784afd5055e2ba9.exe";
+      hash = "sha256-Fym+uhmkDkkA8+32jk3B7B
xFJ8TbM1sTOADJlkUMl6g=";
+    };
+    aarch64-linux = fetchurl {
+      url = "https://downloads.claude.ai/re
leases/win32/arm64/1.12603.1/Claude-3df4fd26
3723119bc45f0af2d784afd5055e2ba9.exe";
+      hash = "sha256-jthQmwDvYBV89iR9xdrUcc
CQOTaEEgyfjxFoVIZEcKw=";
+    };
+  };
+
+  src = srcs.${stdenvNoCC.hostPlatform.syst
em} or (throw "Unsupported system: ${stdenvN
oCC.hostPlatform.system}");
+
+  # The unwrapped electron derivation - con
tains the real ELF binary
+  # and Chromium resources (.pak files, loc
ales/, etc.).
+  electronUnwrapped = electron.passthru.unw
rapped or electron;
+  electronDir = "${electronUnwrapped}/libex
ec/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 - u
se 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 minifie
d --add-dir dispatch snippet
+      # appears exactly once and patches on
ly 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 && allMa
tches.length > 1' 'false' \
+        --replace-fail 'code = code.replace
(match[0], filtered);' 'code = code.split(ma
tch[0]).join(filtered);'
+
+      # Copy exe to a writable location for
 build.sh
+      cp $src Claude-Setup.exe
+
+      # Run build.sh in nix mode - it handl
es extraction, patching, icon
+      # extraction, and asar repacking. --s
ource-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/nod
e_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 proc
ess.resourcesPath from /proc/self/exe,
+            # so it always points to electr
on-unwrapped's resources/ dir - which
+            # doesn't contain the app's loc
ale JSONs, tray icons, etc.  When
+            # ELECTRON_FORCE_IS_PACKAGED=tr
ue, 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 E
LF binary into our own tree so that
+            # /proc/self/exe resolves here,
 then merge both Electron's and the
+            # app's resources into resource
s/.  Everything else (shared libs,
+            # .pak files, locales/) is syml
inked to avoid duplication.
+            #==============================
============================================
+            electron_tree=$out/lib/claude-d
esktop/electron
+
+            mkdir -p $electron_tree/resourc
es
+
+            # Copy the ELF binary - MUST be
 a real copy (not symlink) so that
+            # /proc/self/exe resolves to ou
r tree
+            cp ${electronDir}/electron $ele
ctron_tree/electron
+            chmod +x $electron_tree/electro
n
+
+            # Symlink everything else from 
electron-unwrapped
+            for item in ${electronDir}/*; d
o
+              name=$(basename "$item")
+              [[ "$name" = "electron" ]] &&
 continue
+              [[ "$name" = "resources" ]] &
& continue
+              ln -s "$item" "$electron_tree
/$name"
+            done
+
+            # Populate resources/ - start w
ith Electron's own (default_app.asar)
+            for item in ${electronDir}/reso
urces/*; 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.as
ar.unpacked $electron_tree/resources/
+
+            # Install tray icons into resou
rces
+            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 reso
urces
+            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 (smo
l-bin, plugin shim)
+            for cowork_res in build/electro
n-app/nix-resources/smol-bin.*.vhdx \
+                              build/electro
n-app/nix-resources/cowork-plugin-shim.sh; d
o
+              if [[ -f "$cowork_res" ]]; th
en
+                cp "$cowork_res" $electron_
tree/resources/
+                echo "Installed cowork reso
urce: $(basename "$cowork_res")"
+              fi
+            done
+
+            # Install ion-dist static asset
s (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 resour
ce: ion-dist"
+            fi
+
+            # Install locale JSON files int
o resources
+            for locale_json in build/claude
-extract/lib/net45/resources/*-*.json; do
+              [[ -f "$locale_json" ]] \
+                && cp "$locale_json" $elect
ron_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 wrap
per, then append our own exec.
+            head -n -1 ${electron}/bin/elec
tron > $electron_tree/electron-wrapper
+            echo "exec \"$electron_tree/ele
ctron\" \"\$@\"" >> $electron_tree/electron-
wrapper
+            chmod +x $electron_tree/electro
n-wrapper
+
+            # Update CHROME_DEVEL_SANDBOX t
o point to our tree's chrome-sandbox
+            substituteInPlace $electron_tre
e/electron-wrapper \
+              --replace-quiet "${electron}/
libexec/electron/chrome-sandbox" \
+                "$electron_tree/chrome-sand
box"
+
+            #==============================
============================================
+            # Standard install (icons, desk
top file, launcher)
+            #==============================
============================================
+
+            # Convenience symlink for resou
rces 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/hic
olor/"$size"x"$size"/apps
+              mkdir -p "$icon_dir"
+              icon=$(find build/ -name "cla
ude_*''${size}x''${size}x32.png" 2>/dev/null
 | head -1)
+              if [[ -n "$icon" ]]; then
+                install -Dm644 "$icon" "$ic
on_dir/claude-desktop.png"
+              fi
+            done
+
+            # Install shared launcher libra
ry + doctor (launcher-common.sh
+            # sources doctor.sh at runtime,
 so both must live in the same dir)
+            install -Dm755 ${repoSrc}/scrip
ts/launcher-common.sh \
+              $out/lib/claude-desktop/launc
her-common.sh
+            install -Dm755 ${repoSrc}/scrip
ts/doctor.sh \
+              $out/lib/claude-desktop/docto
r.sh
+
+            # Install .desktop file
+            mkdir -p $out/share/application
s
+            install -Dm644 ${desktopItem}/s
hare/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.a
sar"
+
+      source "LAUNCHER_LIB_PLACEHOLDER"
+
+      # Handle --doctor flag before anythin
g else
+      if [[ "''${1:-}" == '--doctor' ]]; th
en
+      	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 Launc
her Start (NixOS) ---'
+      log_message "Timestamp: $(date)"
+      log_message "Arguments: $@"
+      log_session_env
+
+      # Check for display
+      if ! check_display; then
+      	log_message 'No display detected (TT
Y 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 CLA
UDE_USE_WAYLAND)
+      detect_display_backend
+
+      # Build Electron arguments
+      build_electron_args 'nix'
+
+      # Intentionally NOT appended: app.asa
r 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 handl
er, producing a spurious
+      # "Attach app.asar?" prompt on launch
 and on every taskbar reopen
+      # (the second-instance argv path). Om
itting it is the root-cause fix.
+      # See issue #696.
+      log_message "App (auto-loaded by Elec
tron): $app_path"
+
+      # Execute Electron and keep the launc
her alive so explicit quit can
+      # clean up Desktop-owned helpers that
 outlive the Electron main process.
+      log_message "Executing: $electron_exe
c ''${electron_args[*]} $*"
+      run_electron_and_cleanup "$electron_e
xec" "''${electron_args[@]}" "$@"
+      exit $?
+      LAUNCHER
+            # Substitute placeholders - ele
ctron_exec points to our custom
+            # wrapper (which sets GTK/GIO e
nv then execs our merged binary)
+            substituteInPlace $out/bin/clau
de-desktop \
+              --replace-fail "ELECTRON_PLAC
EHOLDER" "$electron_tree/electron-wrapper" \
+              --replace-fail "RESOURCES_PLA
CEHOLDER" "$electron_tree/resources" \
+              --replace-fail "LAUNCHER_LIB_
PLACEHOLDER" "$out/lib/claude-desktop/launch
er-common.sh"
+            chmod +x $out/bin/claude-deskto
p
+
+            runHook postInstall
+    '';
+
+    meta = with lib; {
+      description = "Claude Desktop for Lin
ux";
+      homepage = "https://github.com/aaddri
ck/claude-desktop-debian";
+      license = licenses.unfree;
+      platforms = ["x86_64-linux" "aarch64-
linux"];
+      sourceProvenance = with sourceTypes; 
[binaryNativeCode];
+      mainProgram = "claude-desktop";
+    };
+  }
 

diff --git a/outputs/pkgs/claude-desktop/fhs.nix b/outputs/pkgs/claude-desktop/fhs
.nix
new file mode 100644
index 0000000..6072a0f
--- /dev/null
+++ b/outputs/pkgs/claude-desktop/fhs.nix
@@ -0,0 +1,41 @@
+{
+  buildFHSEnv,
+  bubblewrap,
+  claude-desktop,
+  nodejs,
+  docker,
+  docker-compose,
+  openssl,
+  glibc,
+  uv,
+}:
+buildFHSEnv {
+  name = "claude-desktop";
+
+  targetPkgs = pkgs: [
+    bubblewrap
+    claude-desktop
+    docker
+    docker-compose
+    glibc
+    nodejs
+    openssl
+    uv
+  ];
+
+  runScript = "${claude-desktop}/bin/claude-desktop";
+
+  extraInstallCommands = ''
+    mkdir -p $out/share/applications
+    cp ${claude-desktop}/share/applications/* $out/share/applications/
+
+    mkdir -p $out/share/icons
+    cp -r ${claude-desktop}/share/icons/* $out/share/icons/
+  '';
+
+  meta =
+    claude-desktop.meta
+    // {
+      description = "Claude Desktop for Linux (FHS environment for MCP servers)";
+    };
+}

diff --git a/outputs/pkgs/claude-desktop/fhs
.nix b/outputs/pkgs/claude-desktop/fhs.nix
new file mode 100644
index 0000000..6072a0f
--- /dev/null
+++ b/outputs/pkgs/claude-desktop/fhs.nix
@@ -0,0 +1,41 @@
+{
+  buildFHSEnv,
+  bubblewrap,
+  claude-desktop,
+  nodejs,
+  docker,
+  docker-compose,
+  openssl,
+  glibc,
+  uv,
+}:
+buildFHSEnv {
+  name = "claude-desktop";
+
+  targetPkgs = pkgs: [
+    bubblewrap
+    claude-desktop
+    docker
+    docker-compose
+    glibc
+    nodejs
+    openssl
+    uv
+  ];
+
+  runScript = "${claude-desktop}/bin/claude
-desktop";
+
+  extraInstallCommands = ''
+    mkdir -p $out/share/applications
+    cp ${claude-desktop}/share/applications
/* $out/share/applications/
+
+    mkdir -p $out/share/icons
+    cp -r ${claude-desktop}/share/icons/* $
out/share/icons/
+  '';
+
+  meta =
+    claude-desktop.meta
+    // {
+      description = "Claude Desktop for Lin
ux (FHS environment for MCP servers)";
+    };
+}
 

diff --git a/outputs/pkgs/claude-desktop/node-pty.nix b/outputs/pkgs/claude-deskto
p/node-pty.nix
new file mode 100644
index 0000000..4c01412
--- /dev/null
+++ b/outputs/pkgs/claude-desktop/node-pty.nix
@@ -0,0 +1,51 @@
+{
+  lib,
+  buildNpmPackage,
+  fetchFromGitHub,
+  python3,
+  node-gyp,
+}:
+buildNpmPackage rec {
+  pname = "node-pty";
+  version = "1.1.0";
+
+  src = fetchFromGitHub {
+    owner = "microsoft";
+    repo = "node-pty";
+    rev = "v${version}";
+    hash = "sha256-R0QxTw3tNJvW4aEi+GOF0iZhGgI42HTYJih90CdF18I=";
+  };
+
+  npmDepsHash = "sha256-HRv/4NO7CHkPs7ld8lx61n2cty0EhmWVrpH/1Vqh+Nk=";
+
+  # node-gyp needs python3 for native compilation
+  nativeBuildInputs = [python3 node-gyp];
+
+  # chokidar (dev dep) has an optional dep on fsevents (macOS-only).
+  # The Nix npm deps fetcher excludes it, so npm ci sees a lock/json
+  # mismatch. Strip the reference from the lock file to fix the sync.
+  postPatch = ''
+    sed -i '/"fsevents"/d' package-lock.json
+  '';
+
+  # Default npmBuildHook only runs "npm run build" (tsc), but we also
+  # need the native addon. Run both explicitly.
+  buildPhase = ''
+    runHook preBuild
+    npm run build
+    node-gyp rebuild
+    runHook postBuild
+  '';
+
+  # npmInstallHook doesn't copy the native addon - do it ourselves
+  postInstall = ''
+    cp -r build $out/lib/node_modules/node-pty/
+  '';
+
+  meta = with lib; {
+    description = "Fork pseudoterminals in Node.JS";
+    homepage = "https://github.com/microsoft/node-pty";
+    license = licenses.mit;
+    platforms = platforms.linux;
+  };
+}

diff --git a/outputs/pkgs/claude-desktop/nod
e-pty.nix b/outputs/pkgs/claude-desktop/node
-pty.nix
new file mode 100644
index 0000000..4c01412
--- /dev/null
+++ b/outputs/pkgs/claude-desktop/node-pty.n
ix
@@ -0,0 +1,51 @@
+{
+  lib,
+  buildNpmPackage,
+  fetchFromGitHub,
+  python3,
+  node-gyp,
+}:
+buildNpmPackage rec {
+  pname = "node-pty";
+  version = "1.1.0";
+
+  src = fetchFromGitHub {
+    owner = "microsoft";
+    repo = "node-pty";
+    rev = "v${version}";
+    hash = "sha256-R0QxTw3tNJvW4aEi+GOF0iZh
GgI42HTYJih90CdF18I=";
+  };
+
+  npmDepsHash = "sha256-HRv/4NO7CHkPs7ld8lx
61n2cty0EhmWVrpH/1Vqh+Nk=";
+
+  # node-gyp needs python3 for native compi
lation
+  nativeBuildInputs = [python3 node-gyp];
+
+  # chokidar (dev dep) has an optional dep 
on fsevents (macOS-only).
+  # The Nix npm deps fetcher excludes it, s
o npm ci sees a lock/json
+  # mismatch. Strip the reference from the 
lock file to fix the sync.
+  postPatch = ''
+    sed -i '/"fsevents"/d' package-lock.jso
n
+  '';
+
+  # Default npmBuildHook only runs "npm run
 build" (tsc), but we also
+  # need the native addon. Run both explici
tly.
+  buildPhase = ''
+    runHook preBuild
+    npm run build
+    node-gyp rebuild
+    runHook postBuild
+  '';
+
+  # npmInstallHook doesn't copy the native 
addon - do it ourselves
+  postInstall = ''
+    cp -r build $out/lib/node_modules/node-
pty/
+  '';
+
+  meta = with lib; {
+    description = "Fork pseudoterminals in 
Node.JS";
+    homepage = "https://github.com/microsof
t/node-pty";
+    license = licenses.mit;
+    platforms = platforms.linux;
+  };
+}
 

diff --git a/outputs/pkgs/default.nix b/outputs/pkgs/default.nix
index 76b919e..7148a8e 100644
--- a/outputs/pkgs/default.nix
+++ b/outputs/pkgs/default.nix
@@ -7,6 +7,18 @@
   perSystem = {pkgs, ...}: let
     inherit (pkgs) callPackage qt6Packages writeTextFile;
 
+    # claude-desktop is unfree, our package set isnt; scope an unfree
+    # nixpkgs to just it rather than flipping allowUnfree globally
+    pkgsClaude = import inputs.nixpkgs {
+      inherit (pkgs.stdenv.hostPlatform) system;
+      config.allowUnfreePredicate = p: lib.getName p == "claude-desktop";
+    };
+    claudeDesktop = rec {
+      node-pty = pkgsClaude.callPackage ./claude-desktop/node-pty.nix {};
+      package = pkgsClaude.callPackage ./claude-desktop {inherit node-pty;};
+      fhs = pkgsClaude.callPackage ./claude-desktop/fhs.nix {claude-desktop = pac
kage;};
+    };
+
     projectPlus = {
       fpp-config = callPackage ./project-plus/fpp-config.nix {};
       fpp-launcher = callPackage ./project-plus/fpp-launcher.nix {};
@@ -41,6 +53,9 @@
 
       claude-code = callPackage ./claude-code {};
 
+      claude-desktop = claudeDesktop.package;
+      claude-desktop-fhs = claudeDesktop.fhs;
+
       # disabled temporarily - regex-syntax compile failure on current nixpkgs
       # spotify-player = pkgs.spotify-player.override {
       #   withImage = false;

diff --git a/outputs/pkgs/default.nix b/outp
uts/pkgs/default.nix
index 76b919e..7148a8e 100644
--- a/outputs/pkgs/default.nix
+++ b/outputs/pkgs/default.nix
@@ -7,6 +7,18 @@
   perSystem = {pkgs, ...}: let
     inherit (pkgs) callPackage qt6Packages 
writeTextFile;
 
+    # claude-desktop is unfree, our package
 set isnt; scope an unfree
+    # nixpkgs to just it rather than flippi
ng allowUnfree globally
+    pkgsClaude = import inputs.nixpkgs {
+      inherit (pkgs.stdenv.hostPlatform) sy
stem;
+      config.allowUnfreePredicate = p: lib.
getName p == "claude-desktop";
+    };
+    claudeDesktop = rec {
+      node-pty = pkgsClaude.callPackage ./c
laude-desktop/node-pty.nix {};
+      package = pkgsClaude.callPackage ./cl
aude-desktop {inherit node-pty;};
+      fhs = pkgsClaude.callPackage ./claude
-desktop/fhs.nix {claude-desktop = package;}
;
+    };
+
     projectPlus = {
       fpp-config = callPackage ./project-pl
us/fpp-config.nix {};
       fpp-launcher = callPackage ./project-
plus/fpp-launcher.nix {};
@@ -41,6 +53,9 @@
 
       claude-code = callPackage ./claude-co
de {};
 
+      claude-desktop = claudeDesktop.packag
e;
+      claude-desktop-fhs = claudeDesktop.fh
s;
+
       # disabled temporarily - regex-syntax
 compile failure on current nixpkgs
       # spotify-player = pkgs.spotify-playe
r.override {
       #   withImage = false;
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET