aaif-goose/goose · critical

goose-cli main thread panicked

Error message

goose-cli main thread panicked

What it means

After spawning the main worker thread, main() joins it; handle.join() returns Err only when that thread panicked (crates/goose-cli/src/main.rs:51-53). The panic payload is discarded and replaced by this generic message, so the real cause is whatever panicked inside the thread — most commonly the `.expect("Failed to build Tokio runtime")` or any panic inside run().

Source

Thrown at crates/goose-cli/src/main.rs:52

fn main() -> Result<()> {
    #[cfg(windows)]
    enable_windows_vt_processing();

    let handle = std::thread::Builder::new()
        .name("goose-cli-main".to_string())
        .stack_size(8 * 1024 * 1024)
        .spawn(|| {
            let runtime = tokio::runtime::Builder::new_multi_thread()
                .enable_all()
                .build()
                .expect("Failed to build Tokio runtime");
            runtime.block_on(run())
        })
        .map_err(|e| anyhow::anyhow!("Failed to spawn goose-cli main thread: {}", e))?;

    handle
        .join()
        .map_err(|_| anyhow::anyhow!("goose-cli main thread panicked"))?
}

View on GitHub (pinned to 3810898a74)

Solutions

  1. Read stderr just above this error: the standard panic message ('thread ... panicked at ...') names the real failing location and payload
  2. Reproduce with RUST_BACKTRACE=1 to get a full backtrace of the panic
  3. Fix the underlying panic (often a runtime-build or configuration failure) rather than this join() wrapper

Example fix

// before (payload discarded)
handle
    .join()
    .map_err(|_| anyhow::anyhow!("goose-cli main thread panicked"))?

// after (surface the panic payload)
handle
    .join()
    .map_err(|payload| anyhow::anyhow!("goose-cli main thread panicked: {:?}", payload))?
Defensive patterns

Strategy: try-catch

Try / catch

// Rust: join() returns Result — surface the panic payload instead of discarding it
match handle.join() {
    Ok(result) => result,
    Err(payload) => {
        let reason = payload
            .downcast_ref::<&str>()
            .map(|s| s.to_string())
            .or_else(|| payload.downcast_ref::<String>().cloned())
            .unwrap_or_else(|| "unknown panic".into());
        anyhow::bail!("goose-cli main thread panicked: {reason}")
    }
}

Prevention

When it happens

Trigger: Any panic in the spawned thread: Tokio runtime construction failing (expected panic), or a panic/unwrap failure anywhere down the CLI's run() path (agent setup, provider config, etc.). The default panic hook prints the panic message to stderr before join() returns.

Common situations: A bug or unreachable state in the CLI path panicking at startup; a broken environment (e.g., invalid provider/model config causing an unwrap) that makes run() panic; the actual reason is visible in stderr just above this error line.

Related errors


AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16). Data as JSON: /api/errors/495c5e209a390205. Report an issue: GitHub.