π File detail
utils/permissions/shellRuleMatching.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes ShellPermissionRule, permissionRuleExtractPrefix, hasWildcards, matchWildcardPattern, and parsePermissionRule (and more) β mainly functions, hooks, or classes. It composes internal code from PermissionUpdateSchema (relative imports). What the file header says: 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.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
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
π€ Exports (heuristic)
ShellPermissionRulepermissionRuleExtractPrefixhasWildcardsmatchWildcardPatternparsePermissionRulesuggestionForExactCommandsuggestionForPrefix
π₯οΈ Source preview
/**
* 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',
},
]
}