source dump of claude code
at main 234 lines 8.1 kB view raw
1import type { ToolPermissionContext } from '../../Tool.js' 2import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js' 3import type { PermissionRule, PermissionRuleSource } from './PermissionRule.js' 4import { 5 getAllowRules, 6 getAskRules, 7 getDenyRules, 8 permissionRuleSourceDisplayString, 9} from './permissions.js' 10 11/** 12 * Type of shadowing that makes a rule unreachable 13 */ 14export type ShadowType = 'ask' | 'deny' 15 16/** 17 * Represents an unreachable permission rule with explanation 18 */ 19export type UnreachableRule = { 20 rule: PermissionRule 21 reason: string 22 shadowedBy: PermissionRule 23 shadowType: ShadowType 24 fix: string 25} 26 27/** 28 * Options for detecting unreachable rules 29 */ 30export type DetectUnreachableRulesOptions = { 31 /** 32 * Whether sandbox auto-allow is enabled for Bash commands. 33 * When true, tool-wide Bash ask rules from personal settings don't block 34 * specific Bash allow rules because sandboxed commands are auto-allowed. 35 */ 36 sandboxAutoAllowEnabled: boolean 37} 38 39/** 40 * Result of checking if a rule is shadowed. 41 * Uses discriminated union for type safety. 42 */ 43type ShadowResult = 44 | { shadowed: false } 45 | { shadowed: true; shadowedBy: PermissionRule; shadowType: ShadowType } 46 47/** 48 * Check if a permission rule source is shared (visible to other users). 49 * Shared settings include: 50 * - projectSettings: Committed to git, shared with team 51 * - policySettings: Enterprise-managed, pushed to all users 52 * - command: From slash command frontmatter, potentially shared 53 * 54 * Personal settings include: 55 * - userSettings: User's global ~/.claude settings 56 * - localSettings: Gitignored per-project settings 57 * - cliArg: Runtime CLI arguments 58 * - session: In-memory session rules 59 * - flagSettings: From --settings flag (runtime) 60 */ 61export function isSharedSettingSource(source: PermissionRuleSource): boolean { 62 return ( 63 source === 'projectSettings' || 64 source === 'policySettings' || 65 source === 'command' 66 ) 67} 68 69/** 70 * Format a rule source for display in warning messages. 71 */ 72function formatSource(source: PermissionRuleSource): string { 73 return permissionRuleSourceDisplayString(source) 74} 75 76/** 77 * Generate a fix suggestion based on the shadow type. 78 */ 79function generateFixSuggestion( 80 shadowType: ShadowType, 81 shadowingRule: PermissionRule, 82 shadowedRule: PermissionRule, 83): string { 84 const shadowingSource = formatSource(shadowingRule.source) 85 const shadowedSource = formatSource(shadowedRule.source) 86 const toolName = shadowingRule.ruleValue.toolName 87 88 if (shadowType === 'deny') { 89 return `Remove the "${toolName}" deny rule from ${shadowingSource}, or remove the specific allow rule from ${shadowedSource}` 90 } 91 return `Remove the "${toolName}" ask rule from ${shadowingSource}, or remove the specific allow rule from ${shadowedSource}` 92} 93 94/** 95 * Check if a specific allow rule is shadowed (unreachable) by an ask rule. 96 * 97 * An allow rule is unreachable when: 98 * 1. There's a tool-wide ask rule (e.g., "Bash" in ask list) 99 * 2. And a specific allow rule (e.g., "Bash(ls:*)" in allow list) 100 * 101 * The ask rule takes precedence, making the specific allow rule unreachable 102 * because the user will always be prompted first. 103 * 104 * Exception: For Bash with sandbox auto-allow enabled, tool-wide ask rules 105 * from PERSONAL settings don't shadow specific allow rules because: 106 * - Sandboxed commands are auto-allowed regardless of ask rules 107 * - This only applies to personal settings (userSettings, localSettings, etc.) 108 * - Shared settings (projectSettings, policySettings) always warn because 109 * other team members may not have sandbox enabled 110 */ 111function isAllowRuleShadowedByAskRule( 112 allowRule: PermissionRule, 113 askRules: PermissionRule[], 114 options: DetectUnreachableRulesOptions, 115): ShadowResult { 116 const { toolName, ruleContent } = allowRule.ruleValue 117 118 // Only check allow rules that have specific content (e.g., "Bash(ls:*)") 119 // Tool-wide allow rules cannot be shadowed by ask rules 120 if (ruleContent === undefined) { 121 return { shadowed: false } 122 } 123 124 // Find any tool-wide ask rule for the same tool 125 const shadowingAskRule = askRules.find( 126 askRule => 127 askRule.ruleValue.toolName === toolName && 128 askRule.ruleValue.ruleContent === undefined, 129 ) 130 131 if (!shadowingAskRule) { 132 return { shadowed: false } 133 } 134 135 // Special case: Bash with sandbox auto-allow from personal settings 136 // The sandbox exception is based on the ASK rule's source, not the allow rule's source. 137 // If the ask rule is from personal settings, the user's own sandbox will auto-allow. 138 // If the ask rule is from shared settings, other team members may not have sandbox enabled. 139 if (toolName === BASH_TOOL_NAME && options.sandboxAutoAllowEnabled) { 140 if (!isSharedSettingSource(shadowingAskRule.source)) { 141 return { shadowed: false } 142 } 143 // Fall through to mark as shadowed - shared settings should always warn 144 } 145 146 return { shadowed: true, shadowedBy: shadowingAskRule, shadowType: 'ask' } 147} 148 149/** 150 * Check if an allow rule is shadowed (completely blocked) by a deny rule. 151 * 152 * An allow rule is unreachable when: 153 * 1. There's a tool-wide deny rule (e.g., "Bash" in deny list) 154 * 2. And a specific allow rule (e.g., "Bash(ls:*)" in allow list) 155 * 156 * Deny rules are checked first in the permission evaluation order, 157 * so the allow rule will never be reached - the tool is always denied. 158 * This is more severe than ask-shadowing because the rule is truly blocked. 159 */ 160function isAllowRuleShadowedByDenyRule( 161 allowRule: PermissionRule, 162 denyRules: PermissionRule[], 163): ShadowResult { 164 const { toolName, ruleContent } = allowRule.ruleValue 165 166 // Only check allow rules that have specific content (e.g., "Bash(ls:*)") 167 // Tool-wide allow rules conflict with tool-wide deny rules but are not "shadowed" 168 if (ruleContent === undefined) { 169 return { shadowed: false } 170 } 171 172 // Find any tool-wide deny rule for the same tool 173 const shadowingDenyRule = denyRules.find( 174 denyRule => 175 denyRule.ruleValue.toolName === toolName && 176 denyRule.ruleValue.ruleContent === undefined, 177 ) 178 179 if (!shadowingDenyRule) { 180 return { shadowed: false } 181 } 182 183 return { shadowed: true, shadowedBy: shadowingDenyRule, shadowType: 'deny' } 184} 185 186/** 187 * Detect all unreachable permission rules in the given context. 188 * 189 * Currently detects: 190 * - Allow rules shadowed by tool-wide deny rules (more severe - completely blocked) 191 * - Allow rules shadowed by tool-wide ask rules (will always prompt) 192 */ 193export function detectUnreachableRules( 194 context: ToolPermissionContext, 195 options: DetectUnreachableRulesOptions, 196): UnreachableRule[] { 197 const unreachable: UnreachableRule[] = [] 198 199 const allowRules = getAllowRules(context) 200 const askRules = getAskRules(context) 201 const denyRules = getDenyRules(context) 202 203 // Check each allow rule for shadowing 204 for (const allowRule of allowRules) { 205 // Check deny shadowing first (more severe) 206 const denyResult = isAllowRuleShadowedByDenyRule(allowRule, denyRules) 207 if (denyResult.shadowed) { 208 const shadowSource = formatSource(denyResult.shadowedBy.source) 209 unreachable.push({ 210 rule: allowRule, 211 reason: `Blocked by "${denyResult.shadowedBy.ruleValue.toolName}" deny rule (from ${shadowSource})`, 212 shadowedBy: denyResult.shadowedBy, 213 shadowType: 'deny', 214 fix: generateFixSuggestion('deny', denyResult.shadowedBy, allowRule), 215 }) 216 continue // Don't also report ask-shadowing if deny-shadowed 217 } 218 219 // Check ask shadowing 220 const askResult = isAllowRuleShadowedByAskRule(allowRule, askRules, options) 221 if (askResult.shadowed) { 222 const shadowSource = formatSource(askResult.shadowedBy.source) 223 unreachable.push({ 224 rule: allowRule, 225 reason: `Shadowed by "${askResult.shadowedBy.ruleValue.toolName}" ask rule (from ${shadowSource})`, 226 shadowedBy: askResult.shadowedBy, 227 shadowType: 'ask', 228 fix: generateFixSuggestion('ask', askResult.shadowedBy, allowRule), 229 }) 230 } 231 } 232 233 return unreachable 234}