dot

vizie's NixOS desktop: Hyprland, CachyOS kernel and gaming tweaks

dd2e363

Claude Desktop: Claude Code from the Nix store; drop nix-ld

By Gustaf Granström

The Code tab ran a claude binary the app downloads into ~/.config/Claude,
which needed nix-ld to start. Claude Code is now its own package at the
version the app pins (pkgs/claude-desktop/claude-code.nix, the same file and
checksum the app would download, patched to run on NixOS), and the app is
patched to use it through its local-binary override, which release builds
compile out. uv comes from nixpkgs too, so the app doesn't download one.

nix-ld is off again: programs built for other Linux distributions don't run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

3 files changed +95 −5

@@ -34,11 +34,6 @@
3434 "L+ /usr/share/OVMF/OVMF_CODE_4M.fd - - - - ${pkgs.OVMF.fd}/FV/OVMF_CODE.fd"
3535 ];
3636
37- # The Code tab runs a claude binary the app downloads itself (into
38- # ~/.config/Claude/claude-code), built for generic Linux: it wants the loader
39- # at /lib64/ld-linux-x86-64.so.2, which nix-ld provides.
40- programs.nix-ld.enable = true;
41-
4237 # --- OpenDeck -------------------------------------------------------------------
4338
4439 # Lets your user open the Stream Deck (40-streamdeck.rules on CachyOS).
@@ -1,0 +1,59 @@
1+# Claude Code at the exact version Claude Desktop is built against: the app's
2+# Code tab runs it. Unpatched, the app downloads this same file at run time
3+# into ~/.config/Claude/claude-code, where NixOS can't run it; default.nix
4+# points the app here instead.
5+#
6+# To update (after changing the app's version): the app's pin is in its
7+# app.asar, in the JSON with "baseUrl":"https://downloads.claude.ai/claude-code-releases".
8+# Take "version" and platforms."linux-x64".checksum from it:
9+# asar extract .../resources/app.asar /tmp/app
10+# grep -o '{"version":"[0-9.]*","manifest".\{0,1500\}' /tmp/app/.vite/build/*.js
11+# The app's build fails with a message if the two versions disagree.
12+{
13+ lib,
14+ stdenv,
15+ fetchurl,
16+ zstd,
17+ autoPatchelfHook,
18+}:
19+
20+stdenv.mkDerivation (finalAttrs: {
21+ pname = "claude-code-for-desktop";
22+ version = "2.1.299";
23+
24+ src = fetchurl {
25+ url = "https://downloads.claude.ai/claude-code-releases/${finalAttrs.version}/linux-x64/claude.zst";
26+ sha256 = "20ca25213130255bacdcae3ed5df3fc99e1d8e86cdf46e9188b7ff9c1b478fa4";
27+ };
28+
29+ nativeBuildInputs = [
30+ zstd
31+ autoPatchelfHook
32+ ];
33+
34+ dontUnpack = true;
35+ # The program's JavaScript is appended to the executable; strip would cut it.
36+ dontStrip = true;
37+
38+ installPhase = ''
39+ runHook preInstall
40+ install -d $out/bin
41+ zstd -d $src -o $out/bin/claude
42+ chmod 755 $out/bin/claude
43+ runHook postInstall
44+ '';
45+
46+ doInstallCheck = true;
47+ installCheckPhase = ''
48+ HOME=$TMPDIR $out/bin/claude --version | grep -F ${finalAttrs.version}
49+ '';
50+
51+ meta = {
52+ description = "Claude Code, the version Claude Desktop's Code tab runs";
53+ homepage = "https://claude.com/claude-code";
54+ license = lib.licenses.unfree;
55+ mainProgram = "claude";
56+ platforms = [ "x86_64-linux" ];
57+ sourceProvenance = [ lib.sourceTypes.binaryNativeCode ];
58+ };
59+})
@@ -7,11 +7,18 @@
77 #
88 # Cowork also needs QEMU/KVM, the vhost_vsock module and OVMF firmware at the
99 # Debian path it looks for; modules/nixos/apps.nix provides those.
10+#
11+# The Code tab runs Claude Code from the Nix store (claude-code.nix) rather than
12+# a copy the app downloads, and Python tools use nixpkgs' uv rather than one it
13+# downloads. After a version change, update claude-code.nix too: the build says
14+# so if the app pins a different Claude Code.
1015 {
1116 lib,
1217 stdenv,
18+ callPackage,
1319 fetchurl,
1420 dpkg,
21+ asar,
1522 autoPatchelfHook,
1623 makeWrapper,
1724 wrapGAppsHook3,
@@ -55,8 +62,12 @@
5562 socat,
5663 bubblewrap,
5764 openssh,
65+ uv,
5866 }:
5967
68+let
69+ claude-code = callPackage ./claude-code.nix { };
70+in
6071 stdenv.mkDerivation (finalAttrs: {
6172 pname = "claude-desktop";
6273 version = "2.42304.0";
@@ -68,6 +79,7 @@
6879
6980 nativeBuildInputs = [
7081 dpkg
82+ asar
7183 autoPatchelfHook
7284 makeWrapper
7385 wrapGAppsHook3
@@ -141,6 +153,27 @@
141153 # the user-namespace sandbox instead, which needs no helper.
142154 rm $out/lib/claude-desktop/chrome-sandbox
143155
156+ # The app has an override for running a Claude Code that's already on disk
157+ # instead of downloading one; release builds compile out the part that
158+ # reads it. This puts it back, pointing at the store path. The app then
159+ # also leaves Claude Code updates alone: they come with this package.
160+ res=$out/lib/claude-desktop/resources
161+ asar extract $res/app.asar app
162+ pin=$(grep -l '"baseUrl":"https://downloads.claude.ai/claude-code-releases"' app/.vite/build/*.js)
163+ if ! grep -qF '{"version":"${claude-code.version}","manifest"' $pin; then
164+ echo "claude-desktop: the app pins another Claude Code than ${claude-code.version}; update claude-code.nix" >&2
165+ exit 1
166+ fi
167+ hook='process.env.CLAUDE_CODE_LOCAL_BINARY}async initLocalBinary('
168+ if ! file=$(grep -lF "$hook" app/.vite/build/*.js); then
169+ echo "claude-desktop: the app's Claude Code override has changed; see the comment above" >&2
170+ exit 1
171+ fi
172+ substituteInPlace $file --replace-fail "$hook" \
173+ 'this.localBinaryInitPromise=this.initLocalBinary("${lib.getExe claude-code}")}async initLocalBinary('
174+ rm -r $res/app.asar $res/app.asar.unpacked
175+ asar pack app $res/app.asar --unpack '{*.node,github-mcp-server}'
176+
144177 runHook postInstall
145178 '';
146179
@@ -155,10 +188,13 @@
155188 socat
156189 bubblewrap
157190 openssh
191+ uv
158192 ]
159193 }
160194 '';
161195
196+ passthru = { inherit claude-code; };
197+
162198 meta = {
163199 description = "Claude desktop app (official Linux beta build)";
164200 homepage = "https://claude.ai/download";