Hmbown/CodeWhale · error · anyhow::Error
[role.posture.denied] Tool
Error message
[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. What it means
The sub-agent's Fleet role is read-only and the tool it called is categorically not permitted by the role posture — unlike error 2610, the tool is not even a candidate for bounded read-only shell grammar. The posture check (#3217) hard-denies mutation and shell for read-only roles regardless of parent-session auto-approval.
Solutions
- Use an `implement` or `general` role for the sub-agent, or define the role as `custom` with an explicit `allowed_tools` list including the tools it needs.
- Keep mutation work out of read-only children: have them report findings and let a write-capable member apply changes.
- Verify agent_type.as_str() for the dispatched child matches the intended role in your Fleet configuration.
Example fix
// before
{"subagent":{"role":"review","tools":["edit"]}}
// after
{"subagent":{"role":"implement"}} Defensive patterns
Strategy: validation
Validate before calling
if !role_allows_mutation(role) && write_family_tools.iter().any(|t| requested_tools.contains(t)) {
return Err("role {} cannot run tool {}".format(role, tool));
} Try / catch
if let Err(e) = child.execute_tool(name, input) {
if e.to_string().contains("[role.posture.denied]") {
// re-dispatch with an `implement`/`general`/custom-allowed_tools role
}
} Prevention
- Match role posture to the task's mutation needs at dispatch time.
- Give `custom` roles an explicit allowed_tools list.
- Have read-only children report findings; let write-capable members mutate.
- Log role.as_str() alongside tool calls in tests to catch mismatches early.
When it happens
Trigger: A child agent with a read-only Fleet role calls any mutating tool (write/edit/patch style tools) or a shell tool, and `allows_bounded_readonly_bash(name)` returns false, so the generic posture denial branch fires.
Common situations: Configuring a sub-agent with a review/analysis role but the model attempts file edits or command execution; mislabeling a `custom` agent without an `allowed_tools` list so it defaults to read-only posture.
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…
- [shell.readonly.command] 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/4e7939d32cea9cc1.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/tools/subagent/mod.rs:17536
.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.
// Full Access still fails closed on the non-bypassable safety floor.
// Role posture and the execution envelope below stay authoritative:
// this gate can only decide whether a call the role permits also
// clears the session's approval boundary.
if let ChildGateVerdict::Deny(reason) =
self.gate_held_call(agent_id, tool_id, name, &input).awaitView on GitHub (pinned to 433685b202)