Skip to content

Commit 67be91b

Browse files
claude-code: manifest + updateScript, bump to 2.1.156 (#304)
Bumps Claude Code to 2.1.156 and replaces the hand-maintained version/hash pin with the nixpkgs pattern: a generated `manifest.json` read via `lib.importJSON` plus a `passthru.updateScript`. ## Why The previous `default.nix` pinned the version and four per-platform SRI hashes inline behind a 14-line comment describing a manual "re-prefetch each platform binary" loop. Anthropic actually publishes a per-version `manifest.json` on the GCS bucket (every platform's checksum in one file) plus a `latest` pointer, so the four-prefetch dance was never necessary. nixpkgs already consumes that manifest; this brings our package in line. ## What changed - `packages/claude-code/manifest.json` (generated): version + per-system `{ slug, hash }`. SRI, not the hex upstream ships (repo ast-grep bans `sha256 =` hex in fetchers). - `packages/claude-code/default.nix`: reads the manifest, derives the fetch URL/hash from it, and exposes `passthru.updateScript`. The system→slug map now lives only in the updater (single owner); `default.nix` reads the slug back from the manifest. - `lib/packages.nix`: injects a pkgs-applied `writeNushellApplication` into the flake package-set context so the updater can build. The overlay context does not provide it, so `pkgs.claude-code` (consumed by the dev image) simply omits `updateScript`; the flake output `.#claude-code` carries it. - `agents-md/sections/13-dependency-intake.md` (+ regenerated `AGENTS.md`/`CLAUDE.md`): one durable note that prebuilt-binary packages own a generated manifest + updateScript and are bumped by running the updater. ## Bumping going forward ```sh nix run .#claude-code.updateScript -- <version> # omit version to track `latest` ``` It refetches the upstream manifest, converts hex→SRI, and rewrites `manifest.json`. ## Validation - `nix build .#claude-code` builds 2.1.156 (hashes match). - `nix run .#claude-code.updateScript -- 2.1.156` reproduces the committed `manifest.json` byte-for-byte. - Overlay eval: `pkgs.claude-code.version == "2.1.156"`, `? updateScript == false` (no missing-arg error). - `nix run .#lint` clean.
1 parent afad3df commit 67be91b

7 files changed

Lines changed: 168 additions & 39 deletions

File tree

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,14 @@ lock output that makes later builds pure.
606606
Generated catalogs are build inputs, not hand-edited source. If a generated file
607607
is wrong, change the manifest or generator that owns it.
608608

609+
A prebuilt-binary package pins its version and per-platform hashes in a generated
610+
`manifest.json` read with `lib.importJSON` and refreshed by a
611+
`passthru.updateScript`; bump by running the updater, never by hand-editing the
612+
hashes. When upstream signs its release manifest, the updater verifies that
613+
signature against a pinned key and fails closed before writing hashes. See
614+
[`packages/claude-code`](packages/claude-code) for the worked shape:
615+
`nix run .#claude-code.updateScript -- <version>`.
616+
609617
Keep binary and generated artifacts near the owner that can explain and refresh
610618
them. Use small manifests for curated sets, generated catalogs for URLs and
611619
hashes, and metadata catalogs for search or browsing surfaces.

CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,14 @@ lock output that makes later builds pure.
606606
Generated catalogs are build inputs, not hand-edited source. If a generated file
607607
is wrong, change the manifest or generator that owns it.
608608

609+
A prebuilt-binary package pins its version and per-platform hashes in a generated
610+
`manifest.json` read with `lib.importJSON` and refreshed by a
611+
`passthru.updateScript`; bump by running the updater, never by hand-editing the
612+
hashes. When upstream signs its release manifest, the updater verifies that
613+
signature against a pinned key and fails closed before writing hashes. See
614+
[`packages/claude-code`](packages/claude-code) for the worked shape:
615+
`nix run .#claude-code.updateScript -- <version>`.
616+
609617
Keep binary and generated artifacts near the owner that can explain and refresh
610618
them. Use small manifests for curated sets, generated catalogs for URLs and
611619
hashes, and metadata catalogs for search or browsing surfaces.

agents-md/sections/13-dependency-intake.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ lock output that makes later builds pure.
2626
Generated catalogs are build inputs, not hand-edited source. If a generated file
2727
is wrong, change the manifest or generator that owns it.
2828

29+
A prebuilt-binary package pins its version and per-platform hashes in a generated
30+
`manifest.json` read with `lib.importJSON` and refreshed by a
31+
`passthru.updateScript`; bump by running the updater, never by hand-editing the
32+
hashes. When upstream signs its release manifest, the updater verifies that
33+
signature against a pinned key and fails closed before writing hashes. See
34+
[`packages/claude-code`](packages/claude-code) for the worked shape:
35+
`nix run .#claude-code.updateScript -- <version>`.
36+
2937
Keep binary and generated artifacts near the owner that can explain and refresh
3038
them. Use small manifests for curated sets, generated catalogs for URLs and
3139
hashes, and metadata catalogs for search or browsing surfaces.

lib/packages.nix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ let
3030
ixForPackages
3131
;
3232
ix = ixForPackages;
33+
# Pre-applied to the caller's pkgs so flake-output packages can build a
34+
# `passthru.updateScript` without re-threading `ix` through callPackage.
35+
writeNushellApplication = ixForPackages.writeNushellApplication pkgs;
3336
};
3437
mergePackageTrees =
3538
left: right:

