[Bug] npm-global auto-updater + macOS AppleSystemPolicy guts package directory, breaks CLI
Summary
The CLI's built-in auto-updater runs npm install -g @anthropic-ai/claude-code to update the npm-global installation. On macOS, AppleSystemPolicy (Gatekeeper) blocks the newly-written cli.js from executing, causing the install to fail partway through. Because npm deletes old package files before writing new ones, the failed install leaves the package directory gutted — only vendor/ripgrep/ survives. The CLI becomes completely unusable (command not found) until manually reinstalled.
Steps to Reproduce
- Install Claude Code globally via npm:
npm install -g @anthropic-ai/claude-code - Start one or more Claude Code sessions
- Wait for the auto-updater to detect a newer version and trigger
npm install -g - If macOS AppleSystemPolicy blocks the new
cli.js, the package directory is gutted
Expected Behavior
- The auto-updater should not leave the package directory in a broken state on install failure
- At minimum, the old installation should be preserved if the new one fails (atomic swap or backup-before-delete)
Actual Behavior
After the failed update, /opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/ contains only:
vendor/
ripgrep/ (empty)
No cli.js, no package.json, no bin symlink at /opt/homebrew/bin/claude. New terminal sessions get command not found: claude. Running sessions continue (code in memory) but cannot spawn children.
Evidence (macOS unified log)
Full timeline from log show:
| Time | Event |
|------|-------|
| 21:01–21:18 | Repeated npm view @anthropic-ai/claude-code@latest version checks (~every 1-7 min) |
| 21:08:08 | First npm install @anthropic-ai/claude-code |
| 21:15:24 | Second install attempt |
| 21:21:53 | Third install — npm deletes old files, writes new ones |
| 21:21:55 | kernel: (AppleSystemPolicy) ASP: Security policy would not allow process: 3730, /opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js (3 blocks) |
| 21:21:55 | Immediate retry within 2 seconds |
| 21:23:03 | Another retry |
| 21:23:03 | kernel: (AppleSystemPolicy) ASP: Unable to retrieve vnode for script: cli.js — file is gone (2,671 errors as running sessions try to access the deleted binary) |
The key transition: at 21:21:55 ASP blocks the new file ("would not allow"), and by 21:23:03 the file doesn't exist at all ("unable to retrieve vnode") — the retry deleted whatever remained.
Log commands to reproduce diagnostics
# Version check + install attempts
/usr/bin/log show \
--predicate 'eventMessage CONTAINS "claude-code" OR eventMessage CONTAINS "npm install"' \
--start "2026-04-15 21:00:00" --end "2026-04-15 21:25:00" --style compact
# ASP blocks
/usr/bin/log show \
--predicate 'eventMessage CONTAINS "Security policy" AND eventMessage CONTAINS "cli.js"' \
--start "2026-04-15 21:00:00" --end "2026-04-15 21:24:00" --style compact
Root Cause Analysis
Three interacting problems:
1. npm's non-atomic global install
npm install -g deletes the old package directory contents before writing new files. If the new install fails for any reason, the old installation is destroyed with no rollback.
2. macOS AppleSystemPolicy blocks the new cli.js
The newly-downloaded cli.js is blocked by Gatekeeper/ASP, likely due to quarantine attributes on the npm-fetched tarball. This causes the install's postinstall or verification step to fail.
3. Aggressive retry without backoff
The auto-updater retries within seconds of failure. Each retry re-triggers npm's delete-then-install cycle, compounding the damage. Three install attempts in 90 seconds (21:21:53, 21:21:55, 21:23:03).
Recovery
npm install -g @anthropic-ai/claude-code
Proposed Fixes
- Backup before update: Copy the existing installation to a temp location before running
npm install -g. Restore on failure. - Verify before deleting: Check that the new binary is executable (passes ASP) before removing the old one.
- Backoff on failure: Don't retry immediately. Exponential backoff (minutes, not seconds).
- Clear quarantine: Run
xattr -d com.apple.quarantineon the newcli.jsafter npm writes it, before attempting execution. - Respect
DISABLE_AUTOUPDATER: Per #36250, this env var doesn't prevent the version check. It should also prevent the install to avoid this scenario entirely.
Workaround
# Disable the auto-updater entirely
# In ~/.claude/settings.json:
{
"env": {
"DISABLE_AUTOUPDATER": "1"
}
}
Note: per #36250, this only prevents the install step, not the npm view version checks.
Relationship to Other Issues
| Issue | Relationship |
|-------|-------------|
| #47253 | Same category (auto-updater destroys running install) but different install method (native installer vs npm-global) and different failure mechanism (binary unlinked vs ASP-blocked install guts package dir) |
| #36250 | DISABLE_AUTOUPDATER doesn't suppress npm view — the version check that triggers the install in this bug |
| #41602 | Auto-updater fails to activate downloaded version — same updater, different failure mode |
| #43719 | Auto-update data loss (Cowork) — same class of "updater destroys state" bugs |
Environment
- macOS 26.2 (Darwin 25.2.0), Apple Silicon (arm64)
- Claude Code 2.1.110 (post-recovery), installed via
npm install -g - Node v25.8.2, npm 11.11.1
- npm prefix:
/opt/homebrew
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