sinelaw/fresh · error

Failed to spawn shell

Error message

Failed to spawn shell: {}

What it means

run_shell_command_blocking pauses the TUI and spawns the user's default shell (detect_shell) with `sh -c <command>` to run a shell command synchronously. If Command::spawn fails — shell binary missing, not executable, or OS resource limits — this error reports the underlying io::Error.

Solutions

  1. Check the wrapped io::Error in the message (NotFound => shell missing, PermissionDenied => chmod +x or pick another shell)
  2. Verify the shell resolved by detect_shell() exists: echo $SHELL and which <shell>
  3. Set $SHELL to an existing shell or install one in the environment/container
  4. Add a fallback in detect_shell() to /bin/sh when the detected shell cannot be spawned

Example fix

// before
let shell = detect_shell();
let mut child = Command::new(&shell).args(["-c", command]).hide_window().spawn()
    .map_err(|e| anyhow::anyhow!("Failed to spawn shell: {}", e))?;
// after
let shell = detect_shell();
let mut child = Command::new(&shell).args(["-c", command]).hide_window().spawn()
    .map_err(|e| anyhow::anyhow!("Failed to spawn shell '{}': {} (check $SHELL and PATH)", shell.display(), e))?;
Defensive patterns

Strategy: validation

Validate before calling

let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into());
if which::which(&shell).is_err() {
    // fall back to /bin/sh before invoking the command
}

Try / catch

match run_shell_command_blocking(cmd) {
    Err(e) if e.to_string().starts_with("Failed to spawn shell") => {
        show_error(format!("{e}; check $SHELL exists and is executable"));
    }
    other => other?,
}

Prevention

When it happens

Trigger: detect_shell() resolves to a binary that doesn't exist or isn't executable (bad $SHELL, minimal container without bash); fork/exec fails due to process/file-descriptor limits; PATH lacks the shell.

Common situations: Running the editor inside minimal Docker images lacking a shell; $SHELL pointing to a removed binary; sandboxed environments (flatpak/nix) where the shell isn't on PATH; ulimit restrictions.

Related errors


AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13). Data as JSON: /api/errors/8e0462552a331881. Report an issue: GitHub.

Appendix: source

Thrown at crates/fresh-editor/src/app/shell_command.rs:268

    pub(crate) fn run_shell_command_blocking(&mut self, command: &str) -> anyhow::Result<()> {
        use crossterm::terminal::{
            disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
        };
        use crossterm::ExecutableCommand;
        use std::io::stdout;

        // Suspend TUI — best-effort, nothing useful to do on failure.
        #[allow(clippy::let_underscore_must_use)]
        let _ = disable_raw_mode();
        #[allow(clippy::let_underscore_must_use)]
        let _ = stdout().execute(LeaveAlternateScreen);

        let shell = detect_shell();
        let mut child = Command::new(&shell)
            .args(["-c", command])
            .hide_window()
            .spawn()
            .map_err(|e| anyhow::anyhow!("Failed to spawn shell: {}", e))?;

        let status = child
            .wait()
            .map_err(|e| anyhow::anyhow!("Failed to wait for command: {}", e))?;

        // Resume TUI — best-effort, nothing useful to do on failure.
        #[allow(clippy::let_underscore_must_use)]
        let _ = stdout().execute(EnterAlternateScreen);
        #[allow(clippy::let_underscore_must_use)]
        let _ = enable_raw_mode();

        // Request a full hard redraw to clear any ghost text from the external command
        self.request_full_redraw();

        if status.success() {
            Ok(())
        } else {
            anyhow::bail!("Command failed with exit code: {:?}", status.code())

View on GitHub (pinned to 67894ca546)