May 04, 2026 β€’ Version: 2026.4.29

openclaw update leaves gateway down + unbootable on split Homebrew/nvm installs

Combined failure of npm prefix resolution, launchctl disable state, and missing recovery logic leaves the OpenClaw gateway service down and unreachable after failed updates on macOS systems with split Homebrew/nvm Node installations.

πŸ” Symptoms

Immediate Post-Update Behavior

After executing openclaw update, the process exhibits one of the following failure modes:

Failure Mode A: Silent Hangs bash $ openclaw update Checking for updates… Downloading OpenClaw v2026.5.1… Stopping managed gateway service before package update… Stopped LaunchAgent (degraded) [Process hangs indefinitely or exits silently with exit code 0]

Failure Mode B: Apparent Success with No Change bash $ openclaw update Checking for updates… Downloading OpenClaw v2026.5.1… Stopping managed gateway service before package update… Stopped LaunchAgent (degraded) [Process completes but version unchanged]

Gateway Unavailability Verification

bash $ launchctl list | grep openclaw

Returns nothing β€” service is not loaded

$ curl -s http://localhost:18789/health curl: (7) Failed to connect to localhost port 18789: Connection refused

Bootstrap Failure with I/O Error

bash $ launchctl bootstrap gui/501/ ~/Library/LaunchAgents/ai.openclaw.gateway.plist Bootstrap failed: 5: Input/output error

$ launchctl load -w ~/Library/LaunchAgents/ai.openclaw.gateway.plist Load failed: 5: Input/output error

Disabled State in launchctl

bash $ launchctl print-disabled gui/501 … “ai.openclaw.gateway” => disabled …

Version Remains Unchanged

bash $ cat /opt/homebrew/lib/node_modules/openclaw/package.json | grep ‘“version”’ “version”: “2026.4.29”,

$ openclaw –version 2026.4.29

🧠 Root Cause

The Split-Prefix Pathology

The critical environmental condition occurs when two installation paths diverge:

ComponentPath
OpenClaw installation/opt/homebrew/lib/node_modules/openclaw
Active npm binary~/.nvm/versions/node/<v>/bin/npm
Active node binary~/.nvm/versions/node/<v>/bin/node

This configuration is extremely common because:

  • Homebrew installs Node packages under /opt/homebrew/lib/node_modules/
  • nvm prepends itself to PATH in shell profile configuration
  • The user installed OpenClaw via Homebrew’s npm, but npm update picks up nvm’s npm from PATH

Bug 1: launchctl bootout Induces Disabled State

Mechanism:

  1. launchctl bootout gui/<uid>/ai.openclaw.gateway removes the running service
  2. macOS’s launchd daemon writes a "disabled" override entry for this service in its per-user domain state
  3. Subsequent launchctl bootstrap or load operations respect this override and fail with 5: Input/output error
  4. The only remediation is launchctl enable gui/<uid>/ai.openclaw.gateway before bootstrap

Affected Code: dist/update-cli-CycY__x8.js:584-587 contains restart logic that performs launchctl enable but only executes if the npm install step completes successfully.

Bug 2: npm install Ignores Detected Package Root

Mechanism:

  1. installTarget.packageRoot in update-cli correctly resolves to /opt/homebrew/lib/node_modules/openclaw
  2. However, the spawned npm install -g openclaw@<tag> command does not pass --prefix /opt/homebrew
  3. npm resolves its global prefix via npm prefix -g, which returns nvm’s prefix (~/.nvm/versions/node/<v>)
  4. npm checks for existing openclaw at nvm’s prefix, finds nothing, and either:
    • Installs a fresh copy under nvm’s prefix (leaving Homebrew copy stale)
    • Silently no-ops because it believes the package is already satisfied
    • Hangs indefinitely due to conflicting lock states

Affected Code: The npm command construction in update-cli receives the correct packageRoot but does not translate it into a --prefix argument.

Bug 3: No Recovery Path on npm Failure