packages/claude-code/default.nix

Lines changed: 91 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,30 @@
88
ripgrep,
99
bubblewrap,
1010
socat,
11+
nix,
12+
gnupg,
1113
binName ? "claude",
14+
# Only the flake package set injects the Nushell writer; the overlay eval
15+
# context does not. The updater is a maintainer-facing flake output, so the
16+
# overlay build of `pkgs.claude-code` simply omits `passthru.updateScript`.
17+
writeNushellApplication ? null,
1218
}:
1319

1420
let
15-
# Pinned to a prerelease build on purpose. Anthropic publishes new Claude
16-
# Code versions to the npm `next` (prerelease) tag days before promoting
17-
# them to `latest` (stable), and every channel that normally surfaces an
18-
# upgrade only watches `latest`: the built-in `claude` auto-updater and
19-
# `claude doctor` both reported "up to date", and sadjow/claude-code-nix
20-
# (the usual Nix source) tracks stable too. At the Opus 4.8 launch 2.1.154
21-
# sat on `next` while everything else still showed 2.1.153, yet 2.1.154 is
22-
# the first build that defaults `/fast` to Opus 4.8. Fetching the platform
23-
# binary directly by version is the only way to pin ahead of the stable
24-
# promotion. When `latest` catches up, this can fall back to a channel that
25-
# tracks stable. Bump by re-prefetching each platform binary:
26-
# nix store prefetch-file --json \
27-
# https://downloads.claude.ai/claude-code-releases/<version>/<slug>/claude
28-
version = "2.1.154";
29-
30-
# Claude Code ships prebuilt Bun single-file executables per platform. The
31-
# download path keys off Anthropic's own platform slugs rather than the Nix
32-
# system doubles, so map between them here.
33-
platforms = {
34-
aarch64-darwin = {
35-
slug = "darwin-arm64";
36-
hash = "sha256-vJiBsQfXvhdDxkyLct1meY9dCUfbxI7Q13lkxHNmH9Q=";
37-
};
38-
x86_64-darwin = {
39-
slug = "darwin-x64";
40-
hash = "sha256-FgjZMmGHkgHc933TLcFz777qcVGH01Qv0Fr899W17E0=";
41-
};
42-
x86_64-linux = {
43-
slug = "linux-x64";
44-
hash = "sha256-Z/bKt+bBJAEPYqwY+AeLwJ4NtqW56K6HTp5zAzxFF5M=";
45-
};
46-
aarch64-linux = {
47-
slug = "linux-arm64";
48-
hash = "sha256-n3Mt4nj3rcYdKf1bBV3a8brjuybXX+bgahJWAlZXd6g=";
49-
};
50-
};
21+
# Version and per-platform SRI hashes are generated, never hand-edited. Bump
22+
# with `nix run .#claude-code.updateScript -- <version>`, which refetches
23+
# Anthropic's per-version manifest and rewrites manifest.json. We pin by raw
24+
# version (not the npm `latest` tag) because Anthropic ships new builds to the
25+
# `next` prerelease tag days before promoting them to `latest`, and every
26+
# channel that normally surfaces an upgrade (the built-in updater, `claude
27+
# doctor`, sadjow/claude-code-nix) only watches `latest`.
28+
manifest = lib.importJSON ./manifest.json;
29+
inherit (manifest) version;
5130

5231
inherit (stdenv.hostPlatform) system;
5332
target =
54-
platforms.${system}
55-
or (throw "claude-code: no prebuilt binary for ${system}; supported: ${lib.concatStringsSep ", " (builtins.attrNames platforms)}");
33+
manifest.platforms.${system}
34+
or (throw "claude-code: no prebuilt binary for ${system}; supported: ${lib.concatStringsSep ", " (builtins.attrNames manifest.platforms)}");
5635

