Add support for OPENCLAW_ERROR_HANDLER — an environment variable that lets users route OpenClaw's fatal error diagnostics to an external executable as a structured JSON payload.
Intent:
- Non-blocking external handler for fatal errors (uncaught exceptions, CLI failures)
- Passes structured but redacted error context (
schemaVersion,reason,timestamp,pid) as a single JSON argv argument - Zero new dependencies,
shell: falsefor command injection safety,detached+unref()for non-blocking shutdown
What is intentionally out of scope:
- Not a replacement for the existing
registerFatalErrorHookplugin API (internal hooks still preferred for bundled diagnostics) - Not a daemon or watchdog — fire-and-forget execution only
- Not a crash-dump collector — the payload is intentionally redacted to protect process-argv visibility. Operators who need full stack details should use OpenClaw's existing stability-bundle mechanism.
What does success look like:
- Users can set
OPENCLAW_ERROR_HANDLER="/usr/bin/logger"and see structured error metadata in syslog - Users can point it at a custom script to POST alerts to their own notification pipeline
- OpenClaw's exit path remains deterministic — handler failure never blocks shutdown
| Revision | Change |
|---|---|
| v1 (initial proposal) | shell: true + stdin pipe + RAW fields. Blocking: shell injection via env var + stdin flush race with process.exit(). |
| v2 (audited) | shell: false + argv[1] delivery + RAW opt-in (OPENCLAW_ERROR_HANDLER_RAW=1). Resolved injection and flush race. 7-field extended payload behind opt-in gate. |
| v3 (current) | Removed RAW=1 entirely. Payload fixed to 4 non-sensitive fields (schemaVersion, reason, timestamp, pid). Rationale: (1) argv visibility — 4-field payload contains no sensitive diagnostic data; (2) untestable — RAW path couldn't be verified without a full OpenClaw build (≥12GB RAM); (3) simplicity — removed ~15 lines of conditional branching; (4) extensibility — RAW opt-in can be added via follow-up PR if community requests it. |
OpenClaw currently has no standard mechanism for users to hook an external command into the fatal-error lifecycle. The existing registerFatalErrorHook API is internal/bundled — operators who want to route crash diagnostics to their own alerting (syslog, webhook, custom script) have no zero-dep entry point.
This PR provides that entry point via an environment variable, following the same precedent as OPENCLAW_GATEWAY_STARTUP_TRACE (docs/cli/gateway.md:132).
- OS: WSL2 (Debian 12, kernel 6.6.87.2-microsoft-standard-WSL2) under Windows 11
- Runtime: Node.js v22.22.3
- Handler target:
/usr/bin/logger(syslog) - Payload schema:
{ schemaVersion: 1, reason: string, timestamp: ISO8601, pid: number }
node openclaw-fatal-hook-proof.mjs 2>&1The test script exercises the spawn() code path that runExternalErrorHandler uses, demonstrating handler invocation in a real WSL2 environment with the OpenClaw CLI installed.
Host: DESKTOP-H9EMUD9 | Platform: linux 6.6.87.2-microsoft-standard-WSL2 | Node: v22.22.3 | PID: 95243
### 1. openclaw CLI baseline
$ openclaw --version
OpenClaw 2026.6.6 (8c802aa)
### 2. Standard payload (4 fields: schemaVersion, reason, timestamp, pid)
$ spawn(/usr/bin/logger, [payload], { detached: true, shell: false })
→ handler invoked, payload delivered via argv[1] ✓
### 3. Nonexistent handler — graceful degrade
→ ENOENT swallowed, exit path unaffected ✓
### 4. shell:false — injection prevention
→ shell:false blocks injection, literal path ENOENT ✓
### 5. Syslog delivery
$ journalctl | grep schemaVersion
{"schemaVersion":1,"reason":"uncaught_exception","timestamp":"2026-06-16T03:43:52.734Z","pid":95243}
--- All scenarios passed ---
Every configured scenario passes:
- No env var → OpenClaw behavior unchanged (zero impact)
- Valid handler → payload written to syslog atomically via argv
- Invalid handler path → ENOENT swallowed by child
errorlistener, main process unaffected - Shell injection attempt →
shell: falseprevents command execution — the injected string is treated as a literal file path, fails with ENOENT - Exit race →
detached + unrefconfirmed: parent does not wait for handler completion
Full OpenClaw runtime integration (requires a build environment with ≥12GB RAM; the tsdown bundler OOMs at 11GB). The function under review — runExternalErrorHandler — is a ~30-line composition of stdlib calls with no dependencies on OpenClaw's runtime state. The spawn logic is identical whether called in isolation or from runFatalErrorHooks.
| Question | Answer |
|---|---|
| Did user-visible behavior change? | No — env var unset → zero change |
| Did config/environment behavior change? | Yes — new OPENCLAW_ERROR_HANDLER env var |
| Did security/auth/network behavior change? | No — shell: false prevents injection, handler is detached |
| Highest-risk area? | Environment variable sourced from untrusted input |
| How is that risk mitigated? | shell: false — handler must be a single executable path, no shell expansion. Documented in Security section. |
- Next action: Maintainer review
- Waiting on: CI, proof verification