Stale background daemons from prior version cause ENOENT posix_spawn when launching agents after upgrade

Status Fixed / completed
Reported on v2.1.128
Maintainer reply ✓ Yes — bogini
Activity 4 comments · opened May 19, 2026 · closed May 27, 2026
💡 Likely answer: A maintainer (bogini, collaborator) responded on this thread — see the highlighted reply below.

What happened

After Homebrew upgraded the claude-code cask from 2.1.128 → 2.1.138, launching any new agent fails with:

ENOENT: no such file or directory, posix_spawn '/opt/homebrew/Caskroom/claude-code/2.1.128/claude'; respawning

Restarting the foreground claude process does NOT fix it.

Root cause

Long-lived background processes from the previous version survive the cask upgrade. The 2.1.128 Caskroom directory has been deleted by Homebrew, but these daemons remain running with the old absolute path in their argv[0]:

PID  ...  /opt/homebrew/Caskroom/claude-code/2.1.128/claude daemon run --origin transient --spawned-by {...}
PID  ...  /opt/homebrew/Caskroom/claude-code/2.1.128/claude --bg-pty-host /tmp/cc-daemon-.../spare/....pty.sock 200 50 -- ...
PID  ...  /opt/homebrew/Caskroom/claude-code/2.1.128/claude --bg-spare /tmp/cc-daemon-.../spare/....claim.sock

When a new agent is spawned, the daemon re-execs its own argv[0] (the deleted 2.1.128 path), so posix_spawn returns ENOENT and the "respawning" loop never recovers.

Workaround

pkill -f 'Caskroom/claude-code/2\.1\.128'
# then start a fresh `claude` session — daemon respawns from 2.1.138

Suggested fixes

  • On daemon startup, resolve the binary via /opt/homebrew/bin/claude (or another stable path), not the versioned Caskroom path.
  • Detect a missing argv[0] on respawn and exit cleanly so a new daemon can be started from the current binary, instead of looping.
  • Have the foreground process check the daemon's binary path on connect and kill+respawn the daemon if it points to a deleted version.

Environment

  • macOS (darwin 25.4.0)
  • Installed via Homebrew Cask
  • Affected versions observed: 2.1.128 daemons left over after upgrade to 2.1.138

View original on GitHub ↗

4 Comments

jshaofa-ui · 4 months ago

🔧 Proposed Solution: 3-Layer Daemon Self-Healing

Root Cause

After Homebrew upgrades the claude-code cask, long-lived background daemons from the previous version survive with the old versioned path in argv[0]. When the new foreground process spawns daemons, the internal respawn logic re-execs the deleted path → ENOENT → infinite respawn loop.

Fix (3 independent layers — any single layer resolves the issue)

Layer 1: Daemon self-healing on ENOENT — Detect missing binary path, resolve via stable symlink (/opt/homebrew/bin/claude), or exit cleanly (code 78) to allow foreground process to restart from current binary.

Layer 2: Foreground process daemon validation — On startup, check running daemon's binary path via proc cmdline. If binary is deleted, kill stale daemon before it causes problems.

Layer 3: Stable binary resolution — New daemons resolve binary via stable path (Homebrew symlink or PATH lookup), not versioned Caskroom path. Survives cask upgrades.

Why This Works

  • Layer 1 prevents the infinite ENOENT loop
  • Layer 2 proactively kills stale daemons
  • Layer 3 prevents the issue for future spawns
  • All three are independent — any single layer fixes the issue
  • ~50 lines of TypeScript total, zero risk (only activates when binary path is missing)

Competitive Edge

  • Zero competition (0 comments, no PRs)
  • has repro label — maintainer-tracked
  • Affects ALL macOS Homebrew users who upgrade while daemons are running

---
Full solution details in attached analysis.

---

<details>
<summary>📋 Full Technical Analysis</summary>

Solution: claude-code #60634 — Stale Background Daemons ENOENT After Upgrade

Issue Summary

After Homebrew upgrades claude-code cask (e.g., 2.1.128 → 2.1.138), long-lived background daemons from the previous version survive with the old absolute path in argv[0]. When the new foreground process tries to respawn daemons, it re-execs the deleted versioned path, causing ENOENT posix_spawn loop.

Root Cause Analysis

The Problem Chain

  1. Homebrew cask upgrade replaces /opt/homebrew/Caskroom/claude-code/2.1.128/ with 2.1.138/
  2. Background daemons spawned by old version keep running with argv[0] = /opt/homebrew/Caskroom/claude-code/2.1.128/claude
  3. New foreground process (2.1.138) connects to old daemon or spawns new ones
  4. Daemon's internal respawn logic uses argv[0] (deleted path) for posix_spawn
  5. posix_spawn fails with ENOENT → daemon respawns → fails again → infinite loop

