import { basename, dirname, isAbsolute, join, sep } from 'path' import type { ToolPermissionContext } from '../Tool.js' import { isEnvTruthy } from './envUtils.js' import { getFileReadIgnorePatterns, normalizePatternsToPath, } from './permissions/filesystem.js' import { getPlatform } from './platform.js' import { getGlobExclusionsForPluginCache } from './plugins/orphanedPluginFilter.js' import { ripGrep } from './ripgrep.js' /** * Extracts the static base directory from a glob pattern. * The base directory is everything before the first glob special character (* ? [ {). * Returns the directory portion and the remaining relative pattern. */ export function extractGlobBaseDirectory(pattern: string): { baseDir: string relativePattern: string } { // Find the first glob special character: *, ?, [, { const globChars = /[*?[{]/ const match = pattern.match(globChars) if (!match || match.index === undefined) { // No glob characters - this is a literal path // Return the directory portion and filename as pattern const dir = dirname(pattern) const file = basename(pattern) return { baseDir: dir, relativePattern: file } } // Get everything before the first glob character const staticPrefix = pattern.slice(0, match.index) // Find the last path separator in the static prefix const lastSepIndex = Math.max( staticPrefix.lastIndexOf('/'), staticPrefix.lastIndexOf(sep), ) if (lastSepIndex === -1) { // No path separator before the glob - pattern is relative to cwd return { baseDir: '', relativePattern: pattern } } let baseDir = staticPrefix.slice(0, lastSepIndex) const relativePattern = pattern.slice(lastSepIndex + 1) // Handle root directory patterns (e.g., /*.txt on Unix or C:/*.txt on Windows) // When lastSepIndex is 0, baseDir is empty but we need to use '/' as the root if (baseDir === '' && lastSepIndex === 0) { baseDir = '/' } // Handle Windows drive root paths (e.g., C:/*.txt) // 'C:' means "current directory on drive C" (relative), not root // We need 'C:/' or 'C:\' for the actual drive root if (getPlatform() === 'windows' && /^[A-Za-z]:$/.test(baseDir)) { baseDir = baseDir + sep } return { baseDir, relativePattern } } export async function glob( filePattern: string, cwd: string, { limit, offset }: { limit: number; offset: number }, abortSignal: AbortSignal, toolPermissionContext: ToolPermissionContext, ): Promise<{ files: string[]; truncated: boolean }> { let searchDir = cwd let searchPattern = filePattern // Handle absolute paths by extracting the base directory and converting to relative pattern // ripgrep's --glob flag only works with relative patterns if (isAbsolute(filePattern)) { const { baseDir, relativePattern } = extractGlobBaseDirectory(filePattern) if (baseDir) { searchDir = baseDir searchPattern = relativePattern } } const ignorePatterns = normalizePatternsToPath( getFileReadIgnorePatterns(toolPermissionContext), searchDir, ) // Use ripgrep for better memory performance // --files: list files instead of searching content // --glob: filter by pattern // --sort=modified: sort by modification time (oldest first) // --no-ignore: don't respect .gitignore (default true, set CLAUDE_CODE_GLOB_NO_IGNORE=false to respect .gitignore) // --hidden: include hidden files (default true, set CLAUDE_CODE_GLOB_HIDDEN=false to exclude) // Note: use || instead of ?? to treat empty string as unset (defaulting to true) const noIgnore = isEnvTruthy(process.env.CLAUDE_CODE_GLOB_NO_IGNORE || 'true') const hidden = isEnvTruthy(process.env.CLAUDE_CODE_GLOB_HIDDEN || 'true') const args = [ '--files', '--glob', searchPattern, '--sort=modified', ...(noIgnore ? ['--no-ignore'] : []), ...(hidden ? ['--hidden'] : []), ] // Add ignore patterns for (const pattern of ignorePatterns) { args.push('--glob', `!${pattern}`) } // Exclude orphaned plugin version directories for (const exclusion of await getGlobExclusionsForPluginCache(searchDir)) { args.push('--glob', exclusion) } const allPaths = await ripGrep(args, searchDir, abortSignal) // ripgrep returns relative paths, convert to absolute const absolutePaths = allPaths.map(p => isAbsolute(p) ? p : join(searchDir, p), ) const truncated = absolutePaths.length > offset + limit const files = absolutePaths.slice(offset, offset + limit) return { files, truncated } }