Hmbown/CodeWhale · error · anyhow::Error

classifier-approved read command was empty

Error message

classifier-approved read command was empty

What it means

`hardened_readonly_argv` requires at least one token after `shell_words` splitting. Zero tokens means the command was empty, whitespace-only, or only a shell comment (shell_words strips `#` comments by default). The readonly classifier is expected to reject such input upstream, so reaching this error means an effectively-empty command slipped through classification — the path fails closed rather than executing anything.

Source

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

    }
    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. Skip execution entirely when the command trims to empty or starts with '#'
  2. Validate a non-empty command field at the tool-input schema before dispatch
  3. If a non-empty command still reaches this check, the comment-stripping behavior is the likely cause — remove leading '#' commentary and retry
  4. Report as a classifier gap if a meaningful command was intended

Example fix

# before: comment-only command dispatched to the read-only path
{"action": "run", "command": "# check status"}
# after: a real read command
{"action": "run", "command": "git status"}
Defensive patterns

Strategy: validation

Validate before calling

let trimmed = command.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
    return report("skip execution: command is empty or comment-only");
}

Type guard

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

Try / catch

match hardened_readonly_argv(command) {
    Ok(parsed) => Ok(parsed),
    Err(err) if err.to_string().contains("was empty") => {
        Ok(skip_silently("nothing to execute"))
    }
    Err(err) => Err(err),
}

Prevention

When it happens

Trigger: Dispatching an exec_shell 'run' action whose `command` is `""`, `" "`, or `"# just a comment"` through the read-only (Scout/Reviewer) policy branch without a pipe.

Common situations: Template bugs that interpolate an empty variable into the command string; harness probes testing the readonly gate with blank input; comment-only placeholder commands.

Related errors


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