{"record":{"id":"404de4f8cace90d8","repo":"Hmbown/CodeWhale","slug":"read-only-command-must-name-a-bare-allowlisted-exe","errorCode":null,"errorMessage":"read-only command must name a bare allowlisted executable","messagePattern":"read-only command must name a bare allowlisted executable","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/shell.rs","lineNumber":3894,"sourceCode":"fn readonly_sanitized_path(workspace: &std::path::Path) -> Option<String> {\n    let path = std::env::var_os(\"PATH\")?;\n    readonly_sanitized_path_from(workspace, &path).map(|value| value.to_string_lossy().into_owned())\n}\n\nfn resolve_readonly_program(program: &str, workspace: &std::path::Path) -> Result<PathBuf> {\n    let path = std::env::var_os(\"PATH\")\n        .ok_or_else(|| anyhow!(\"no executable search path is configured\"))?;\n    resolve_readonly_program_from_path(program, workspace, &path)\n}\n\nfn resolve_readonly_program_from_path(\n    program: &str,\n    workspace: &std::path::Path,\n    path: &std::ffi::OsStr,\n) -> Result<PathBuf> {\n    let workspace = workspace.canonicalize()?;\n    if std::path::Path::new(program).components().count() != 1 {\n        return Err(anyhow!(\n            \"read-only command must name a bare allowlisted executable\"\n        ));\n    }\n    let safe_path = readonly_sanitized_path_from(&workspace, path).ok_or_else(|| {\n        anyhow!(\"no trusted executable search path remains outside the workspace\")\n    })?;\n    let names = if cfg!(windows) {\n        vec![format!(\"{program}.exe\"), format!(\"{program}.com\")]\n    } else {\n        vec![program.to_string()]\n    };\n    for directory in std::env::split_paths(&safe_path) {\n        for name in &names {\n            let candidate = directory.join(name);\n            if !candidate.is_file() {\n                continue;\n            }\n            #[cfg(unix)]","sourceCodeStart":3876,"sourceCodeEnd":3912,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/shell.rs#L3876-L3912","documentation":"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.","triggerScenarios":"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.","commonSituations":"Models emitting absolute tool paths to be 'explicit'; wrappers that prefix `./` for repo-local binaries; hardened environments where users alias tools via paths.","solutions":["Use the bare program name: `git log`, not `/usr/bin/git log`","Ensure the needed tool is installed on PATH outside the workspace (system-wide)","For workspace-local tools, use a full-permission shell call with approval instead of the read-only path","Lint generated commands for path-qualified program tokens before dispatch"],"exampleFix":"# before: path-qualified program token\n/usr/bin/git log --oneline\n# after: bare name resolved through the sanitized PATH\ngit log --oneline","handlingStrategy":"validation","validationCode":"fn is_bare_program(token: &str) -> bool {\n    std::path::Path::new(token).components().count() == 1\n}\n\nlet mut argv = shell_words::split(command)?;\nif !is_bare_program(&argv[0]) {\n    return report(format!(\"use a bare program name, not {token:?}\", token = argv[0]));\n}","typeGuard":"fn bare_program_token(command: &str) -> bool {\n    shell_words::split(command)\n        .ok()\n        .and_then(|argv| argv.first().cloned())\n        .is_some_and(|program| std::path::Path::new(&program).components().count() == 1)\n}","tryCatchPattern":"if let Err(err) = run_readonly_command(&command) {\n    if err.to_string().contains(\"bare allowlisted executable\") {\n        return report(\"strip the path prefix from the program and retry with a bare name\");\n    }\n    return Err(err);\n}","preventionTips":["Emit bare program names (`git`, `rg`) — never absolute or `./`-relative tool paths","Block path-qualified program tokens in command generation/linting","Install required tools system-wide so bare names resolve","Remember path-qualified programs would bypass the anti-shadowing PATH filter — the refusal is intentional"],"tags":["readonly-shell","command-resolution","allowlist","security","rust"],"backgroundTag":"path-qualified-command-rejected","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}