zeroclaw-labs/zeroclaw · error · anyhow::Error

daemon child exited with status {status}

Error message

daemon child exited with status {status}

What it means

On macOS the daemon is spawned as a child process under launchd capture (run_with_launchd_capture -> supervise_launchd_child). After stdout/stderr pipes are drained, the supervisor waits for the child and checks its exit status; a non-zero exit raises this error. It means the zeroclaw daemon process itself terminated abnormally while being supervised, not that launchd failed.

Source

Thrown at crates/zeroclaw-runtime/src/service/mod.rs:407

    let stderr = child
        .stderr
        .take()
        .context("daemon stderr pipe unavailable")?;
    let stdout_sink = writers.stdout.clone();
    let stderr_sink = writers.stderr.clone();
    let stdout_task = zeroclaw_spawn::spawn!(drain_launchd_pipe(stdout, stdout_sink));
    let stderr_task = zeroclaw_spawn::spawn!(drain_launchd_pipe(stderr, stderr_sink));

    #[cfg(any(target_os = "macos", all(test, unix)))]
    let outcome = wait_for_launchd_child(&mut child, &mut signals).await;
    #[cfg(all(test, not(unix)))]
    let outcome = wait_for_launchd_child(&mut child).await;
    finish_launchd_pipes(stdout_task, stderr_task).await;
    let status = outcome?;
    if status.success() {
        Ok(())
    } else {
        bail!("daemon child exited with status {status}")
    }
}

#[cfg(any(target_os = "macos", test))]
async fn finish_launchd_pipes(
    mut stdout: tokio::task::JoinHandle<()>,
    mut stderr: tokio::task::JoinHandle<()>,
) {
    if tokio::time::timeout(LAUNCHD_PIPE_DRAIN_TIMEOUT, async {
        let _ = tokio::join!(&mut stdout, &mut stderr);
    })
    .await
    .is_err()
    {
        stdout.abort();
        stderr.abort();
        let _ = tokio::join!(stdout, stderr);
    }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Run the daemon in the foreground first (e.g. 'zeroclaw run') to see the real startup error and fix it
  2. Check 'zeroclaw service logs' (or the launchd StandardOutPath/StandardErrorPath files) for the crash reason
  3. Validate the config file and required keys, then retry 'zeroclaw service start'
  4. Reinstall with 'zeroclaw service install' if the plist points at a moved or deleted binary

Example fix

// before: start the service blind
service::start(&config, InitSystem::Auto)?;

// after: smoke-test the daemon in the foreground, then start it
// $ zeroclaw run   (fix whatever error it prints, then)
service::start(&config, InitSystem::Auto)?;
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight: confirm the daemon binary itself launches before supervising it
let probe = Command::new(std::env::current_exe()?).arg("--version").status()?;
if !probe.success() {
    // do not attempt service start; surface the environment problem first
}

Try / catch

match service::start(&config, InitSystem::Auto) {
    Ok(()) => Ok(()),
    Err(err) if err.to_string().starts_with("daemon child exited with status") => {
        // daemon crashed at startup: surface the logs instead of a raw status code
        let _ = service::logs(&config, InitSystem::Auto, 100, false);
        Err(err)
    }
    Err(err) => Err(err),
}

Prevention

When it happens

Trigger: Calling the macOS service start path that ends in supervise_launchd_child when the daemon binary exits non-zero: invalid zeroclaw.toml, missing provider API key, port already bound, or an immediate crash on startup.

Common situations: Fresh install with a broken config file; upgrade to a version with breaking config changes; API credentials missing from the environment the launchd agent runs in; stale binary path in the plist after an upgrade.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/7364f826374f5261. Report an issue: GitHub.