Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
8.1 kB · 234 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235import type { ToolPermissionContext } from '../../Tool.js'import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js'import type { PermissionRule, PermissionRuleSource } from './PermissionRule.js'import { getAllowRules, getAskRules, getDenyRules, permissionRuleSourceDisplayString,} from './permissions.js'
/** * Type of shadowing that makes a rule unreachable */export type ShadowType = 'ask' | 'deny'
/** * Represents an unreachable permission rule with explanation */export type UnreachableRule = { rule: PermissionRule reason: string shadowedBy: PermissionRule shadowType: ShadowType fix: string}
/** * Options for detecting unreachable rules */export type DetectUnreachableRulesOptions = { /** * Whether sandbox auto-allow is enabled for Bash commands. * When true, tool-wide Bash ask rules from personal settings don't block * specific Bash allow rules because sandboxed commands are auto-allowed. */ sandboxAutoAllowEnabled: boolean}
/** * Result of checking if a rule is shadowed. * Uses discriminated union for type safety. */type ShadowResult = | { shadowed: false } | { shadowed: true; shadowedBy: PermissionRule; shadowType: ShadowType }
/** * Check if a permission rule source is shared (visible to other users). * Shared settings include: * - projectSettings: Committed to git, shared with team * - policySettings: Enterprise-managed, pushed to all users * - command: From slash command frontmatter, potentially shared * * Personal settings include: * - userSettings: User's global ~/.claude settings * - localSettings: Gitignored per-project settings * - cliArg: Runtime CLI arguments * - session: In-memory session rules * - flagSettings: From --settings flag (runtime) */export function isSharedSettingSource(source: PermissionRuleSource): boolean { return ( source === 'projectSettings' || source === 'policySettings' || source === 'command' )}
/** * Format a rule source for display in warning messages. */function formatSource(source: PermissionRuleSource): string { return permissionRuleSourceDisplayString(source)}
/** * Generate a fix suggestion based on the shadow type. */function generateFixSuggestion( shadowType: ShadowType, shadowingRule: PermissionRule, shadowedRule: PermissionRule,): string { const shadowingSource = formatSource(shadowingRule.source) const shadowedSource = formatSource(shadowedRule.source) const toolName = shadowingRule.ruleValue.toolName
if (shadowType === 'deny') { return `Remove the "${toolName}" deny rule from ${shadowingSource}, or remove the specific allow rule from ${shadowedSource}` } return `Remove the "${toolName}" ask rule from ${shadowingSource}, or remove the specific allow rule from ${shadowedSource}`}
/** * Check if a specific allow rule is shadowed (unreachable) by an ask rule. * * An allow rule is unreachable when: * 1. There's a tool-wide ask rule (e.g., "Bash" in ask list) * 2. And a specific allow rule (e.g., "Bash(ls:*)" in allow list) * * The ask rule takes precedence, making the specific allow rule unreachable * because the user will always be prompted first. * * Exception: For Bash with sandbox auto-allow enabled, tool-wide ask rules * from PERSONAL settings don't shadow specific allow rules because: * - Sandboxed commands are auto-allowed regardless of ask rules * - This only applies to personal settings (userSettings, localSettings, etc.) * - Shared settings (projectSettings, policySettings) always warn because * other team members may not have sandbox enabled */function isAllowRuleShadowedByAskRule( allowRule: PermissionRule, askRules: PermissionRule[], options: DetectUnreachableRulesOptions,): ShadowResult { const { toolName, ruleContent } = allowRule.ruleValue
// Only check allow rules that have specific content (e.g., "Bash(ls:*)") // Tool-wide allow rules cannot be shadowed by ask rules if (ruleContent === undefined) { return { shadowed: false } }
// Find any tool-wide ask rule for the same tool const shadowingAskRule = askRules.find( askRule => askRule.ruleValue.toolName === toolName && askRule.ruleValue.ruleContent === undefined, )
if (!shadowingAskRule) { return { shadowed: false } }
// Special case: Bash with sandbox auto-allow from personal settings // The sandbox exception is based on the ASK rule's source, not the allow rule's source. // If the ask rule is from personal settings, the user's own sandbox will auto-allow. // If the ask rule is from shared settings, other team members may not have sandbox enabled. if (toolName === BASH_TOOL_NAME && options.sandboxAutoAllowEnabled) { if (!isSharedSettingSource(shadowingAskRule.source)) { return { shadowed: false } } // Fall through to mark as shadowed - shared settings should always warn }
return { shadowed: true, shadowedBy: shadowingAskRule, shadowType: 'ask' }}
/** * Check if an allow rule is shadowed (completely blocked) by a deny rule. * * An allow rule is unreachable when: * 1. There's a tool-wide deny rule (e.g., "Bash" in deny list) * 2. And a specific allow rule (e.g., "Bash(ls:*)" in allow list) * * Deny rules are checked first in the permission evaluation order, * so the allow rule will never be reached - the tool is always denied. * This is more severe than ask-shadowing because the rule is truly blocked. */function isAllowRuleShadowedByDenyRule( allowRule: PermissionRule, denyRules: PermissionRule[],): ShadowResult { const { toolName, ruleContent } = allowRule.ruleValue
// Only check allow rules that have specific content (e.g., "Bash(ls:*)") // Tool-wide allow rules conflict with tool-wide deny rules but are not "shadowed" if (ruleContent === undefined) { return { shadowed: false } }
// Find any tool-wide deny rule for the same tool const shadowingDenyRule = denyRules.find( denyRule => denyRule.ruleValue.toolName === toolName && denyRule.ruleValue.ruleContent === undefined, )
if (!shadowingDenyRule) { return { shadowed: false } }
return { shadowed: true, shadowedBy: shadowingDenyRule, shadowType: 'deny' }}
/** * Detect all unreachable permission rules in the given context. * * Currently detects: * - Allow rules shadowed by tool-wide deny rules (more severe - completely blocked) * - Allow rules shadowed by tool-wide ask rules (will always prompt) */export function detectUnreachableRules( context: ToolPermissionContext, options: DetectUnreachableRulesOptions,): UnreachableRule[] { const unreachable: UnreachableRule[] = []
const allowRules = getAllowRules(context) const askRules = getAskRules(context) const denyRules = getDenyRules(context)
// Check each allow rule for shadowing for (const allowRule of allowRules) { // Check deny shadowing first (more severe) const denyResult = isAllowRuleShadowedByDenyRule(allowRule, denyRules) if (denyResult.shadowed) { const shadowSource = formatSource(denyResult.shadowedBy.source) unreachable.push({ rule: allowRule, reason: `Blocked by "${denyResult.shadowedBy.ruleValue.toolName}" deny rule (from ${shadowSource})`, shadowedBy: denyResult.shadowedBy, shadowType: 'deny', fix: generateFixSuggestion('deny', denyResult.shadowedBy, allowRule), }) continue // Don't also report ask-shadowing if deny-shadowed }
// Check ask shadowing const askResult = isAllowRuleShadowedByAskRule(allowRule, askRules, options) if (askResult.shadowed) { const shadowSource = formatSource(askResult.shadowedBy.source) unreachable.push({ rule: allowRule, reason: `Shadowed by "${askResult.shadowedBy.ruleValue.toolName}" ask rule (from ${shadowSource})`, shadowedBy: askResult.shadowedBy, shadowType: 'ask', fix: generateFixSuggestion('ask', askResult.shadowedBy, allowRule), }) } }
return unreachable}