Hmbown/CodeWhale · error
append_allow_rules only accepts action = "allow"
Error message
append_allow_rules only accepts action = "allow"
What it means
append_allow_rules persists tool permission rules, but only allow-grants: every rule must have action == PermissionAction::Allow. Any deny/ask rule passed to this boundary is rejected so a UI bug cannot persist an unscoped or wrongly typed grant.
Solutions
- Filter the input to rules with action == PermissionAction::Allow before calling
- Route deny/ask rules to their intended persistence path instead
- Fix the caller that built the rule list if Allow rules are arriving with the wrong action
Example fix
// before config.append_allow_rules(&all_prompt_rules)?; // after let allows: Vec<_> = all_prompt_rules.iter().filter(|r| r.action == PermissionAction::Allow).cloned().collect(); config.append_allow_rules(&allows)?;
Defensive patterns
Strategy: validation
Validate before calling
if rules.iter().any(|r| r.action != PermissionAction::Allow) {
return Err("append_allow_rules accepts only action = allow rules");
} Type guard
fn is_allow_rule(r: &ToolAskRule) -> bool { r.action == PermissionAction::Allow } Try / catch
match config.append_allow_rules(&rules) {
Err(e) if e.to_string().contains("only accepts action") => eprintln!("filter to Allow rules before persisting"),
other => other?,
} Prevention
- Filter by action at the call site, not inside the API
- Keep allow/deny/ask persistence paths separate
- Add a regression test that mixed-action batches are rejected
When it happens
Trigger: Calling append_allow_rules with a ToolAskRule whose action is not Allow — e.g. passing captured ask/deny records straight through from a permission prompt handler.
Common situations: A permission UI batching all displayed rules (including denials) into one persist call; reusing a generic rule-append API for policy rules; a refactor that changed the action enum on existing records.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- agent action=claim widens an enforced write scope, and the…
- Codewhale credentials directory has an unsupported component
- Codewhale credentials directory must be owned by the…
- Codewhale-owned credential file must be singly linked…
- Codewhale-owned xAI OAuth storage owner is not the current…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/fa705810711dbea4.
Report an issue: GitHub.
Appendix: source
Thrown at crates/config/src/lib.rs:5329
/// `permissions.toml` file.
///
/// Existing comments and formatting are preserved. Exact duplicate rules
/// are ignored, and the in-memory permissions snapshot is refreshed after
/// a successful write.
pub fn append_ask_rules(&mut self, rules: &[ToolAskRule]) -> Result<usize> {
self.append_permission_rules(rules, PermissionAction::Ask)
}
/// Atomically append exact, repo-scoped allow rules to the sibling
/// `permissions.toml` file.
///
/// The caller is responsible for deciding which tool calls are eligible;
/// this boundary rejects broad or incorrectly typed records so a UI bug
/// cannot persist an unscoped allow grant.
pub fn append_allow_rules(&mut self, rules: &[ToolAskRule]) -> Result<usize> {
for rule in rules {
if rule.action != PermissionAction::Allow {
bail!("append_allow_rules only accepts action = \"allow\"");
}
let Some(workspace) = rule
.workspace
.as_deref()
.and_then(codewhale_execpolicy::normalize_workspace_scope)
else {
bail!("persistent allow rules must be scoped to a workspace");
};
if rule.command.is_some() && !rule.command_exact {
bail!("persistent command allow rules must use exact matching");
}
if rule.command.is_none() && rule.path.is_none() {
bail!("persistent allow rules must match an exact command or path");
}
if let Some(command) = rule.command.as_deref()
&& command.trim().is_empty()
{
bail!("persistent command allow rules must not be empty");View on GitHub (pinned to 73e0f67d83)