[BUG] Auto-update silently ships a non-functional install on stock npm 12

Status Open
Reported on v2.1.233
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Aug 15, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

npm 12 (released 2026-07-08) blocks dependency lifecycle scripts by default unless covered by the root package's allowScripts policy. When Claude Code self-updates via the npm install path, npm blocks its postinstall: node install.cjs, exits 0, and the update completes — but the native binary is never moved into place.

The platform optional dependency downloads correctly (@anthropic-ai/claude-code-linux-x64/claude, ~324 MB, mode 755, intact). Because install.cjs never runs, bin/claude.exe — the path node_modules/.bin/claude resolves to — keeps the shipped fallback stub. The result is an installation that reports success and does not run.

This requires no user configuration. No flags, ignore-scripts=false, user and global npmrc both empty; verbose argv in the npm debug log confirms a bare install invocation. It affects every npm-path user as they reach npm 12.

The stub also has no shebang. Callers going through a shell see its (helpful) error text; callers using execve/posix_spawn get ENOEXEC instead, losing the diagnostic entirely.

What Should Happen?

An update that cannot complete should not report success, and should not replace a working install.

Specifically:

  1. Verify before declaring success — confirm post-install that the binary is real (size, executability, or a --version smoke test). npm's exit code is not sufficient here.
  2. Pre-flight the policy — detect blocked scripts before updating and abort, leaving the existing working install intact.
  3. Surface the fix — tell the user to run npm install-scripts approve @anthropic-ai/claude-code. Writing that entry automatically is arguably defensible (the updater is already-trusted code running as the user, not an untrusted package escalating inside npm's sandbox), but it loosens a security control the user may have adopted deliberately, so it should be announced rather than silent.
  4. Don't misroute on suppression — have install.cjs write the real binary to a distinct path and make the stub a launcher, so a blocked postinstall fails visibly instead of silently substituting a non-functional file.
  5. Add a shebang to the stub so its error message survives non-shell invocation.

Error Messages/Logs

npm warning during install (npm 12.0.2):


npm warn install-scripts @anthropic-ai/claude-code@2.1.233 (postinstall: node install.cjs)


`npm install-scripts ls`:


1 package has install scripts blocked because they are not covered by allowScripts:
  @anthropic-ai/claude-code@2.1.233 (postinstall: node install.cjs)


Stub contents (first bytes — note: no shebang):


echo "Error: claude native binary not installed." >&2


Via shell:


Error: claude native binary not installed.
Either postinstall did not run (--ignore-scripts, some pnpm configs)
or the platform-native optional dependency was not downloaded
(--omit=optional).


Via `posix_spawn` (agent session spawning):


Error: Failed to spawn session <id>: ENOEXEC: unknown error, posix_spawn
'/home/<user>/.claude/local/node_modules/@anthropic-ai/claude-code/bin/claude.exe'


npm debug log, confirming no flags were passed:


8 verbose argv "install" "@anthropic-ai/claude-code@2.1.233"


**Environment:** Claude Code 2.1.233 · npm 12.0.2 · Arch Linux x86_64, unprivileged LXC container · npm local install under `~/.claude/local`

Steps to Reproduce

  1. Use npm >= 12.0.0 with the package not previously approved (fresh environment, or one where allowScripts has no entry for it).
  2. Run npm install @anthropic-ai/claude-code.
  3. Observe npm exits 0, with only a warning about blocked install scripts.
  4. file node_modules/@anthropic-ai/claude-code/bin/claude.exe → reports ASCII text, not an ELF binary.
  5. npm install-scripts ls → shows the package as blocked.
  6. Run the resulting claude — via a shell it prints "native binary not installed"; via execve/posix_spawn it fails with ENOEXEC.

In the observed case this occurred through background auto-update rather than a manual install, so steps 1–3 happened unattended.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.233

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

iTerm2

Additional Information

Relationship to existing issues. #62684 reports the same stub-instead-of-binary outcome on self-update, but triggered by a user-set ignore-scripts=true. Filing separately because the trigger has moved from opt-in to default: that report reads as affecting a security-conscious minority, whereas this fires on stock configuration for everyone reaching npm 12. Its suggested fix (write the binary to a distinct path; reconsider self-update tearing the binary out from under live processes) applies here too. Also related: #50203 (Bun blocks the same postinstall) and npm/cli#9738 (the approve path has bugs of its own — ENOMATCH, "no trusted identity for policy key").

Why this is hard to diagnose. The failure is deferred — the broken update landed ~04:00, nothing failed until ~15:40 when a new session needed the binary. The symptom points away from the cause: .exe plus ENOEXEC on Linux reads as a wrong-platform binary. Every config surface reads clean. Debug logs show three identical install attempts ten minutes apart, so something retried, but an identical npm install against a policy block only reproduces the block.

Naming varies by npm version. 12.0.2 emits install-scripts (npm install-scripts ls|approve); earlier versions emit allow-scripts (npm approve-scripts --allow-scripts-pending). Docs should cover both.

Structural note. The native installer already avoids this class of failure — versions stage under ~/.local/share/claude/versions/ and a symlink flips, so a bad download never becomes live. The npm path has no equivalent atomic swap or rollback.

Workaround.

npm install-scripts approve @anthropic-ai/claude-code
node node_modules/@anthropic-ai/claude-code/install.cjs

Attribution. Drafted with Claude Opus 5 (claude.ai chat interface, extended thinking enabled; web search used to confirm npm 12 behavior and locate related issues). All command output was run and verified by the human reporter; the model had no access to the affected host. Environment details generalized to remove identifying information. Diagnosis took roughly six exchanges, with several wrong hypotheses first (wrong-platform binary, disk exhaustion, container OOM, ZFS snapshot rollback) before the npm debug log revealed the cause — which supports the diagnosability point above. Conclusions are the model's synthesis of human-verified evidence and warrant checking rather than assumption.

View original on GitHub ↗

3 Comments

bcherny collaborator · 1 month ago

Confirmed / reproduced on 2.1.234 with npm 12.0.2 (Linux): npm install @anthropic-ai/claude-code exits 0 with only an install-scripts warning that the postinstall was blocked; the platform package is downloaded, but bin/claude.exe remains the ~500-byte text stub (file reports ASCII text), and running claude prints "claude native binary not installed" and exits 1. The same install with npm 10.9 on the same machine produces a working binary. npm install-scripts approve @anthropic-ai/claude-code followed by re-running the postinstall works around it, as you noted.

🤖 Generated with Claude Code

---
_Generated by Claude Code_

git-kubik · 1 month ago

Hitting this on Linux, and I think there is a second, independent way to reach the
same broken state that is worth separating from the allowScripts cause in the
report — because the diagnostic you get points somewhere different.

Variant: the JS package is published ahead of the platform binary.

@anthropic-ai/claude-code@2.1.237 pins its native optional dependencies to the
exact version:

"optionalDependencies": {
  "@anthropic-ai/claude-code-linux-x64": "2.1.237",
  ...
}

As of 2026-08-20 01:05 UTC, that version does not exist:

$ npm view @anthropic-ai/claude-code version
2.1.237
$ npm view @anthropic-ai/claude-code-linux-x64 version
2.1.236

npm silently skips an optional dependency it cannot resolve, so the install
completes with node_modules/ entirely empty:

$ ls .../node_modules/@anthropic-ai/claude-code/
bin  cli-wrapper.cjs  install.cjs  LICENSE.md  package.json  README.md  sdk-tools.d.ts
                                                          ^ no node_modules at all
$ ls .../node_modules/@anthropic-ai/claude-code/node_modules/@anthropic-ai/
(empty)

This matters for diagnosis. In the original report the optional dependency
downloads correctly and only the postinstall link is missing, so the binary is
present and repairable. Here there is nothing to link, and running the
postinstall by hand says so:

$ node .../@anthropic-ai/claude-code/install.cjs
[@anthropic-ai/claude-code postinstall] Native package "@anthropic-ai/claude-code-linux-x64" not found.
  This happens with --omit=optional or when the download failed.

Neither of those two suggestions applies — no flags were passed, and nothing
failed to download. The version simply is not published. The message sends you
looking at your own npm configuration, which is where I spent the first twenty
minutes.

The end state is byte-identical to the report: bin/claude.exe is the 500-byte
shebang-less stub, and node_modules/.bin/claude resolves to it.

The auto-updater actively fights the workaround. Pinning to the last version
whose native package exists is undone within seconds. From ~/.npm/_logs,
unedited:

01:02:57  npm i @anthropic-ai/claude-code@2.1.236 --global     <- my pin
01:03:11  npm view @anthropic-ai/claude-code@latest version --prefer-online
01:03:11  npm install --global @anthropic-ai/claude-code@2.1.237   <- updater, 14s later

So on a machine with a session running, there is currently no self-service fix
at all: 2.1.237 cannot install its binary, and 2.1.236 does not survive long
enough to be used. #88091 describes the updater racing itself; this is the
updater racing the operator's remediation, with the same outcome.

Environment

  • Arch Linux x86_64, kernel 7.1.4
  • node v26.5.0 (mise-managed), npm 11.17.0 — note this is npm 11, not 12.

It does not block lifecycle scripts outright but does warn:
npm warn allow-scripts 1 package has install scripts not yet covered by allowScripts
so both causes are live on the same box.

  • Install path: ~/.local/share/mise/installs/node/26.5.0/lib/node_modules/@anthropic-ai/claude-code
  • A second node install (25.8.0) carrying 2.1.222 is intact and complete, which

makes the contrast easy to see:

``
node 25.8.0 claude-code 2.1.222 node_modules/@anthropic-ai/claude-code-linux-x64 present
node 26.5.0 claude-code 2.1.237 node_modules/@anthropic-ai/ empty
``

Knock-on effect worth noting for the "fails loudly" argument. The blast
radius is wider than the CLI itself. Anything that shells out to claude
inherits the failure, and the stub's text mentions --ignore-scripts and
--omit=optional — so an unrelated tool suddenly fails with what reads like a
packaging complaint about that tool. On this machine every ssh invocation
from an automation shell started returning the postinstall error mid-session,
and the obvious first hypothesis was a network or SSH-agent problem. The install
had been working ten minutes earlier and nothing about it had been touched.

Suggested addition to the fixes already proposed in the report: whatever
publishes the release should not tag the JS package latest until every
platform binary of that exact version is published, and the updater should
refuse to move to a version whose platform binary it cannot resolve, rather than
installing it and leaving the machine worse than before.

marcindulak · 26 days ago

For your information: the label reproduced on this issue does not prevent it from becoming stale and eventually auto-closed. See https://github.com/anthropics/claude-code/issues/87647 for details, and consider upvoting that for visibility.