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
- Skip execution entirely when the command trims to empty or starts with '#'
- Validate a non-empty command field at the tool-input schema before dispatch
- If a non-empty command still reaches this check, the comment-stripping behavior is the likely cause — remove leading '#' commentary and retry
- 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
- Reject empty/whitespace command fields at the tool-input schema
- Remember shell_words strips `# comment` lines — comment-only strings split to zero tokens
- Guard template interpolation so empty variables cannot blank a command
- Never submit placeholder commands to the read-only path
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
- persistent allow rules must be scoped to a workspace
- persistent command allow rules must use exact matching
- persistent allow rules must match an exact command or path
- persistent command allow rules must not be empty
- persistent path allow rules must stay within the workspace
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/f4bf0df5be281bb0.
Report an issue: GitHub.