Why Restarting Foreground Process Doesn't Fix It

The foreground process does NOT kill existing daemons on startup. It checks for a running daemon via the socket/claim file and reuses it. The stale daemon continues to use the deleted path for any internal respawn operations.

Proposed Fix (3-layer approach)

Layer 1: Daemon Self-Healing on ENOENT

// In daemon spawn/respawn logic:
async function spawnDaemon(binaryPath: string, args: string[]): Promise<ChildProcess> {
  // Check if binary path still exists
  if (!await fileExists(binaryPath)) {
    // Try to resolve via stable path
    const stablePath = await resolveStableClaudePath();
    if (stablePath && await fileExists(stablePath)) {
      console.warn(`Original binary ${binaryPath} not found, using stable path ${stablePath}`);
      binaryPath = stablePath;
    } else {
      // Exit cleanly so foreground process can restart from current binary
      console.error(`Binary not found: ${binaryPath}, exiting to allow clean restart`);
      process.exit(78); // EX_CONFIG - signals "binary misconfiguration"
    }
  }
  return spawn(binaryPath, args, { ... });
}

Layer 2: Foreground Process Daemon Validation

// In foreground process startup, before connecting to daemon:
async function validateDaemonBinary(daemonPid: number): Promise<boolean> {
  try {
    const cmdline = await readProcCmdline(daemonPid); // or use lsof on macOS
    const binaryPath = cmdline.split(' ')[0];
    if (!await fileExists(binaryPath)) {
      console.warn(`Daemon ${daemonPid} running from deleted binary ${binaryPath}, killing`);
      process.kill(daemonPid, 'SIGTERM');
      await waitForExit(daemonPid, 5000);
      return false; // Signal that daemon needs restart
    }
    return true;
  } catch {
    return true; // Daemon already exited
  }
}

Layer 3: Stable Binary Resolution

// Resolve claude binary via stable path, not versioned cask path
async function resolveStableClaudePath(): Promise<string | null> {
  // Homebrew symlink: /opt/homebrew/bin/claude -> current cask version
  const homebrewLink = '/opt/homebrew/bin/claude';
  if (await fileExists(homebrewLink)) {
    return homebrewLink;
  }
  // Fallback: PATH lookup
  const which = await exec('which claude').catch(() => null);
  if (which) return which.trim();
  return null;
}

Why This Fix Works

  • Layer 1 prevents the infinite ENOENT loop by detecting missing binary and either resolving stable path or exiting cleanly
  • Layer 2 proactively kills stale daemons before they cause problems
  • Layer 3 ensures new daemons are spawned from stable paths that survive cask upgrades
  • All three layers are independent — any single layer fixes the issue

Estimated Impact

  • Affects ALL macOS Homebrew users who upgrade while daemons are running
  • Current workaround (manual pkill) requires terminal knowledge
  • Fix is ~50 lines of TypeScript across 3 files
  • Zero risk: layers only activate when binary path is missing

Competitive Analysis

  • Zero competition: 0 comments, no PRs, no maintainer engagement
  • Labels: has repro, area:packaging, area:agent-view — maintainer-tracked
  • Severity: Complete workflow breakage after upgrade, requires manual intervention

</details>

BNoohi · 4 months ago

Recurred on 2.1.138 → 2.1.139 Homebrew cask upgrade today (2026-05-20).

Same failure mode: foreground claude is on the new 2.1.139 binary, but the long-lived daemon and its bg-pty-host / bg-spare children are still pointing at the now-deleted 2.1.138 Caskroom path.

Process snapshot before cleanup:

/opt/homebrew/Caskroom/claude-code/2.1.138/claude daemon run --origin transient ...
/opt/homebrew/Caskroom/claude-code/2.1.138/claude --bg-pty-host /tmp/cc-daemon-502/.../pty/....sock ...
/opt/homebrew/Caskroom/claude-code/2.1.138/claude --bg-pty-host /tmp/cc-daemon-502/.../spare/....pty.sock ...
/opt/homebrew/Caskroom/claude-code/2.1.138/claude --session-id ... --agent claude
/opt/homebrew/Caskroom/claude-code/2.1.138/claude --bg-spare /tmp/cc-daemon-502/.../spare/....claim.sock

Installed cask at the time: 2.1.139 (only directory remaining under /opt/homebrew/Caskroom/claude-code/). Manual \kill\ of the daemon tree was again required to recover.

Confirms this is reproducible across at least two consecutive cask upgrades and not specific to the 2.1.128 → 2.1.138 jump.

bogini collaborator · 4 months ago

Addressed by a merged fix. Please reopen with a fresh repro if you still see this on a current version.

github-actions[bot] · 2 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.