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

runtime.shell {shell:?} (resolved to {}) is not executable

Error message

runtime.shell {shell:?} (resolved to {}) is not executable

What it means

The final validate_shell check on unix: the resolved file must have at least one execute bit set (mode & 0o111). The source comment is explicit that this is deliberately coarse — it rejects 'nobody can execute' rather than predicting whether the current user can; the kernel's spawn remains the real authority on ACLs and caps.

Source

Thrown at crates/zeroclaw-config/src/platform/mod.rs:87

        Err(e) => anyhow::bail!(
            "runtime.shell {shell:?} (resolved to {}) could not be inspected: {e}",
            resolved.display()
        ),
    };
    if !metadata.is_file() {
        anyhow::bail!(
            "runtime.shell {shell:?} (resolved to {}) is not a regular file",
            resolved.display()
        );
    }

    // Coarse check: reject only when no execute bit is set at all. A precise
    // "can *we* execute it" test (uid/gid vs. the file owner) buys little —
    // the kernel's spawn is the real authority (ACLs, caps, mount flags) — and
    // this is a fail-fast sanity check, not a security gate.
    let mode = metadata.permissions().mode();
    if mode & 0o111 == 0 {
        anyhow::bail!(
            "runtime.shell {shell:?} (resolved to {}) is not executable",
            resolved.display()
        );
    }

    Ok(())
}

/// Validate a configured `runtime.shell` on Windows.
///
/// Unlike the Unix check this does not resolve a binary on `PATH`: on Windows
/// `runtime.shell` selects the interpreter family (`cmd.exe` vs PowerShell),
/// and the interpreter is located at spawn time. The only fail-fast condition
/// worth catching up front is an empty/whitespace value, which would otherwise
/// spawn with no program.
#[cfg(windows)]
fn validate_shell_windows(shell: &str) -> anyhow::Result<()> {
    if shell.trim().is_empty() {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. chmod +x <shell-path> and retry.
  2. If the value is your own wrapper, restore its exec bit in whatever provisions it (install -m 755).
  3. If the file sits on a noexec mount, move it (or remount with exec) — the bit alone will not make spawn succeed.
  4. Or point runtime.shell at a system shell that is already executable.

Example fix

# before: -rw-r--r-- /opt/wrappers/sh
$ zeroclaw ...  # error: not executable

# after
$ chmod +x /opt/wrappers/sh
Defensive patterns

Strategy: validation

Validate before calling

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

fn has_exec_bit(p: &std::path::Path) -> bool {
    std::fs::metadata(p)
        .map(|m| m.permissions().mode() & 0o111 != 0)
        .unwrap_or(false)
}

Try / catch

match create_runtime(&config) {
    Err(e) if e.to_string().contains("is not executable") => {
        // chmod +x the resolved path (watch for noexec mounts) and retry
    }
    other => other,
}

Prevention

When it happens

Trigger: runtime.shell resolves to a file with mode 644 (rw-r--r--) — a copied binary or wrapper script that lost its exec bit — or any file where no execute bit is set for user, group, or other.

Common situations: Copying a shell binary or wrapper script without preserving modes; extracting archives that drop permissions; wrapper scripts created by echo instead of install -m +x; cloud-synced files restored without their exec bit.

Related errors


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