Hmbown/CodeWhale · error · anyhow::Error

persistent allow rules must be scoped to a workspace

Error message

persistent allow rules must be scoped to a workspace

What it means

append_allow_rules (crates/config/src/lib.rs:5261) refuses to persist an allow rule whose workspace field is missing or fails codewhale_execpolicy::normalize_workspace_scope. Persistent allow grants must be scoped to one workspace so a UI bug can never write a global, unscoped allow. Session-scoped rules bypass this path entirely.

Source

Thrown at crates/config/src/lib.rs:5261

    }

    /// 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");
            }
            if let Some(path) = rule.path.as_deref()
                && codewhale_execpolicy::normalize_workspace_relative_path(path, &workspace)
                    .is_none_or(|path| path.is_empty())
            {
                bail!("persistent path allow rules must stay within the workspace");
            }

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Set rule.workspace to the absolute path of the workspace the decision was made in before appending
  2. Use the same construction path as the interactive permission prompt (it fills workspace automatically)
  3. Pre-validate with normalize_workspace_scope and surface a form error instead of hitting the bail

Example fix

// before
let rule = ToolAskRule { action: PermissionAction::Allow, tool: tool.clone(), workspace: None, /* ... */ };
config.append_allow_rules(&[rule])?; // -> must be scoped to a workspace

// after
let rule = ToolAskRule { action: PermissionAction::Allow, tool: tool.clone(), workspace: Some(workspace_root.display().to_string()), /* ... */ };
config.append_allow_rules(&[rule])?;
Defensive patterns

Strategy: validation

Validate before calling

use codewhale_execpolicy::normalize_workspace_scope;
let Some(ws) = rule.workspace.as_deref().and_then(normalize_workspace_scope) else {
    /* refuse before append: fill rule.workspace with the current workspace root */
    unreachable!()
};

Type guard

fn is_persistable_allow_rule(rule: &ToolAskRule) -> bool {
    rule.action == PermissionAction::Allow
        && rule.workspace.as_deref().and_then(normalize_workspace_scope).is_some()
}

Try / catch

if let Err(e) = config.append_allow_rules(&[rule]) {
    if e.to_string().contains("scoped to a workspace") { /* re-prompt with workspace filled */ }
    else { return Err(e); }
}

Prevention

When it happens

Trigger: Constructing a ToolAskRule with action=Allow and workspace=None (or an empty/malformed workspace value) and passing it to append_allow_rules; typically a caller that builds rules by hand instead of via the permission-prompt flow.

Common situations: Custom UI/automation persisting 'always allow' decisions without capturing the current workspace root, deserialized rules from another machine losing the workspace field, refactors that drop the field.

Related errors


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