Hmbown/CodeWhale · error
could not parse classifier-approved read command
Error message
could not parse classifier-approved read command: {error} What it means
hardened_readonly_argv tokenizes the classifier-approved command using shell_words::split (POSIX shell word splitting) after Windows path normalization. If the command string is not valid POSIX shell syntax for splitting (unbalanced quotes, trailing backslash, etc.), the tool cannot safely decompose it into argv and rejects it.
Solutions
- Re-issue the command with balanced, properly escaped quoting — wrap arguments containing spaces or special characters in double quotes.
- Remove problematic characters (apostrophes, trailing backslashes) from arguments, or use simpler glob-free arguments.
- If this comes from model output, retry the request so the model regenerates a syntactically valid command.
Example fix
// before (unbalanced quote) git log --format='%h %s it's a commit | head // after git log --format="%h %s" | head
Defensive patterns
Strategy: validation
Validate before calling
function quotesBalanced(cmd: string): boolean {
let s = false, d = false;
for (let i = 0; i < cmd.length; i++) {
const c = cmd[i];
if (c === '\\') { i++; continue; }
if (c === "'" && !d) s = !s;
if (c === '"' && !s) d = !d;
}
return !s && !d && !cmd.trimEnd().endsWith('\\');
} Prevention
- Prefer double quotes around arguments that contain apostrophes or spaces.
- Avoid trailing backslashes and unbalanced quotes in generated commands.
- Sanitize interpolated user text before embedding it in command strings.
When it happens
Trigger: A pipeline segment contains shell syntax shell_words::split rejects: unbalanced single/double quotes, an unterminated escape like 'foo\', or other malformed quoting in the command the classifier approved.
Common situations: The agent model emits a command with a stray apostrophe (e.g. git log --format='%h | it's fine'); generated commands interpolating user text with quotes; Windows-style paths with backslashes misinterpreted as escapes.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- classifier-approved read command was empty
- pipeline contains a command outside the read-only policy
- read-only pipelines require bash or zsh; run each read…
- agent profile may not request allow_shell=true
- allowlisted read-only executable
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/9378c0e23eb42122.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/tools/shell.rs:4188
let program = resolve_readonly_program(&program, workspace)?;
let program = program
.to_str()
.ok_or_else(|| anyhow!("read-only executable path is not valid UTF-8"))?;
Ok(std::iter::once(program)
.chain(args.iter().map(String::as_str))
.map(|arg| shell_words::quote(arg).into_owned())
.collect::<Vec<_>>()
.join(" "))
})
.collect::<Result<Vec<_>>>()?;
// The only shell operators are our pipes. Filenames cannot expand into
// options or unvalidated symlinks; every Git stage retains helper guards.
Ok(format!("set -o pipefail; {}", segments.join(" | ")))
}
fn hardened_readonly_argv(command: &str) -> Result<(String, Vec<String>)> {
let mut argv = shell_words::split(&normalize_windows_command_paths(command))
.map_err(|error| anyhow!("could not parse classifier-approved read command: {error}"))?;
if argv.is_empty() {
return Err(anyhow!("classifier-approved read command was empty"));
}
// Even when repository/user configuration names a diff or signature
// helper, these flags make Git keep the read inside its own process.
if argv.first().is_some_and(|program| program == "git") {
// The agent read-only classifier admits `git -C <dir>` and
// `git --no-pager` before the subcommand; keep the preamble but
// locate the subcommand after it so the hardening flags splice in
// the right place. `-C` targets were already workspace-checked by
// `enforce_readonly_workspace_operands`.
let mut subcommand_index = 1;
while let Some(flag) = argv.get(subcommand_index) {
match flag.as_str() {
"--no-pager" => subcommand_index += 1,
"-C" => subcommand_index += 2,
_ => break,View on GitHub (pinned to 73e0f67d83)