{"record":{"id":"1f453ff41934b3d0","repo":"Hmbown/CodeWhale","slug":"no-trusted-executable-search-path-remains-outside","errorCode":null,"errorMessage":"no trusted executable search path remains outside the workspace","messagePattern":"no trusted executable search path remains outside the workspace","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/shell.rs","lineNumber":3899,"sourceCode":"fn 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)]\n            {\n                use std::os::unix::fs::PermissionsExt as _;\n                if candidate.metadata()?.permissions().mode() & 0o111 == 0 {\n                    continue;\n                }","sourceCodeStart":3881,"sourceCodeEnd":3917,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/shell.rs#L3881-L3917","documentation":"For read-only execution, PATH is sanitized: relative entries, entries that fail to canonicalize, and entries resolving inside the workspace are all dropped — this is what stops workspace-planted binaries from shadowing system tools (see the shadow test at crates/tui/src/tools/shell/tests.rs:801). This error means nothing survived the filter, so there is no trusted location left to resolve any program from, and the command is refused.","triggerScenarios":"PATH containing only the workspace directory (or only relative/broken entries): devcontainer or Nix setups that put just project-local bin dirs on PATH; scrubbed environments whose sole entry is the repo; entries pointing at deleted directories plus one workspace entry.","commonSituations":"Project-local toolchains that replace PATH entirely; sandbox configs that reduce PATH to the repo; misconstructed test environments.","solutions":["Prepend standard system directories (/usr/bin, /usr/local/bin, /bin) to PATH outside the workspace","Remove broken or relative PATH entries so the filter keeps at least one trusted directory","Install the allowlisted tools (git, gh, rg) system-wide rather than only inside the workspace","Validate the sanitized PATH at startup and report the environment problem early"],"exampleFix":"# before: PATH only covers the workspace\nPATH=$PWD/node_modules/.bin codewhale tui\n# after: system dirs first so a trusted path survives sanitization\nPATH=/usr/local/bin:/usr/bin:/bin:$PWD/node_modules/.bin codewhale tui","handlingStrategy":"validation","validationCode":"fn sanitized_path_survives(workspace: &std::path::Path) -> bool {\n    let Some(path) = std::env::var_os(\"PATH\") else { return false };\n    std::env::split_paths(&path).any(|entry| {\n        entry.is_absolute()\n            && entry\n                .canonicalize()\n                .map(|resolved| !resolved.starts_with(workspace))\n                .unwrap_or(false)\n    })\n}\n\nif !sanitized_path_survives(&workspace) {\n    return report(\"PATH has no trusted directories outside the workspace; add /usr/bin:/bin\");\n}","typeGuard":"fn trusted_path_available(workspace: &std::path::Path) -> bool {\n    std::env::var_os(\"PATH\").is_some_and(|path| {\n        std::env::split_paths(&path).any(|entry| {\n            entry.is_absolute()\n                && entry\n                    .canonicalize()\n                    .map(|resolved| !resolved.starts_with(workspace))\n                    .unwrap_or(false)\n        })\n    })\n}","tryCatchPattern":"if let Err(err) = run_readonly_command(&command) {\n    if err.to_string().contains(\"no trusted executable search path remains\") {\n        return report(\"prepend system bin dirs (/usr/bin:/usr/local/bin:/bin) to PATH outside the workspace and retry\");\n    }\n    return Err(err);\n}","preventionTips":["Keep standard system directories on PATH ahead of project-local entries","Remove broken/relative PATH entries so canonicalize does not drop them","Install allowlisted tools (git, gh, rg) outside the workspace, system-wide","Validate the sanitized PATH at startup — the filter deliberately discards workspace entries to stop shadow executables"],"tags":["readonly-shell","path","environment","security","rust"],"backgroundTag":"executable-not-found-on-path","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}