Hmbown/CodeWhale · error

persistent allow rules must be scoped to a workspace

Error message

persistent allow rules must be scoped to a workspace

What it means

Persistent allow rules must be scoped: the rule's workspace must be present and normalizable (codewhale_execpolicy::normalize_workspace_scope). Unscoped (global) allow grants are refused so a UI bug cannot write a blanket allow into the persistent policy.

Solutions

  1. Set rule.workspace to the normalized workspace path before appending
  2. Normalize with codewhale_execpolicy::normalize_workspace_scope at rule-creation time and drop rules it rejects
  3. Have the UI capture the workspace scope of the session that produced the approval

Example fix

// before
config.append_allow_rules(&[ToolAskRule { action: Allow, workspace: None, command: Some("git push".into()), .. }])?;
// after
let ws = normalize_workspace_scope(&cwd).ok_or_else(|| anyhow::anyhow!("no workspace"))?;
config.append_allow_rules(&[ToolAskRule { action: Allow, workspace: Some(ws), command: Some("git push".into()), .. }])?;
Defensive patterns

Strategy: validation

Validate before calling

for r in rules {
    let ws = r.workspace.as_deref().and_then(codewhale_execpolicy::normalize_workspace_scope)
        .ok_or_else(|| anyhow::anyhow!("allow rule needs a workspace scope"))?;
}

Type guard

fn is_scoped_allow_rule(r: &ToolAskRule) -> bool {
    r.workspace.as_deref().and_then(codewhale_execpolicy::normalize_workspace_scope).is_some()
}

Try / catch

match config.append_allow_rules(&rules) {
    Err(e) if e.to_string().contains("scoped to a workspace") => eprintln!("attach the session's workspace to each rule"),
    other => other?,
}

Prevention

When it happens

Trigger: append_allow_rules with a rule whose workspace is None, empty, or fails normalize_workspace_scope — e.g. a prompt UI persisting a rule created without a workspace scope.

Common situations: A tool-approval dialog that omits the workspace context when constructing the rule; migrating rules from a store that did not record workspace; a workspace path that normalization cannot canonicalize.

Understand the failure class

Background: "You do not have permission" / 403 Forbidden errors: authenticated but not allowed — causes and fixes across open-source libraries — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/f8c147692ab17766. Report an issue: GitHub.

Appendix: source

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

    }

    /// 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 73e0f67d83)