Background auto-updater lacks a post-install effectiveness check — five identical same-version reinstalls in 109 min when the running copy isn't the prefix install

Status Open
Reported on v2.1.237
Maintainer reply None cached
Activity 2 comments · opened Aug 25, 2026

Version: running copy 2.1.237, npm-prefix install 2.1.245 (the loop reinstalls 2.1.245)
Platform: linux x64 (kernel 7.0.0-30-generic), npm 9.2.0, node v22.22.1
Install method: npm global (~/.npm-global prefix via ~/.npmrc)

Summary

#84224 documents the targeting defect: the updater compares the registry's latest against the running process's version but installs into the config-resolved npm prefix. This issue is the missing guard one layer down: nothing ever checks that an install changed the thing the decider measures. When the running copy and the prefix install diverge, the background updater settles into a stable, unattended loop — poll, conclude "outdated", npm install -g the same version into a prefix that already holds it, change nothing, repeat next cycle.

On this box that produced five identical reinstalls of 2.1.245 in 109 minutes (11:12Z → 13:00:51Z, ~27 min apart on average, matching the ~30-min poll cadence) from one long-lived stale session, with no user action. Nothing bounds the loop: it runs as long as the stale session lives — a session left overnight is ~48 full package reinstalls a day, each one a fresh trip through the retire-rename window that strands the deterministic-name orphans and ENOTEMPTY bricks of #88091 / #12026 (a 649 MB orphan was measured on this box).

Measured evidence (2026-08-25, times UTC)

Setup: stale root-owned 2.1.237 under /usr/local (what non-login SSH PATH resolved), current 2.1.245 under ~/.npm-global (the npm-config prefix). Full setup and attribution detail is in my measurement comment on #84224: https://github.com/anthropics/claude-code/issues/84224#issuecomment-5410148808

Five same-version reinstalls by the stale 2.1.237 session's background updater — every one @2.1.245, into a prefix already holding 2.1.245:

| # | time | evidence |
|---|------|----------|
| 1 | 11:12Z | npm debug log: verbose argv "install" "--global" "@anthropic-ai/claude-code@2.1.245" |
| 2 | 11:26Z | same fingerprint |
| 3 | between 11:26Z and 12:08Z | counted at measurement time; its debug log rotated away before the timestamp was copied out |
| 4 | 12:08:49Z | same argv; same deterministic retire names; 3,756 ms reify; exit 0 |
| 5 | 13:00:48–51Z | filesystem fingerprint: 24 package paths rewritten in one pass, 3 s spread matching the logged ~3.7 s reify |

Each pass re-enters the ~3.7 s window in which npm retires the live package dir to a deterministic name (.claude-code-9bcaTW9F here). Interrupt any one of the ~48 daily passes in that window and you get the orphan/brick failure class of #88091 / #12026 — the loop is an exposure multiplier for those bugs.

Note for reproducers: npm caps debug logs at 10 files and its cleaner removes in readdir order (measured over three consecutive runs — each npm invocation deleted its predecessor's same-day log), so churn evidence rotates away within hours. The install tree's bin mtime is the durable witness: an install, unlike a log, cannot rotate away the mtime it leaves behind.

Why this isn't just #84224

Fixing the targeting (#84224) removes today's divergence, so it happens to stop today's loop. But the loop itself is a missing feedback path: the updater performs a full package reinstall with zero effect, forever, and never notices. Any future divergence class — broken symlink, permissions fallback into a different prefix, PATH quirks — loops the same way. A post-install effectiveness check bounds all of them, including ones that don't exist yet.

Verified fix on this box (before/after)

Converge the launch path onto the install the updater actually writes to:

sudo ln -sfn /home/<user>/.npm-global/bin/claude /usr/local/bin/claude   # re-point stale bin
sudo rm -rf /usr/local/lib/node_modules/@anthropic-ai                    # remove stale tree (after stale sessions exit)

After the stale session exited and a replacement started through the re-pointed symlink (execpath in ~/.npm-global, self-reported 2.1.245):

  • First poll after convergence (13:12:52Z): npm view @anthropic-ai/claude-code@latest version --prefer-online, GET 200 in 71 ms → no install. Same check as before, opposite outcome, because decider and target finally belong to the same install.
  • Second poll (14:08:42Z): same fingerprint, exit 0 → no install.
  • ~/.npm-global/bin/claude mtime frozen at 13:00:51Z — churn #5 remains the last write to the install tree. Registry latest stayed 2.1.245 throughout, so both no-install decisions were made against an unchanged latest.
  • Scale: the 79 minutes before convergence contained reinstall churns #4 and #5; the 79 minutes after contained two polls and zero installs.

Suggested fixes (any one bounds the loop)

  1. Effectiveness check (this issue's ask): after an install, re-derive the decider's input; if it is unchanged, stop repeating — log once, back off, or surface it to the user.
  2. Convergence guard: before installing, compare the running execpath against the target prefix; on mismatch, report "a stale copy is running — restart it" instead of reinstalling.
  3. Target fix (#84224): compare against, and install into, the running binary's own prefix.

Repro

  1. Two npm-global installs of claude-code in different prefixes (e.g. /usr/local and ~/.npm-global), with npm's config prefix pointing at the newer one.
  2. Launch the older binary and leave the session running.
  3. Watch ~/.npm/_logs/: a same-version install --global appears every poll cycle, indefinitely.

Related: #84224 (targeting root cause; my measurements in this comment), #88091 / #12026 (the orphan and disk consequences each loop pass risks).

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