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:
| Component | Path |
|---|---|
| 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
PATHin shell profile configuration - The user installed OpenClaw via Homebrew’s npm, but
npm updatepicks up nvm’s npm from PATH
Bug 1: launchctl bootout Induces Disabled State
Mechanism:
launchctl bootout gui/<uid>/ai.openclaw.gatewayremoves the running service- macOS’s launchd daemon writes a
"disabled"override entry for this service in its per-user domain state - Subsequent
launchctl bootstraporloadoperations respect this override and fail with5: Input/output error - The only remediation is
launchctl enable gui/<uid>/ai.openclaw.gatewaybefore 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:
installTarget.packageRootinupdate-clicorrectly resolves to/opt/homebrew/lib/node_modules/openclaw- However, the spawned
npm install -g openclaw@<tag>command does not pass--prefix /opt/homebrew - npm resolves its global prefix via
npm prefix -g, which returns nvm’s prefix (~/.nvm/versions/node/<v>) - npm checks for existing
openclawat 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:
maybeRestartServiceat line 1968 is downstream of the npm install promise chain- If npm install rejects (timeout, network error) or hangs, the promise chain breaks
- Control never reaches the restart logic that would call
launchctl enable - 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/
π οΈ 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
Option C: Permanent Environment Fix (Recommended)
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 listshows service with PID (not-)- Health endpoint returns
{"ok":true,"status":"live"} package.jsonversion 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
π Related Errors
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. Requireslaunchctl enablebefore load.ENOTDIR / EACCESduring 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.ETIMEDOUTon 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 indist/index.jsor 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 withnode /opt/homebrew/lib/node_modules/openclaw/dist/index.jsto capture error output.