openclaw - 💡(How to fix) Fix [Bug] Auto-update subprocess crashes before restart handoff: piped stdout/stderr breaks when bootout kills parent gateway [1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#58890Fetched 2026-04-08 02:31:26
View on GitHub
Comments
1
Participants
1
Timeline
0
Reactions
0
Author
Participants

The built-in auto-update consistently kills the gateway on macOS with no recovery, leaving the service unloaded indefinitely. The root cause is that resolveCommandStdio forces piped stdout/stderr for the update subprocess, creating a fatal dependency on the gateway parent process — which is then killed by launchctl bootout during the restart step.

This is the underlying mechanism behind several related reports (#54861, #57379, #58041, #40811, #48992) that describe symptoms (service unloaded, multi-hour outages, KeepAlive not respected) without pinpointing why the restart handoff never completes.

Error Message

  1. Any subsequent write to stdout/stderr triggers EPIPE / unhandled error

Root Cause

The built-in auto-update consistently kills the gateway on macOS with no recovery, leaving the service unloaded indefinitely. The root cause is that resolveCommandStdio forces piped stdout/stderr for the update subprocess, creating a fatal dependency on the gateway parent process — which is then killed by launchctl bootout during the restart step.

Fix Action

Workaround

Disable built-in auto-update and run updates from a standalone launchd job:

// openclaw.json
"update": { "auto": { "enabled": false } }
<!-- ~/Library/LaunchAgents/ai.openclaw.auto-update.plist -->
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>ai.openclaw.auto-update</string>
    <key>ProgramArguments</key>
    <array>
      <string>/opt/homebrew/opt/node@22/bin/node</string>
      <string>/opt/homebrew/lib/node_modules/openclaw/dist/entry.js</string>
      <string>update</string>
      <string>--yes</string>
      <string>--channel</string>
      <string>stable</string>
    </array>
    <key>StartInterval</key>
    <integer>3600</integer>
    <key>EnvironmentVariables</key>
    <dict>
      <key>HOME</key>
      <string>/Users/you</string>
      <key>PATH</key>
      <string>/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
      <key>OPENCLAW_LAUNCHD_LABEL</key>
      <string>ai.openclaw.gateway</string>
    </dict>
  </dict>
</plist>

Code Example

function resolveCommandStdio(params): ["pipe" | "inherit" | "ignore", "pipe", "pipe"] {
  return [
    params.hasInput ? "pipe" : params.preferInherit ? "inherit" : "pipe",
    "pipe",  // stdout always piped
    "pipe",  // stderr always piped
  ];
}

---

// openclaw.json
"update": { "auto": { "enabled": false } }

---

<!-- ~/Library/LaunchAgents/ai.openclaw.auto-update.plist -->
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>ai.openclaw.auto-update</string>
    <key>ProgramArguments</key>
    <array>
      <string>/opt/homebrew/opt/node@22/bin/node</string>
      <string>/opt/homebrew/lib/node_modules/openclaw/dist/entry.js</string>
      <string>update</string>
      <string>--yes</string>
      <string>--channel</string>
      <string>stable</string>
    </array>
    <key>StartInterval</key>
    <integer>3600</integer>
    <key>EnvironmentVariables</key>
    <dict>
      <key>HOME</key>
      <string>/Users/you</string>
      <key>PATH</key>
      <string>/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
      <key>OPENCLAW_LAUNCHD_LABEL</key>
      <string>ai.openclaw.gateway</string>
    </dict>
  </dict>
</plist>
RAW_BUFFERClick to expand / collapse

Summary

The built-in auto-update consistently kills the gateway on macOS with no recovery, leaving the service unloaded indefinitely. The root cause is that resolveCommandStdio forces piped stdout/stderr for the update subprocess, creating a fatal dependency on the gateway parent process — which is then killed by launchctl bootout during the restart step.

This is the underlying mechanism behind several related reports (#54861, #57379, #58041, #40811, #48992) that describe symptoms (service unloaded, multi-hour outages, KeepAlive not respected) without pinpointing why the restart handoff never completes.

Root Cause

1. Piped stdout/stderr creates a parent dependency

In src/process/spawn-utils.ts, resolveCommandStdio always returns "pipe" for stdout and stderr:

function resolveCommandStdio(params): ["pipe" | "inherit" | "ignore", "pipe", "pipe"] {
  return [
    params.hasInput ? "pipe" : params.preferInherit ? "inherit" : "pipe",
    "pipe",  // stdout always piped
    "pipe",  // stderr always piped
  ];
}

When runAutoUpdateCommand calls runCommandWithTimeout, the update subprocess's stdout/stderr are piped back to the gateway process for capture. The gateway holds the reading end of these pipes.

2. refreshGatewayServiceEnv kills the pipe-holder

The update flow:

  1. Gateway spawns openclaw update --yes --channel stable --json (child, piped stdout/stderr)
  2. npm install succeeds (binary updated on disk)
  3. printResult writes JSON to stdout → pipe to gateway (still alive, works fine)
  4. updatePluginsAfterCoreUpdate runs
  5. maybeRestartServicerefreshGatewayServiceEnvopenclaw gateway install --force
  6. gateway install --forceactivateLaunchAgentlaunchctl bootout
  7. Bootout kills the gateway (the process holding the pipe's reading end)
  8. The update subprocess's stdout/stderr pipes break
  9. Any subsequent write to stdout/stderr triggers EPIPE / unhandled error
  10. Update subprocess crashes before runRestartScript is called in maybeRestartService
  11. Service stays unloaded — no restart script, no recovery

3. The restart script is spawned too late

The detached restart script (which could recover the service) is spawned in maybeRestartService after refreshGatewayServiceEnv completes. But the update subprocess crashes during refreshGatewayServiceEnv (or shortly after, when any code path writes to stdout/stderr). The restart script never gets a chance to run.

Evidence

  • Confirmed on macOS 26.4 (arm64), Node 22.22.0, OpenClaw 2026.3.28 → 2026.3.31
  • The same openclaw update --yes --channel stable --json command works perfectly when run from a standalone process (not a child of the gateway)
  • All launchctl operations (bootout, bootstrap, kickstart) work correctly in isolation
  • openclaw gateway install --force succeeds when run independently
  • The ONLY failure mode is when the update runs as a child of the process being bootout'd

Workaround

Disable built-in auto-update and run updates from a standalone launchd job:

// openclaw.json
"update": { "auto": { "enabled": false } }
<!-- ~/Library/LaunchAgents/ai.openclaw.auto-update.plist -->
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>ai.openclaw.auto-update</string>
    <key>ProgramArguments</key>
    <array>
      <string>/opt/homebrew/opt/node@22/bin/node</string>
      <string>/opt/homebrew/lib/node_modules/openclaw/dist/entry.js</string>
      <string>update</string>
      <string>--yes</string>
      <string>--channel</string>
      <string>stable</string>
    </array>
    <key>StartInterval</key>
    <integer>3600</integer>
    <key>EnvironmentVariables</key>
    <dict>
      <key>HOME</key>
      <string>/Users/you</string>
      <key>PATH</key>
      <string>/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
      <key>OPENCLAW_LAUNCHD_LABEL</key>
      <string>ai.openclaw.gateway</string>
    </dict>
  </dict>
</plist>

Suggested Fix

Several possible approaches (not mutually exclusive):

  1. Skip refreshGatewayServiceEnv when running as auto-update child — detect via OPENCLAW_AUTO_UPDATE=1 env var, defer all restart logic to the detached restart script only
  2. Spawn the restart script before refreshGatewayServiceEnv — ensure the recovery mechanism exists before bootout can kill the parent
  3. Use launchctl stop or launchctl kickstart -k instead of bootout in activateLaunchAgent — keeps the service registered so KeepAlive can recover it (see PR #43652)
  4. Detach the update subprocess from the gateway's pipes — use stdio: ['ignore', 'ignore', 'ignore'] or write to a temp file instead of piping, so the parent dying doesn't crash the child

Related Issues

  • #54861 — launchd removes service after rapid restart cycle (downstream symptom)
  • #57379 — plist race condition kills gateway for 9+ hours (downstream symptom)
  • #58041 — config/plugin version mismatch causes crash loops (related but distinct)
  • #40811 — gateway entrypoint mismatch after update (related refreshGatewayServiceEnv issue)
  • #48992 — LaunchAgent not updated on non-git installs (related)

Environment

  • OS: macOS 26.4 (arm64, Mac Mini)
  • Node: 22.22.0
  • OpenClaw: 2026.3.28 → 2026.3.31
  • Install method: npm global
  • Service manager: launchd (LaunchAgent)

extent analysis

TL;DR

The most likely fix involves modifying the auto-update process to avoid killing the gateway on macOS by detaching the update subprocess from the gateway's pipes or skipping the refreshGatewayServiceEnv step when running as an auto-update child.

Guidance

  1. Review the resolveCommandStdio function: Consider modifying it to not always pipe stdout/stderr, allowing for more flexibility in handling subprocess output.
  2. Implement one of the suggested fixes: Choose from skipping refreshGatewayServiceEnv when running as an auto-update child, spawning the restart script before refreshGatewayServiceEnv, using launchctl stop or launchctl kickstart -k instead of bootout, or detaching the update subprocess from the gateway's pipes.
  3. Test the workaround: Disable built-in auto-update and run updates from a standalone launchd job to verify if the issue is resolved.

Example

// Example of detaching the update subprocess from the gateway's pipes
function runAutoUpdateCommand() {
  // ...
  const updateProcess = spawn('openclaw', ['update', '--yes', '--channel', 'stable'], {
    stdio: ['ignore', 'ignore', 'ignore'] // Detach from gateway pipes
  });
  // ...
}

Notes

  • The provided workaround and suggested fixes are specific to the described environment (macOS, Node 22.22.0, OpenClaw 2026.3.28 → 2026.3.31) and may not apply universally.
  • Testing and verification are crucial to ensure the chosen solution works as expected in different scenarios.

Recommendation

Apply the workaround by disabling built-in auto-update and running updates from a standalone launchd job, as this approach has been confirmed to work in the described environment. This allows for a reliable update process without interfering with the gateway service.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING