Hmbown/CodeWhale · error

read-only command must name a bare allowlisted executable

Error message

read-only command must name a bare allowlisted executable

What it means

`resolve_readonly_program_from_path` only resolves bare executable names for the read-only shell. If the command's program token contains a path separator (any component count other than 1, e.g. `./script.sh`, `/usr/bin/git`, `../bin/tool`), it is rejected so that a classifier-approved command cannot smuggle in an arbitrary or workspace-relative binary through the program position.

Solutions

  1. Invoke the tool by bare name (`tsc`, `ls`) and let the resolver find it on the sanitized PATH
  2. Install the needed binary into a standard directory on PATH instead of calling it by path
  3. If the tool is genuinely needed by path, use the normal permission-prompted shell path rather than the read-only fast path

Example fix

// before
run_readonly("./node_modules/.bin/tsc --version")
// after
run_readonly("tsc --version")
Defensive patterns

Strategy: validation

Validate before calling

fn program_is_bare(cmd: &str) -> bool {
    shell_words::split(cmd).ok()
        .and_then(|argv| argv.first().cloned())
        .map(|p| std::path::Path::new(&p).components().count() == 1)
        .unwrap_or(false)
}
if !program_is_bare(&cmd) { eprintln!("use a bare executable name on PATH"); }

Prevention

When it happens

Trigger: Running a read-only command whose first token is a relative or absolute path: `./node_modules/.bin/tsc --version`, `/bin/ls`, `subdir/tool arg`. Any separator in the program token triggers the error before PATH lookup begins.

Common situations: Agents or scripts emitting absolute paths for well-known tools; invoking project-local binaries like `./gradlew`; Windows-style paths surviving `normalize_windows_command_paths`; users pasting shell commands with explicit paths.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/404de4f8cace90d8. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/tools/shell.rs:4391

fn readonly_sanitized_path(workspace: &std::path::Path) -> Option<String> {
    let path = std::env::var_os("PATH")?;
    readonly_sanitized_path_from(workspace, &path).map(|value| value.to_string_lossy().into_owned())
}

fn resolve_readonly_program(program: &str, workspace: &std::path::Path) -> Result<PathBuf> {
    let path = std::env::var_os("PATH")
        .ok_or_else(|| anyhow!("no executable search path is configured"))?;
    resolve_readonly_program_from_path(program, workspace, &path)
}

fn resolve_readonly_program_from_path(
    program: &str,
    workspace: &std::path::Path,
    path: &std::ffi::OsStr,
) -> Result<PathBuf> {
    let workspace = workspace.canonicalize()?;
    if std::path::Path::new(program).components().count() != 1 {
        return Err(anyhow!(
            "read-only command must name a bare allowlisted executable"
        ));
    }
    let safe_path = readonly_sanitized_path_from(&workspace, path).ok_or_else(|| {
        anyhow!("no trusted executable search path remains outside the workspace")
    })?;
    let names = if cfg!(windows) {
        vec![format!("{program}.exe"), format!("{program}.com")]
    } else {
        vec![program.to_string()]
    };
    for directory in std::env::split_paths(&safe_path) {
        for name in &names {
            let candidate = directory.join(name);
            if !candidate.is_file() {
                continue;
            }
            #[cfg(unix)]

View on GitHub (pinned to 73e0f67d83)