Hmbown/CodeWhale · error · anyhow::Error

could not parse classifier-approved read command: {error}

Error message

could not parse classifier-approved read command: {error}

What it means

On the agent read-only path (ShellPolicy::ReadOnly), a command first passes the classifier `is_agent_readonly_shell_command`, then `hardened_readonly_argv` re-tokenizes it with the `shell_words` crate to build a literal argv (shell.rs:2008). This error means `shell_words::split` rejected the string — typically an unmatched quote or dangling escape — i.e. the classifier's tokenizer and the crate disagree. It is a fail-closed defense-in-depth check: the command never runs.

Source

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

    {
        return false;
    }
    if ["task_id", "id", "wait", "block", "close_stdin", "all"]
        .iter()
        .any(|key| input.get(*key).is_some())
    {
        return false;
    }

    input
        .get("command")
        .and_then(serde_json::Value::as_str)
        .is_some()
}

fn hardened_readonly_argv(command: &str) -> Result<(String, Vec<String>)> {
    let mut argv = shell_words::split(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 0c42157ee5)

Solutions

  1. Fix the quoting in the command (balance single/double quotes, remove stray backslashes) and retry
  2. Simplify to a plainly-quoted, pipeline-free invocation the hardener can tokenize
  3. If the command looks syntactically valid, report the classifier/hardener mismatch as a bug against the shell tool
  4. Use the bounded File read/search tools for patterns that are awkward to quote

Example fix

# before: unterminated quote admitted by the classifier
git log --grep="open
# after: balanced quoting
git log --grep="open"
Defensive patterns

Strategy: validation

Validate before calling

fn tokenize_cleanly(command: &str) -> bool {
    shell_words::split(command).is_ok()
}

if !tokenize_cleanly(command) {
    return report(format!("fix the quoting in: {command}"));
}

Type guard

fn parseable_readonly_command(command: &str) -> bool {
    shell_words::split(command).map(|argv| !argv.is_empty()).unwrap_or(false)
}

Try / catch

let (program, args) = match hardened_readonly_argv(command) {
    Ok(parsed) => parsed,
    Err(err) if err.to_string().starts_with("could not parse") => {
        return report(format!("re-quote the command and retry: {command}"));
    }
    Err(err) => return Err(err),
};

Prevention

When it happens

Trigger: Executing a classifier-admitted, pipeline-free read command containing an unbalanced quote or trailing backslash (e.g. `rg 'pattern` or `git log --grep="foo`), which reaches the `hardened_readonly_argv` call in the non-pipe branch of exec.

Common situations: Model-generated commands with mangled quoting that still pass the classifier's charset/token filters; drift between the classifier's own tokenizer and the shell_words crate after an upgrade.

Related errors


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