{"record":{"id":"f8c147692ab17766","repo":"Hmbown/CodeWhale","slug":"persistent-allow-rules-must-be-scoped-to-a-workspa","errorCode":null,"errorMessage":"persistent allow rules must be scoped to a workspace","messagePattern":"persistent allow rules must be scoped to a workspace","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/config/src/lib.rs","lineNumber":5336,"sourceCode":"    }\n\n    /// Atomically append exact, repo-scoped allow rules to the sibling\n    /// `permissions.toml` file.\n    ///\n    /// The caller is responsible for deciding which tool calls are eligible;\n    /// this boundary rejects broad or incorrectly typed records so a UI bug\n    /// cannot persist an unscoped allow grant.\n    pub fn append_allow_rules(&mut self, rules: &[ToolAskRule]) -> Result<usize> {\n        for rule in rules {\n            if rule.action != PermissionAction::Allow {\n                bail!(\"append_allow_rules only accepts action = \\\"allow\\\"\");\n            }\n            let Some(workspace) = rule\n                .workspace\n                .as_deref()\n                .and_then(codewhale_execpolicy::normalize_workspace_scope)\n            else {\n                bail!(\"persistent allow rules must be scoped to a workspace\");\n            };\n            if rule.command.is_some() && !rule.command_exact {\n                bail!(\"persistent command allow rules must use exact matching\");\n            }\n            if rule.command.is_none() && rule.path.is_none() {\n                bail!(\"persistent allow rules must match an exact command or path\");\n            }\n            if let Some(command) = rule.command.as_deref()\n                && command.trim().is_empty()\n            {\n                bail!(\"persistent command allow rules must not be empty\");\n            }\n            if let Some(path) = rule.path.as_deref()\n                && codewhale_execpolicy::normalize_workspace_relative_path(path, &workspace)\n                    .is_none_or(|path| path.is_empty())\n            {\n                bail!(\"persistent path allow rules must stay within the workspace\");\n            }","sourceCodeStart":5318,"sourceCodeEnd":5354,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/config/src/lib.rs#L5318-L5354","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconfig.append_allow_rules(&[ToolAskRule { action: Allow, workspace: None, command: Some(\"git push\".into()), .. }])?;\n// after\nlet ws = normalize_workspace_scope(&cwd).ok_or_else(|| anyhow::anyhow!(\"no workspace\"))?;\nconfig.append_allow_rules(&[ToolAskRule { action: Allow, workspace: Some(ws), command: Some(\"git push\".into()), .. }])?;","handlingStrategy":"validation","validationCode":"for r in rules {\n    let ws = r.workspace.as_deref().and_then(codewhale_execpolicy::normalize_workspace_scope)\n        .ok_or_else(|| anyhow::anyhow!(\"allow rule needs a workspace scope\"))?;\n}","typeGuard":"fn is_scoped_allow_rule(r: &ToolAskRule) -> bool {\n    r.workspace.as_deref().and_then(codewhale_execpolicy::normalize_workspace_scope).is_some()\n}","tryCatchPattern":"match config.append_allow_rules(&rules) {\n    Err(e) if e.to_string().contains(\"scoped to a workspace\") => eprintln!(\"attach the session's workspace to each rule\"),\n    other => other?,\n}","preventionTips":["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"],"tags":["permissions","security","workspace"],"backgroundTag":"insufficient-permissions","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}