The npm that answered from the past
A routine freshness check said npm's latest Claude Code was nine days
and ten releases old, and our registry was somehow ahead of the source
it mirrors. The story almost shipped — a dev log post, a home-page
feature. Then one curl returned a different answer than
npm view, and the real story turned out to be about which
of them was lying.
1The claim that almost shipped
While reviewing registry freshness we asked npm what the latest Claude Code release was:
$ npm view @anthropic-ai/claude-code dist-tags
{ stable: '2.1.227', latest: '2.1.235', next: '2.1.235' }
Our own registry, claude.json, carried
2.1.247 — benchmarked, verified on both platforms. The
explanation wrote itself: Anthropic now ships through its
native-installer CDN first, npm publishing lags behind, and OVM —
which installs Claude from that CDN — sees releases npm has not been
given yet. Interesting, publishable, and worth a home-page panel so
users could watch the channels drift apart.
It had one problem. None of it was true.
2One request over a different road
Before writing it down we asked the same registry the same question without going through npm:
$ curl -s https://registry.npmjs.org/-/package/\
@anthropic-ai/claude-code/dist-tags
{ "stable": "2.1.231", "latest": "2.1.247", "next": "2.1.247" }
npm was current. 2.1.247 had been published the previous
day, the latest tag pointed at it, and every release
between it and the version the CLI reported existed too. Same
registry, same package, same machine, same minute — two answers,
nine days apart.
3Ruling things out
The configured registry was the real one, not a mirror. The
abbreviated metadata document npm requests — a smaller "corgi" variant
of the package document — was as fresh as the full one over raw HTTP.
And --prefer-online, the flag whose documented job is to
force staleness checks, returned the same nine-day-old answer.
What settled it was pointing the CLI at a brand-new, empty cache directory:
$ npm view @anthropic-ai/claude-code dist-tags \
--cache /tmp/fresh-cache
npm error code ETIMEDOUT
npm error network request to
https://registry.npmjs.org/@anthropic-ai%2fclaude-code failed
With nothing to fall back on, the truth surfaced: on this machine,
npm's network path was broken. Node could not reach the registry at
all — while curl, resolving and routing on its own,
reached it fine. The workstation had been in that state for nine days.
4An outage rendered as data
Every npm view in those nine days had failed. None of
them looked failed: warm cache underneath, npm's fetch
layer served the copy it had — well-formed JSON, no warning line,
exit code 0. The outage did not render as an error. It
rendered as data — old data, presented at the same confidence as
live data.
Serving stale on a dead network is a defensible availability choice — an install that works offline beats one that does not. But an answer assembled from cache during an outage is evidence of what upstream looked like when the cache was written, presented as evidence of what upstream looks like now, with nothing in the output to tell the two apart. A version manager, a CI gate, or an agent reading that output has no way to know it is looking at the past.
This is the failure shape this dev log keeps returning to — refusals that look like answers, absences that render like presence. Here it is again wearing freshness: an answer that looks current. The near miss is the instructive part: the wrong story survived every check we ran through the same tool, and died on the first check that travelled a different road.
5Why the observatory was right
The registry the pipeline publishes never disagreed with npm, and the reason is structural rather than virtuous: the refresh runs on ephemeral hosted runners. A machine that is born, fetches, publishes and dies has no nine-day-old cache to be wrong from. The workstation — long-lived, warm caches, accumulated network state — is exactly where this class of error lives. The infrastructure out-freshed the operator.
That is also why nothing user-facing was ever wrong. ovm
resolves versions from the published registry, not from a local npm's
view of upstream. The only place the stale answer existed was in the
hands of the humans and agents investigating — which is how it nearly
became a home-page feature announcing a lag that did not exist.
6What changes
- A freshness claim about an upstream channel needs two transports to agree before we publish it — the CLI's answer and the registry's HTTP API, not the CLI's answer twice.
- Exit code 0 from a tool that caches is not evidence the network was consulted. If the answer matters, ask with an empty cache — the error you get instead is the more honest result.
- The post about npm lagging the native channel is not being written, and the channel-drift panel is not being built. This post is what the evidence supported instead.
Observed with npm 11.11.0 on Node 24.14.1, macOS.
7Postscript: the actual break
Diagnosing the machine afterwards found the real fault, and it was
not the registry, DNS, or the route. Since v20, Node connects with
Happy Eyeballs (autoSelectFamily): it races address
candidates and gives each attempt 250 ms before
aborting it. The workstation was on a phone hotspot where a TCP
connect takes 300–900 ms — so every candidate was killed just
before it could complete, the whole list burned down in about three
seconds, and the socket layer reported ETIMEDOUT. curl,
with its own more patient fallback, connected fine; so did a plain
Node socket given a literal IP, which skips the racing path entirely.
A network that was merely slow rendered as one that was down.
$ node --network-family-autoselection-attempt-timeout=3000 \
-e "fetch('https://registry.npmjs.org/-/ping')..."
OK 200
The fix is one flag —
--network-family-autoselection-attempt-timeout=2000 in
NODE_OPTIONS — which keeps the IPv6-to-IPv4 fallback and
simply gives real links time to answer. The layering of the two
failures is what made the original mystery: a too-eager timeout made
Node's network look dead, and a too-quiet cache made the dead network
look fine.
8References
- registry.npmjs.org — dist-tags endpoint · the direct answer that contradicted the CLI
- npm/make-fetch-happen · npm's fetch layer; HTTP caching and offline fallback behaviour
-
npm docs —
prefer-online· the flag that forces staleness checking, when the network works