Hmbown/CodeWhale · error · anyhow::Error
[shell.readonly.command] Tool
Error message
[shell.readonly.command] Tool {name} input did not match the bounded read-only shell grammar for Fleet role `{role}`. {guidance} What it means
A sub-agent whose Fleet role is read-only tried to run a shell command that did not match the bounded read-only shell grammar. The library enforces per-role posture authoritatively (#3217): read-only roles cannot mutate or run arbitrary shell, even if the parent session is auto-approved. The command string failed `allows_bounded_readonly_bash` / the exec-policy grammar check, so the call is rejected with role-specific guidance.
Solutions
- Rewrite the command to a form accepted by codewhale_execpolicy::command_safety (pure read-only: no redirects, no mutation, no arbitrary program execution).
- Re-dispatch the task with an `implement`, `general`, or `custom` role that has shell/write capability.
- If the child only needs bounded reads, use explicit read tools (read/grep/glob) instead of bash.
- Check readonly_command_help() output included in the error for the accepted grammar.
Example fix
// before (read-only role)
{"tool":"bash","input":{"command":"git status > /tmp/out.txt"}}
// after: bounded read-only form, or switch role to `implement`
{"tool":"bash","input":{"command":"git status"}} Defensive patterns
Strategy: validation
Validate before calling
// before dispatching bash to a child
if role_is_readonly(role) && !codewhale_execpolicy::command_safety::is_readonly_command(cmd) {
return Err("command not in bounded read-only grammar for role");
} Type guard
fn is_bounded_readonly_bash(name: &str, input: &Value) -> bool {
name == "bash"
&& input.get("command").and_then(Value::as_str)
.map(codewhale_execpolicy::command_safety::is_readonly_command)
.unwrap_or(false)
} Try / catch
match child.execute_tool(name, input) {
Err(e) if e.to_string().contains("[shell.readonly.command]") => {
// fall back to read tools or re-dispatch with write-capable role
}
other => other?,
} Prevention
- Keep read-only roles on explicit read/grep/glob tools instead of bash.
- Pre-validate shell strings with codewhale_execpolicy::command_safety before dispatch.
- Do not rely on parent auto-approve to relax child posture.
- Consult readonly_command_help() for accepted grammar when composing commands.
When it happens
Trigger: Calling a bash/shell tool from a sub-agent whose agent_type is a read-only Fleet role (e.g. a review/analysis role) with a command string that is not on the proven read-only allowlist (e.g. contains redirects, pipes to writers, or mutating binaries), or calling any non-bounded-shell tool that posture_permits_tool rejects while the tool happens to allow bounded readonly bash.
Common situations: Delegating an `implement`-style task to a read-only role; a child agent hallucinating `git push`, `rm`, or file-redirecting commands; relying on parent auto-approve to relax child restrictions (explicitly not permitted).
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- read-only roles may revise their private working notes
- Refusing from agent ' ' to ' '; a child may control only…
- Refusing on self (agent_id ' '); child coordination…
- [role.posture.denied] Tool
- agent action=claim widens an enforced write scope, and the…
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/452e1cf7e8538c14.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/tools/subagent/mod.rs:17530
{
true
} else if let Some(action) = action {
self.is_action_allowed(name, action)
} else {
self.allowed_tools
.as_ref()
.is_none_or(|list| list.iter().any(|allowed| allowed == name))
};
if !self.is_tool_allowed(name) || !family_action_allowed {
return Err(anyhow!("Tool {name} not allowed for this sub-agent"));
}
// #3217: authoritative per-role posture — read-only roles cannot mutate
// and non-`Full`-shell roles cannot run shell, regardless of whether
// the parent session is auto-approved. This closes the auto-approve
// bypass where a read-only child could quietly write or shell out.
if !self.posture_permits_tool(name, Some(&input)) {
if self.allows_bounded_readonly_bash(name) {
return Err(anyhow!(
"[shell.readonly.command] Tool {name} input did not match the bounded read-only shell grammar for Fleet role `{role}`. {guidance}",
role = self.agent_type.as_str(),
guidance = codewhale_execpolicy::command_safety::readonly_command_help()
));
}
return Err(anyhow!(
"[role.posture.denied] Tool {name} is not permitted for the read-only Fleet role `{role}`. Use an `implement` or `general` role (or `custom` with an explicit allowed_tools list) to mutate the workspace or run shell commands.",
role = self.agent_type.as_str()
));
}
// Denied network capability cannot be expanded by answering a prompt.
if self.network_is_denied() {
reject_network_reaching_input(name, &input)?;
}
// The session's permission posture, applied to this child exactly as
// it is applied to the parent turn: the deterministic Auto-Review
// floor first, then (Auto-Review) the model guardian for holds it
// could not prove safe, or (Ask) a prompt raised in the parent's UI.View on GitHub (pinned to 433685b202)