Mechanism:

  1. maybeRestartService at line 1968 is downstream of the npm install promise chain
  2. If npm install rejects (timeout, network error) or hangs, the promise chain breaks
  3. Control never reaches the restart logic that would call launchctl enable
  4. The service remains bootout’d and disabled β€” a zombie state

Affected Code: No try/finally wrapper exists around the npm install step that guarantees service recovery.

Combined Failure Sequence

openclaw update └─> Detect packageRoot = /opt/homebrew/lib/node_modules/openclaw βœ“ └─> launchctl bootout gui//ai.openclaw.gateway βœ“ (service stopped, disabled flag set) └─> spawn npm install -g openclaw@latest └─> npm uses nvm’s prefix (~/.nvm/…) instead of /opt/homebrew βœ— └─> Install fails/silent-no-ops/hangs βœ— └─> npm step fails β†’ promise chain breaks └─> maybeRestartService never executes βœ— └─> Gateway is down and unbootable βœ—

πŸ› οΈ Step-by-Step Fix

Option A: Immediate Recovery (Restore Gateway Without Update)

bash

Step 1: Re-enable the service in launchd’s disabled registry

launchctl enable gui/$(id -u)/ai.openclaw.gateway

Step 2: Load the LaunchAgent with overwrite flag

launchctl load -w ~/Library/LaunchAgents/ai.openclaw.gateway.plist

Step 3: Verify service startup

sleep 3 curl -s http://localhost:18789/health

Expected output: {“ok”:true,“status”:“live”}

Option B: Perform Update with Explicit Prefix

bash

Stop the service gracefully first

launchctl bootout gui/$(id -u)/ai.openclaw.gateway 2>/dev/null

Re-enable before bootstrap

launchctl enable gui/$(id -u)/ai.openclaw.gateway

Install with explicit prefix targeting Homebrew location

npm install -g openclaw@latest –prefix /opt/homebrew

Bootstrap the service

launchctl load -w ~/Library/LaunchAgents/ai.openclaw.gateway.plist

Verify

curl -s http://localhost:18789/health

Ensure Homebrew’s npm takes precedence over nvm by adjusting PATH order:

For Zsh (~/.zshrc): bash

Place Homebrew before nvm in PATH

export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH"

Then load nvm

export NVM_DIR="$HOME/.nvm" [ -s “/opt/homebrew/opt/nvm/nvm.sh” ] && . “/opt/homebrew/opt/nvm/nvm.sh”

For Bash (~/.bashrc or ~/.bash_profile): bash

Place Homebrew before nvm in PATH

export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH"

Then load nvm

export NVM_DIR="$HOME/.nvm" [ -s “$NVM_DIR/nvm.sh” ] && . “$NVM_DIR/nvm.sh”

Then verify correct resolution: bash source ~/.zshrc # or source ~/.bashrc which npm # Should return /opt/homebrew/bin/npm npm prefix -g # Should return /opt/homebrew

Downstream Fix (Requires OpenClaw Code Update)

The update script at dist/update-cli-CycY__x8.js requires the following modifications:

Fix 1: Pass --prefix to npm install javascript // Before (broken): const npmInstall = spawn(’npm’, [‘install’, ‘-g’, openclaw@${tag}], opts);

// After (fixed): const npmInstall = spawn(’npm’, [‘install’, ‘-g’, openclaw@${tag}, ‘–prefix’, packageRoot], opts);

