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
- chmod +x <shell-path> and retry.
- If the value is your own wrapper, restore its exec bit in whatever provisions it (install -m 755).
- If the file sits on a noexec mount, move it (or remount with exec) — the bit alone will not make spawn succeed.
- 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
- Provision wrapper scripts with install -m 755, not by writing a file.
- Preserve modes when copying shells (cp -p) and extracting archives.
- Remember the check is coarse: an exec bit on a noexec mount still fails at spawn.
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
- runtime.shell {shell:?} (resolved to {}) could not be inspec
- runtime.shell must not be empty or whitespace
- runtime.shell {shell:?} is a relative path; use a bare name
- runtime.shell {shell:?} was not found on PATH; use an absolu
- runtime.shell {shell:?} (resolved to {}) does not exist
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/b9176ee2e5340304.
Report an issue: GitHub.