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
- Set rule.workspace to the normalized workspace path before appending
- Normalize with codewhale_execpolicy::normalize_workspace_scope at rule-creation time and drop rules it rejects
- 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
- Normalize workspace scope when the rule is first created, not when persisted
- Have permission UIs always carry the current workspace
- Drop unnormalizable workspaces at the source
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
- agent action=claim widens an enforced write scope, and the…
- append_allow_rules only accepts action = "allow"
- 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/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)