Fix 2: Wrap update flow in try/finally for guaranteed restart javascript try { // … npm install logic … } finally { // Always restore service state regardless of npm outcome await maybeRestartService(); }

Fix 3: Pre-bootout enable to prevent disabled state javascript // Run enable before bootout to clear any stale disabled flag await exec(’launchctl enable gui/’ + uid + ‘/ai.openclaw.gateway’); await exec(’launchctl bootout gui/’ + uid + ‘/ai.openclaw.gateway’);

πŸ§ͺ Verification

Verify Gateway is Running

bash $ launchctl list | grep openclaw

  • 0 ai.openclaw.gateway

Exit code 0 with PID shown indicates healthy service

bash $ curl -s http://localhost:18789/health {“ok”:true,“status”:“live”}

Verify No Disabled Override Remains

bash $ launchctl print-disabled gui/$(id -u) | grep openclaw

No output = service is not in disabled list = healthy

Verify Version is Updated

bash $ cat /opt/homebrew/lib/node_modules/openclaw/package.json | grep ‘“version”’ “version”: “2026.5.1”,

$ openclaw –version 2026.5.1

Verify Correct npm Prefix Resolution

bash $ which npm /opt/homebrew/bin/npm

$ npm prefix -g /opt/homebrew/lib/node_modules

Full Integration Test

bash

1. Confirm current state

launchctl list | grep openclaw && curl -s http://localhost:18789/health

2. Trigger update

openclaw update

3. Verify post-update state

launchctl list | grep openclaw curl -s http://localhost:18789/health cat /opt/homebrew/lib/node_modules/openclaw/package.json | grep ‘“version”’

Expected final state:

  • launchctl list shows service with PID (not -)
  • Health endpoint returns {"ok":true,"status":"live"}
  • package.json version reflects updated release

⚠️ Common Pitfalls

1. PATH Order Sensitivity

The issue reproduces only when nvm’s npm appears before Homebrew’s npm in PATH. Users who configured Homebrew before nvm in their shell initialization typically do not encounter this bug. Always verify which npm before reporting or debugging.

2. Silent No-Ops on npm install

When npm believes the package is already satisfied (checks nvm prefix, finds nothing, but due to caching believes installation succeeded), it exits 0 without error but performs no actual work. This makes the failure invisible unless version is explicitly checked.

Mitigation: Compare package.json version before and after, or use npm install -f --force --prefix /opt/homebrew.

3. Disabled Override Persists Across Sessions

The launchctl disabled override is persisted in ~/Library/LaunchAgents/ plist files and launchd’s per-user domain state. Simply unloading the plist does not clear the disabled flag. Must explicitly run launchctl enable before load.

4. Version in package.json vs. dist/index.js Version

The update script may update package.json but fail to update the bundled dist/index.js. Always verify both: bash cat /opt/homebrew/lib/node_modules/openclaw/package.json | grep ‘“version”’ cat /opt/homebrew/lib/node_modules/openclaw/dist/index.js | grep ‘version’

5. Docker/Container Environments

If running inside a Docker container on macOS, launchctl commands target the container’s namespace, not the host’s launchd. The service management commands are irrelevant inside containers β€” use direct process management (node dist/index.js) instead.

6. Permission Errors on launchctl

Non-root users cannot manage services in other users’ domains. Ensure commands use gui/$(id -u) format, not gui/0 (root) or explicit UID values from another user.

7. Stale Lock Files After Interrupted Update

If the update process was killed mid-execution, npm may leave behind ~/.npm/_locks/ files that cause subsequent installs to hang. Clear locks: bash rm -rf ~/.npm/_locks

  • Bootstrap failed: 5: Input/output error
    Generic launchd bootstrap failure. Occurs when service is in disabled state or plist is malformed. In this context, indicates the service was disabled by prior bootout without subsequent enable.
  • Load failed: 5: Input/output error
    Same root cause as bootstrap failure β€” service disabled override active. Requires launchctl enable before load.
  • ENOTDIR / EACCES during npm install
    May occur if --prefix path does not exist or npm lacks write permission to the Homebrew node_modules directory. Ensure npm is run with appropriate permissions or via Homebrew's node environment.
  • ETIMEDOUT on npm registry lookup
    Network timeout reaching registry.npmjs.org. The update script has no retry logic, leading to silent failure and subsequent service downtime.
  • Service shows PID but health check fails
    OpenClaw process is running but gateway is non-responsive. May indicate corruption in dist/index.js or configuration mismatch after partial update. Full reinstall recommended.
  • LaunchAgent appears in list but immediately exits
    Exit code -1 or signal termination indicates the process is crashing on startup. Check logs at ~/Library/Logs/openclaw/ or run manually with node /opt/homebrew/lib/node_modules/openclaw/dist/index.js to capture error output.

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.