5736
# Primary host is the Anthropic-branded CDN so the source is verifiable; the
5837
# GCS bucket is the direct origin and stays as a mirror if the CDN is down.
@@ -65,6 +44,75 @@ let
6544
];
6645
inherit (target) hash;
6746
};
47+
48+
# Refreshes manifest.json from Anthropic's published per-version manifest,
49+
# converting its hex checksums to the SRI hashes the fetcher pins. The slug
50+
# map lives here as the single owner; default.nix only reads it back. The
51+
# updater fails closed unless the manifest's detached GPG signature verifies
52+
# against the pinned release signing key (release-signing-key.asc, fingerprint
53+
# 31DD DE24 DDFA B679 F42D 7BD2 BAA9 29FF 1A7E CACE, published at
54+
# downloads.claude.ai/keys/claude-code.asc), so a spoofed manifest cannot
55+
# inject hashes for attacker-controlled binaries.
56+
updateScript =
57+
if writeNushellApplication == null then
58+
null
59+
else
60+
writeNushellApplication {
61+
name = "claude-code-update";
62+
runtimeInputs = [
63+
nix
64+
gnupg
65+
];
66+
meta.description = "Refresh packages/claude-code/manifest.json to a signed Claude Code release";
67+
text = ''
68+
const base = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"
69+
const signing_key = "${./release-signing-key.asc}"
70+
const slugs = {
71+
"aarch64-darwin": "darwin-arm64",
72+
"x86_64-darwin": "darwin-x64",
73+
"x86_64-linux": "linux-x64",
74+
"aarch64-linux": "linux-arm64"
75+
}
76+
77+
# Run from the repo root: `nix run .#claude-code.updateScript -- [version]`.
78+
# Without a version argument it tracks Anthropic's `latest` pointer.
79+
def main [version?: string] {
80+
let v = ($version | default (http get $"($base)/latest" | str trim))
81+
82+
# Download the exact bytes we verify, then parse the same file.
83+
let work = (mktemp --directory)
84+
let manifest_path = $"($work)/manifest.json"
85+
let sig_path = $"($work)/manifest.json.sig"
86+
http get --raw $"($base)/($v)/manifest.json" | save --force $manifest_path
87+
http get --raw $"($base)/($v)/manifest.json.sig" | save --force $sig_path
88+
89+
# Fail closed: only the pinned key lives in this GNUPGHOME, so a
90+
# zero exit from --verify proves Anthropic signed these exact bytes.
91+
let gnupghome = (mktemp --directory)
92+
with-env { GNUPGHOME: $gnupghome } {
93+
^gpg --batch --quiet --import $signing_key
94+
let check = (do { ^gpg --batch --verify $sig_path $manifest_path } | complete)
95+
if $check.exit_code != 0 {
96+
error make { msg: $"claude-code: manifest signature verification failed for ($v)\n($check.stderr)" }
97+
}
98+
}
99+
100+
let upstream = (open $manifest_path)
101+
let platforms = (
102+
$slugs
103+
| transpose system slug
104+
| reduce --fold {} {|row acc|
105+
let hex = ($upstream.platforms | get $row.slug | get checksum)
106+
let sri = (^nix hash convert --hash-algo sha256 --to sri $hex | str trim)
107+
$acc | insert $row.system { slug: $row.slug, hash: $sri }
108+
}
109+
)
110+
let out = "packages/claude-code/manifest.json"
111+
{ version: $v, platforms: $platforms } | to json --indent 2 | save --force $out
112+
print $"updated ($out) to ($v); signature verified"
113+
}
114+
'';
115+
};
68116
in
69117
stdenv.mkDerivation {
70118
pname = "claude-code";
@@ -111,6 +159,10 @@ stdenv.mkDerivation {
111159
runHook postInstall
112160
'';
113161

162+
passthru = lib.optionalAttrs (updateScript != null) {
163+
inherit updateScript;
164+
};
165+
114166
meta = {
115167
description = "Claude Code, Anthropic's agentic coding tool in the terminal";
116168
homepage = "https://www.anthropic.com/claude-code";
@@ -120,7 +172,7 @@ stdenv.mkDerivation {
120172
# `nix run .#claude-code`. Distribution terms are Anthropic's commercial
121173
# Claude Code license.
122174
mainProgram = binName;
123-
platforms = builtins.attrNames platforms;
175+
platforms = builtins.attrNames manifest.platforms;
124176
sourceProvenance = [ lib.sourceTypes.binaryNativeCode ];
125177
};
126178
}

packages/claude-code/manifest.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"version": "2.1.156",
3+
"platforms": {
4+
"aarch64-darwin": {
5+
"slug": "darwin-arm64",
6+
"hash": "sha256-nB6GAQMfXLsxAeSd2iK/i6MRg2kscF4memkjWF+iugk="
7+
},
8+
"x86_64-darwin": {
9+
"slug": "darwin-x64",
10+
"hash": "sha256-zNYIxpRncyTiTex9ElO1H4h6e+g4zbdbItU2LJc1EQc="
11+
},
12+
"x86_64-linux": {
13+
"slug": "linux-x64",
14+
"hash": "sha256-bYPNImRFDF5U/JiL4QMsKIz0GO5gQpSs+4/ErCj196M="
15+
},
16+
"aarch64-linux": {
17+
"slug": "linux-arm64",
18+
"hash": "sha256-ftldCpOutA4rmOI0t2DZKVtwRO9njGLbjR9eFL/VeHg="
19+
}
20+
}
21+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-----BEGIN PGP PUBLIC KEY BLOCK-----
2+
3+
mQINBGnK73ABEACnbytJXkjweYrwIr0aLEFRlH+C0nF44KxFc7gQmJ6PjSPMGZAD
4+
dxZcaixU7zZl8WxEpVO0wLmIH8cf2zGOdyuZg1Yaugk1vHb2b8WBhAGCQJdPgB8W
5+
XquedepEYtk56uP/gCoTjJDUZluEGBHnlnuujSJ4orxEdhSykEoAUfJZGEILPpMd
6+
bphFt/Sn+Eb/TxM5jpKPdwnv8AShNF/1mZU1fWTQq9tRKJUakZj04gdaDFElQXak
7+
CtTij+GT6yoYCARSHwGO+PC/Pr6q4tc+D7LRjxSBvUWDoFSmlqb/PJ1hj9D/7I2O
8+
e4XXniAPWMR56KvxHlzOzrNQdJujbJdSkCwh1ZijkSd3y8ayW5WYUTGdRab99NUw
9+
agzlabe/VVF6kzJ0Scn5q3PihB2Y9Bwo0CKnkYk7a7KT77EWv0Kkq+VHmOtqX3a2
10+
hhX+b6a6ve9rzJ1qZYGj+obv/C3Sx1LzUjAfqVy7RJDf2uAoP5t2g8u/TkSpUxhM
11+
VEjZBkSxYZhMyzQM6t8IgkUfnSrIPTHixbDWARZ4beMOBjxyPZK1nP7OOrNR3TkK
12+
JtwLMQAabURCDnL0PjS0iwBTU4jtumBD1XSULyWuoTvMljrpQr1nV1oDyOt0OLqa
13+
KA2McWtd9PdXhC8y2EIg7TmrTlJLfHYbdmkiCYj4J49Q8HWkN/6WE+RTUwARAQAB
14+
tD5BbnRocm9waWMgQ2xhdWRlIENvZGUgUmVsZWFzZSBTaWduaW5nIDxzZWN1cml0
15+
eUBhbnRocm9waWMuY29tPokCUQQTAQoAOxYhBDHd3iTd+rZ59C170rqpKf8afsrO
16+
BQJpyu9wAhsPBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJELqpKf8afsrO
17+
l5IP/2I8X1dFy5xYczWB/coIxGjuzS/V6ByZGZZEJsbr04pmuHiFUykJqPGWGQ6q
18+
U0YF5iEwvEkaagS5m7DzhSEf3FM3Cgafax/6d70tar9Vr1D+w6uPfxetu7u/WYJp
19+
aolIsdh5fTrBh9zSM1Njl8FM8wG8CwZQjS33Oa7d8cwRkgdUWbt6LXgz+cTQNuBn
20+
BgW6Ks7oZFI25dfu0ojDR+aDFJg4+4wZoyDLPvJz1SIrJ5WFGs67zsx9SfS3yZnf
21+
XKmBe+f0dUy+GJ2nFZrXFf99+c0dPEHYO8DCeAHZizjkFrdYtUHdDU0YDYEGkLJa
22+
bE+pgcpkHf5EvsZzHsyDbl95W/eh7pcXMbwkN+W4CBYUE9X4uHhqzWaC5yAVRWUA
23+
1BJ9V4LjZfHPLEJt0I3TxzXiEg9/BVeaTYq9RjaxIFo9Nfk158HqJY6SA5jslBlx
24+
Gv/No8u+xVcze2UJyGVfEIUfm92+0UAIkny3+5cuVV0ICzJxXlXj0CnLM9Lt50wE
25+
p3suVwuBEviCbZ08eAH1Ht8gbBdSsiOkIU8CX3v/scwHHx5q0+NBL6xLrQObg13a
26+
tRXBlKObfElkPN3lTUbUnJOW4U8uSjH8VRP+AujKWMDFe7x0zCs+iYY1mTOvbrTS
27+
9n3CmZUmbynZ+E/QWNENpW/pDNZdWFy43PASmML5FHu4m9Sn
28+
=oqMI
29+
-----END PGP PUBLIC KEY BLOCK-----

0 commit comments

Comments
 (0)