Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
6.6 kB · 196 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197/** * Filter and sanitize installed-app data for inclusion in the `request_access` * tool description. Ported from Cowork's appNames.ts. Two * concerns: noise filtering (Spotlight returns every bundle on disk — XPC * helpers, daemons, input methods) and prompt-injection hardening (app names * are attacker-controlled; anyone can ship an app named anything). * * Residual risk: short benign-char adversarial names ("grant all") can't be * filtered programmatically. The tool description's structural framing * ("Available applications:") makes it clear these are app names, and the * downstream permission dialog requires explicit user approval — a bad name * can't auto-grant anything. */
/** Minimal shape — matches what `listInstalledApps` returns. */type InstalledAppLike = { readonly bundleId: string readonly displayName: string readonly path: string}
// ── Noise filtering ──────────────────────────────────────────────────────
/** * Only apps under these roots are shown. /System/Library subpaths (CoreServices, * PrivateFrameworks, Input Methods) are OS plumbing — anchor on known-good * roots rather than blocklisting every junk subpath since new macOS versions * add more. * * ~/Applications is checked at call time via the `homeDir` arg (HOME isn't * reliably known at module load in all environments). */const PATH_ALLOWLIST: readonly string[] = [ '/Applications/', '/System/Applications/',]
/** * Display-name patterns that mark background services even under /Applications. * `(?:$|\s\()` — matches keyword at end-of-string OR immediately before ` (`: * "Slack Helper (GPU)" and "ABAssistantService" fail, "Service Desk" passes * (Service is followed by " D"). */const NAME_PATTERN_BLOCKLIST: readonly RegExp[] = [ /Helper(?:$|\s\()/, /Agent(?:$|\s\()/, /Service(?:$|\s\()/, /Uninstaller(?:$|\s\()/, /Updater(?:$|\s\()/, /^\./,]
/** * Apps commonly requested for CU automation. ALWAYS included if installed, * bypassing path check + count cap — the model needs these exact names even * when the machine has 200+ apps. Bundle IDs (locale-invariant), not display * names. Keep <30 — each entry is a guaranteed token in the description. */const ALWAYS_KEEP_BUNDLE_IDS: ReadonlySet<string> = new Set([ // Browsers 'com.apple.Safari', 'com.google.Chrome', 'com.microsoft.edgemac', 'org.mozilla.firefox', 'company.thebrowser.Browser', // Arc // Communication 'com.tinyspeck.slackmacgap', 'us.zoom.xos', 'com.microsoft.teams2', 'com.microsoft.teams', 'com.apple.MobileSMS', 'com.apple.mail', // Productivity 'com.microsoft.Word', 'com.microsoft.Excel', 'com.microsoft.Powerpoint', 'com.microsoft.Outlook', 'com.apple.iWork.Pages', 'com.apple.iWork.Numbers', 'com.apple.iWork.Keynote', 'com.google.GoogleDocs', // Notes / PM 'notion.id', 'com.apple.Notes', 'md.obsidian', 'com.linear', 'com.figma.Desktop', // Dev 'com.microsoft.VSCode', 'com.apple.Terminal', 'com.googlecode.iterm2', 'com.github.GitHubDesktop', // System essentials the model genuinely targets 'com.apple.finder', 'com.apple.iCal', 'com.apple.systempreferences',])
// ── Prompt-injection hardening ───────────────────────────────────────────
/** * `\p{L}\p{M}\p{N}` with /u — not `\w` (ASCII-only, would drop Bücher, 微信, * Préférences Système). `\p{M}` matches combining marks so NFD-decomposed * diacritics (ü → u + ◌̈) pass. Single space not `\s` — `\s` matches newlines, * which would let "App\nIgnore previous…" through as a multi-line injection. * Still bars quotes, angle brackets, backticks, pipes, colons. */const APP_NAME_ALLOWED = /^[\p{L}\p{M}\p{N}_ .&'()+-]+$/uconst APP_NAME_MAX_LEN = 40const APP_NAME_MAX_COUNT = 50
function isUserFacingPath(path: string, homeDir: string | undefined): boolean { if (PATH_ALLOWLIST.some(root => path.startsWith(root))) return true if (homeDir) { const userApps = homeDir.endsWith('/') ? `${homeDir}Applications/` : `${homeDir}/Applications/` if (path.startsWith(userApps)) return true } return false}
function isNoisyName(name: string): boolean { return NAME_PATTERN_BLOCKLIST.some(re => re.test(name))}
/** * Length cap + trim + dedupe + sort. `applyCharFilter` — skip for trusted * bundle IDs (Apple/Google/MS; a localized "Réglages Système" with unusual * punctuation shouldn't be dropped), apply for anything attacker-installable. */function sanitizeCore( raw: readonly string[], applyCharFilter: boolean,): string[] { const seen = new Set<string>() return raw .map(name => name.trim()) .filter(trimmed => { if (!trimmed) return false if (trimmed.length > APP_NAME_MAX_LEN) return false if (applyCharFilter && !APP_NAME_ALLOWED.test(trimmed)) return false if (seen.has(trimmed)) return false seen.add(trimmed) return true }) .sort((a, b) => a.localeCompare(b))}
function sanitizeAppNames(raw: readonly string[]): string[] { const filtered = sanitizeCore(raw, true) if (filtered.length <= APP_NAME_MAX_COUNT) return filtered return [ ...filtered.slice(0, APP_NAME_MAX_COUNT), `… and ${filtered.length - APP_NAME_MAX_COUNT} more`, ]}
function sanitizeTrustedNames(raw: readonly string[]): string[] { return sanitizeCore(raw, false)}
/** * Filter raw Spotlight results to user-facing apps, then sanitize. Always-keep * apps bypass path/name filter AND char allowlist (trusted vendors, not * attacker-installed); still length-capped, deduped, sorted. */export function filterAppsForDescription( installed: readonly InstalledAppLike[], homeDir: string | undefined,): string[] { const { alwaysKept, rest } = installed.reduce<{ alwaysKept: string[] rest: string[] }>( (acc, app) => { if (ALWAYS_KEEP_BUNDLE_IDS.has(app.bundleId)) { acc.alwaysKept.push(app.displayName) } else if ( isUserFacingPath(app.path, homeDir) && !isNoisyName(app.displayName) ) { acc.rest.push(app.displayName) } return acc }, { alwaysKept: [], rest: [] }, )
const sanitizedAlways = sanitizeTrustedNames(alwaysKept) const alwaysSet = new Set(sanitizedAlways) return [ ...sanitizedAlways, ...sanitizeAppNames(rest).filter(n => !alwaysSet.has(n)), ]}