Hmbown/CodeWhale · error · anyhow::Error

read-only command must name a bare allowlisted executable

Error message

read-only command must name a bare allowlisted executable

What it means

Read-only dispatch requires the program token to be a bare executable name — exactly one path component (shell.rs:3893) — so the resolver controls lookup and can exclude the workspace from the search path. Any token containing a separator or parent component (`./git`, `bin/ls`, `/usr/bin/git`, `../rg`) is refused before resolution starts; this is deliberate, because a caller-chosen path would bypass the anti-shadowing resolution entirely.

Source

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

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 0c42157ee5)

Solutions

  1. Use the bare program name: `git log`, not `/usr/bin/git log`
  2. Ensure the needed tool is installed on PATH outside the workspace (system-wide)
  3. For workspace-local tools, use a full-permission shell call with approval instead of the read-only path
  4. Lint generated commands for path-qualified program tokens before dispatch

Example fix

# before: path-qualified program token
/usr/bin/git log --oneline
# after: bare name resolved through the sanitized PATH
git log --oneline
Defensive patterns

Strategy: validation

Validate before calling

fn is_bare_program(token: &str) -> bool {
    std::path::Path::new(token).components().count() == 1
}

let mut argv = shell_words::split(command)?;
if !is_bare_program(&argv[0]) {
    return report(format!("use a bare program name, not {token:?}", token = argv[0]));
}

Type guard

fn bare_program_token(command: &str) -> bool {
    shell_words::split(command)
        .ok()
        .and_then(|argv| argv.first().cloned())
        .is_some_and(|program| std::path::Path::new(&program).components().count() == 1)
}

Try / catch

if let Err(err) = run_readonly_command(&command) {
    if err.to_string().contains("bare allowlisted executable") {
        return report("strip the path prefix from the program and retry with a bare name");
    }
    return Err(err);
}

Prevention

When it happens

Trigger: A classifier-admitted read command whose program token is path-qualified, e.g. `/usr/bin/git log` or `./rg pattern`, reaching `resolve_readonly_program_from_path` on the readonly exec branch.

Common situations: Models emitting absolute tool paths to be 'explicit'; wrappers that prefix `./` for repo-local binaries; hardened environments where users alias tools via paths.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/404de4f8cace90d8. Report an issue: GitHub.