Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
6.4 kB · 228 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229/** * Shared permission rule matching utilities for shell tools. * * Extracts common logic for: * - Parsing permission rules (exact, prefix, wildcard) * - Matching commands against rules * - Generating permission suggestions */
import type { PermissionUpdate } from './PermissionUpdateSchema.js'
// Null-byte sentinel placeholders for wildcard pattern escaping — module-level// so the RegExp objects are compiled once instead of per permission check.const ESCAPED_STAR_PLACEHOLDER = '\x00ESCAPED_STAR\x00'const ESCAPED_BACKSLASH_PLACEHOLDER = '\x00ESCAPED_BACKSLASH\x00'const ESCAPED_STAR_PLACEHOLDER_RE = new RegExp(ESCAPED_STAR_PLACEHOLDER, 'g')const ESCAPED_BACKSLASH_PLACEHOLDER_RE = new RegExp( ESCAPED_BACKSLASH_PLACEHOLDER, 'g',)
/** * Parsed permission rule discriminated union. */export type ShellPermissionRule = | { type: 'exact' command: string } | { type: 'prefix' prefix: string } | { type: 'wildcard' pattern: string }
/** * Extract prefix from legacy :* syntax (e.g., "npm:*" -> "npm") * This is maintained for backwards compatibility. */export function permissionRuleExtractPrefix( permissionRule: string,): string | null { const match = permissionRule.match(/^(.+):\*$/) return match?.[1] ?? null}
/** * Check if a pattern contains unescaped wildcards (not legacy :* syntax). * Returns true if the pattern contains * that are not escaped with \ or part of :* at the end. */export function hasWildcards(pattern: string): boolean { // If it ends with :*, it's legacy prefix syntax, not wildcard if (pattern.endsWith(':*')) { return false } // Check for unescaped * anywhere in the pattern // An asterisk is unescaped if it's not preceded by a backslash, // or if it's preceded by an even number of backslashes (escaped backslashes) for (let i = 0; i < pattern.length; i++) { if (pattern[i] === '*') { // Count backslashes before this asterisk let backslashCount = 0 let j = i - 1 while (j >= 0 && pattern[j] === '\\') { backslashCount++ j-- } // If even number of backslashes (including 0), the asterisk is unescaped if (backslashCount % 2 === 0) { return true } } } return false}
/** * Match a command against a wildcard pattern. * Wildcards (*) match any sequence of characters. * Use \* to match a literal asterisk character. * Use \\ to match a literal backslash. * * @param pattern - The permission rule pattern with wildcards * @param command - The command to match against * @returns true if the command matches the pattern */export function matchWildcardPattern( pattern: string, command: string, caseInsensitive = false,): boolean { // Trim leading/trailing whitespace from pattern const trimmedPattern = pattern.trim()
// Process the pattern to handle escape sequences: \* and \\ let processed = '' let i = 0
while (i < trimmedPattern.length) { const char = trimmedPattern[i]
// Handle escape sequences if (char === '\\' && i + 1 < trimmedPattern.length) { const nextChar = trimmedPattern[i + 1] if (nextChar === '*') { // \* -> literal asterisk placeholder processed += ESCAPED_STAR_PLACEHOLDER i += 2 continue } else if (nextChar === '\\') { // \\ -> literal backslash placeholder processed += ESCAPED_BACKSLASH_PLACEHOLDER i += 2 continue } }
processed += char i++ }
// Escape regex special characters except * const escaped = processed.replace(/[.+?^${}()|[\]\\'"]/g, '\\$&')
// Convert unescaped * to .* for wildcard matching const withWildcards = escaped.replace(/\*/g, '.*')
// Convert placeholders back to escaped regex literals let regexPattern = withWildcards .replace(ESCAPED_STAR_PLACEHOLDER_RE, '\\*') .replace(ESCAPED_BACKSLASH_PLACEHOLDER_RE, '\\\\')
// When a pattern ends with ' *' (space + unescaped wildcard) AND the trailing // wildcard is the ONLY unescaped wildcard, make the trailing space-and-args // optional so 'git *' matches both 'git add' and bare 'git'. // This aligns wildcard matching with prefix rule semantics (git:*). // Multi-wildcard patterns like '* run *' are excluded — making the last // wildcard optional would incorrectly match 'npm run' (no trailing arg). const unescapedStarCount = (processed.match(/\*/g) || []).length if (regexPattern.endsWith(' .*') && unescapedStarCount === 1) { regexPattern = regexPattern.slice(0, -3) + '( .*)?' }
// Create regex that matches the entire string. // The 's' (dotAll) flag makes '.' match newlines, so wildcards match // commands containing embedded newlines (e.g. heredoc content after splitCommand_DEPRECATED). const flags = 's' + (caseInsensitive ? 'i' : '') const regex = new RegExp(`^${regexPattern}$`, flags)
return regex.test(command)}
/** * Parse a permission rule string into a structured rule object. */export function parsePermissionRule( permissionRule: string,): ShellPermissionRule { // Check for legacy :* prefix syntax first (backwards compatibility) const prefix = permissionRuleExtractPrefix(permissionRule) if (prefix !== null) { return { type: 'prefix', prefix, } }
// Check for new wildcard syntax (contains * but not :* at end) if (hasWildcards(permissionRule)) { return { type: 'wildcard', pattern: permissionRule, } }
// Otherwise, it's an exact match return { type: 'exact', command: permissionRule, }}
/** * Generate permission update suggestion for an exact command match. */export function suggestionForExactCommand( toolName: string, command: string,): PermissionUpdate[] { return [ { type: 'addRules', rules: [ { toolName, ruleContent: command, }, ], behavior: 'allow', destination: 'localSettings', }, ]}
/** * Generate permission update suggestion for a prefix match. */export function suggestionForPrefix( toolName: string, prefix: string,): PermissionUpdate[] { return [ { type: 'addRules', rules: [ { toolName, ruleContent: `${prefix}:*`, }, ], behavior: 'allow', destination: 'localSettings', }, ]}