Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
21 kB · 600 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601import { feature } from 'bun:bundle'import { statSync } from 'fs'import { lstat, readdir, readFile, realpath, stat } from 'fs/promises'import memoize from 'lodash-es/memoize.js'import { homedir } from 'os'import { dirname, join, resolve, sep } from 'path'import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent,} from 'src/services/analytics/index.js'import { getProjectRoot } from '../bootstrap/state.js'import { logForDebugging } from './debug.js'import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js'import { isFsInaccessible } from './errors.js'import { normalizePathForComparison } from './file.js'import type { FrontmatterData } from './frontmatterParser.js'import { parseFrontmatter } from './frontmatterParser.js'import { findCanonicalGitRoot, findGitRoot } from './git.js'import { parseToolListFromCLI } from './permissions/permissionSetup.js'import { ripGrep } from './ripgrep.js'import { isSettingSourceEnabled, type SettingSource,} from './settings/constants.js'import { getManagedFilePath } from './settings/managedPath.js'import { isRestrictedToPluginOnly } from './settings/pluginOnlyPolicy.js'
// Claude configuration directory namesexport const CLAUDE_CONFIG_DIRECTORIES = [ 'commands', 'agents', 'output-styles', 'skills', 'workflows', ...(feature('TEMPLATES') ? (['templates'] as const) : []),] as const
export type ClaudeConfigDirectory = (typeof CLAUDE_CONFIG_DIRECTORIES)[number]
export type MarkdownFile = { filePath: string baseDir: string frontmatter: FrontmatterData content: string source: SettingSource}
/** * Extracts a description from markdown content * Uses the first non-empty line as the description, or falls back to a default */export function extractDescriptionFromMarkdown( content: string, defaultDescription: string = 'Custom item',): string { const lines = content.split('\n') for (const line of lines) { const trimmed = line.trim() if (trimmed) { // If it's a header, strip the header prefix const headerMatch = trimmed.match(/^#+\s+(.+)$/) const text = headerMatch?.[1] ?? trimmed
// Return the text, limited to reasonable length return text.length > 100 ? text.substring(0, 97) + '...' : text } } return defaultDescription}
/** * Parses tools from frontmatter, supporting both string and array formats * Always returns a string array for consistency * @param toolsValue The value from frontmatter * @returns Parsed tool list as string[] */function parseToolListString(toolsValue: unknown): string[] | null { // Return null for missing/null - let caller decide the default if (toolsValue === undefined || toolsValue === null) { return null }
// Empty string or other falsy values mean no tools if (!toolsValue) { return [] }
let toolsArray: string[] = [] if (typeof toolsValue === 'string') { toolsArray = [toolsValue] } else if (Array.isArray(toolsValue)) { toolsArray = toolsValue.filter( (item): item is string => typeof item === 'string', ) }
if (toolsArray.length === 0) { return [] }
const parsedTools = parseToolListFromCLI(toolsArray) if (parsedTools.includes('*')) { return ['*'] } return parsedTools}
/** * Parse tools from agent frontmatter * Missing field = undefined (all tools) * Empty field = [] (no tools) */export function parseAgentToolsFromFrontmatter( toolsValue: unknown,): string[] | undefined { const parsed = parseToolListString(toolsValue) if (parsed === null) { // For agents: undefined = all tools (undefined), null = no tools ([]) return toolsValue === undefined ? undefined : [] } // If parsed contains '*', return undefined (all tools) if (parsed.includes('*')) { return undefined } return parsed}
/** * Parse allowed-tools from slash command frontmatter * Missing or empty field = no tools ([]) */export function parseSlashCommandToolsFromFrontmatter( toolsValue: unknown,): string[] { const parsed = parseToolListString(toolsValue) if (parsed === null) { return [] } return parsed}
/** * Gets a unique identifier for a file based on its device ID and inode. * This allows detection of duplicate files accessed through different paths * (e.g., via symlinks). Returns null if the file doesn't exist or can't be stat'd. * * Note: On Windows, dev and ino may not be reliable for all file systems. * The code handles this gracefully by returning null on error (fail open), * meaning deduplication may not work on some Windows configurations. * * Uses bigint: true to handle filesystems with large inodes (e.g., ExFAT) * that exceed JavaScript's Number precision (53 bits). Without bigint, different * large inodes can round to the same Number, causing false duplicate detection. * See: https://github.com/anthropics/claude-code/issues/13893 * * @param filePath - Path to the file * @returns A string identifier "device:inode" or null if file can't be identified */async function getFileIdentity(filePath: string): Promise<string | null> { try { const stats = await lstat(filePath, { bigint: true }) // Some filesystems (NFS, FUSE, network mounts) report dev=0 and ino=0 // for all files, which would cause every file to look like a duplicate. // Return null to skip deduplication for these unreliable identities. if (stats.dev === 0n && stats.ino === 0n) { return null } return `${stats.dev}:${stats.ino}` } catch { return null }}
/** * Compute the stop boundary for getProjectDirsUpToHome's upward walk. * * Normally the walk stops at the nearest `.git` above `cwd`. But if the Bash * tool has cd'd into a nested git repo inside the session's project (submodule, * vendored dep with its own `.git`), that nested root isn't the right boundary — * stopping there makes the parent project's `.claude/` unreachable (#31905). * * The boundary is widened to the session's git root only when BOTH: * - the nearest `.git` from cwd belongs to a *different* canonical repo * (submodule/vendored clone — not a worktree, which resolves back to main) * - that nearest `.git` sits *inside* the session's project tree * * Worktrees (under `.claude/worktrees/`) stay on the old behavior: their `.git` * file is the stop, and loadMarkdownFilesForSubdir's fallback adds the main-repo * copy only when the worktree lacks one. */function resolveStopBoundary(cwd: string): string | null { const cwdGitRoot = findGitRoot(cwd) const sessionGitRoot = findGitRoot(getProjectRoot()) if (!cwdGitRoot || !sessionGitRoot) { return cwdGitRoot } // findCanonicalGitRoot resolves worktree `.git` files to the main repo. // Submodules (no commondir) and standalone clones fall through unchanged. const cwdCanonical = findCanonicalGitRoot(cwd) if ( cwdCanonical && normalizePathForComparison(cwdCanonical) === normalizePathForComparison(sessionGitRoot) ) { // Same canonical repo (main, or a worktree of main). Stop at nearest .git. return cwdGitRoot } // Different canonical repo. Is it nested *inside* the session's project? const nCwdGitRoot = normalizePathForComparison(cwdGitRoot) const nSessionRoot = normalizePathForComparison(sessionGitRoot) if ( nCwdGitRoot !== nSessionRoot && nCwdGitRoot.startsWith(nSessionRoot + sep) ) { // Nested repo inside the project — skip past it, stop at the project's root. return sessionGitRoot } // Sibling repo or elsewhere. Stop at nearest .git (old behavior). return cwdGitRoot}
/** * Traverses from the current directory up to the git root (or home directory if not in a git repo), * collecting all .claude directories along the way. * * Stopping at git root prevents commands/skills from parent directories outside the repository * from leaking into projects. For example, if ~/projects/.claude/commands/ exists, it won't * appear in ~/projects/my-repo/ if my-repo is a git repository. * * @param subdir Subdirectory (eg. "commands", "agents") * @param cwd Current working directory to start from * @returns Array of directory paths containing .claude/subdir, from most specific (cwd) to least specific */export function getProjectDirsUpToHome( subdir: ClaudeConfigDirectory, cwd: string,): string[] { const home = resolve(homedir()).normalize('NFC') const gitRoot = resolveStopBoundary(cwd) let current = resolve(cwd) const dirs: string[] = []
// Traverse from current directory up to git root (or home if not in a git repo) while (true) { // Stop if we've reached the home directory (don't check it, as it's loaded separately as userDir) // Use normalized comparison to handle Windows drive letter casing (C:\ vs c:\) if ( normalizePathForComparison(current) === normalizePathForComparison(home) ) { break }
const claudeSubdir = join(current, '.claude', subdir) // Filter to existing dirs. This is a perf filter (avoids spawning // ripgrep on non-existent dirs downstream) and the worktree fallback // in loadMarkdownFilesForSubdir relies on it. statSync + explicit error // handling instead of existsSync — re-throws unexpected errors rather // than silently swallowing them. Downstream loadMarkdownFiles handles // the TOCTOU window (dir disappearing before read) gracefully. try { statSync(claudeSubdir) dirs.push(claudeSubdir) } catch (e: unknown) { if (!isFsInaccessible(e)) throw e }
// Stop after processing the git root directory - this prevents commands from parent // directories outside the repository from appearing in the project if ( gitRoot && normalizePathForComparison(current) === normalizePathForComparison(gitRoot) ) { break }
// Move to parent directory const parent = dirname(current)
// Safety check: if parent is the same as current, we've reached the root if (parent === current) { break }
current = parent }
return dirs}
/** * Loads markdown files from managed, user, and project directories * @param subdir Subdirectory (eg. "agents" or "commands") * @param cwd Current working directory for project directory traversal * @returns Array of parsed markdown files with metadata */export const loadMarkdownFilesForSubdir = memoize( async function ( subdir: ClaudeConfigDirectory, cwd: string, ): Promise<MarkdownFile[]> { const searchStartTime = Date.now() const userDir = join(getClaudeConfigHomeDir(), subdir) const managedDir = join(getManagedFilePath(), '.claude', subdir) const projectDirs = getProjectDirsUpToHome(subdir, cwd)
// For git worktrees where the worktree does NOT have .claude/<subdir> checked // out (e.g. sparse-checkout), fall back to the main repository's copy. // getProjectDirsUpToHome stops at the worktree root (where the .git file is), // so it never sees the main repo on its own. // // Only add the main repo's copy when the worktree root's .claude/<subdir> // is absent. A standard `git worktree add` checks out the full tree, so the // worktree already has identical .claude/<subdir> content — loading the main // repo's copy too would duplicate every command/agent/skill // (anthropics/claude-code#29599, #28182, #26992). // // projectDirs already reflects existence (getProjectDirsUpToHome checked // each dir), so we compare against that instead of stat'ing again. const gitRoot = findGitRoot(cwd) const canonicalRoot = findCanonicalGitRoot(cwd) if (gitRoot && canonicalRoot && canonicalRoot !== gitRoot) { const worktreeSubdir = normalizePathForComparison( join(gitRoot, '.claude', subdir), ) const worktreeHasSubdir = projectDirs.some( dir => normalizePathForComparison(dir) === worktreeSubdir, ) if (!worktreeHasSubdir) { const mainClaudeSubdir = join(canonicalRoot, '.claude', subdir) if (!projectDirs.includes(mainClaudeSubdir)) { projectDirs.push(mainClaudeSubdir) } } }
const [managedFiles, userFiles, projectFilesNested] = await Promise.all([ // Always load managed (policy settings) loadMarkdownFiles(managedDir).then(_ => _.map(file => ({ ...file, baseDir: managedDir, source: 'policySettings' as const, })), ), // Conditionally load user files isSettingSourceEnabled('userSettings') && !(subdir === 'agents' && isRestrictedToPluginOnly('agents')) ? loadMarkdownFiles(userDir).then(_ => _.map(file => ({ ...file, baseDir: userDir, source: 'userSettings' as const, })), ) : Promise.resolve([]), // Conditionally load project files from all directories up to home isSettingSourceEnabled('projectSettings') && !(subdir === 'agents' && isRestrictedToPluginOnly('agents')) ? Promise.all( projectDirs.map(projectDir => loadMarkdownFiles(projectDir).then(_ => _.map(file => ({ ...file, baseDir: projectDir, source: 'projectSettings' as const, })), ), ), ) : Promise.resolve([]), ])
// Flatten nested project files array const projectFiles = projectFilesNested.flat()
// Combine all files with priority: managed > user > project const allFiles = [...managedFiles, ...userFiles, ...projectFiles]
// Deduplicate files that resolve to the same physical file (same inode). // This prevents the same file from appearing multiple times when ~/.claude is // symlinked to a directory within the project hierarchy, causing the same // physical file to be discovered through different paths. const fileIdentities = await Promise.all( allFiles.map(file => getFileIdentity(file.filePath)), )
const seenFileIds = new Map<string, SettingSource>() const deduplicatedFiles: MarkdownFile[] = []
for (const [i, file] of allFiles.entries()) { const fileId = fileIdentities[i] ?? null if (fileId === null) { // If we can't identify the file, include it (fail open) deduplicatedFiles.push(file) continue } const existingSource = seenFileIds.get(fileId) if (existingSource !== undefined) { logForDebugging( `Skipping duplicate file '${file.filePath}' from ${file.source} (same inode already loaded from ${existingSource})`, ) continue } seenFileIds.set(fileId, file.source) deduplicatedFiles.push(file) }
const duplicatesRemoved = allFiles.length - deduplicatedFiles.length if (duplicatesRemoved > 0) { logForDebugging( `Deduplicated ${duplicatesRemoved} files in ${subdir} (same inode via symlinks or hard links)`, ) }
logEvent(`tengu_dir_search`, { durationMs: Date.now() - searchStartTime, managedFilesFound: managedFiles.length, userFilesFound: userFiles.length, projectFilesFound: projectFiles.length, projectDirsSearched: projectDirs.length, subdir: subdir as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, })
return deduplicatedFiles }, // Custom resolver creates cache key from both subdir and cwd parameters (subdir: ClaudeConfigDirectory, cwd: string) => `${subdir}:${cwd}`,)
/** * Native implementation to find markdown files using Node.js fs APIs * * This implementation exists alongside ripgrep for the following reasons: * 1. Ripgrep has poor startup performance in native builds (noticeable on app startup) * 2. Provides a fallback when ripgrep is unavailable * 3. Can be explicitly enabled via CLAUDE_CODE_USE_NATIVE_FILE_SEARCH env var * * Symlink handling: * - Follows symlinks (equivalent to ripgrep's --follow flag) * - Uses device+inode tracking to detect cycles (same as ripgrep's same_file library) * - Falls back to realpath on systems without inode support * * Does not respect .gitignore (matches ripgrep with --no-ignore flag) * * @param dir Directory to search * @param signal AbortSignal for timeout * @returns Array of file paths */async function findMarkdownFilesNative( dir: string, signal: AbortSignal,): Promise<string[]> { const files: string[] = [] const visitedDirs = new Set<string>()
async function walk(currentDir: string): Promise<void> { if (signal.aborted) { return }
// Cycle detection: track visited directories by device+inode // Uses bigint: true to handle filesystems with large inodes (e.g., ExFAT) // that exceed JavaScript's Number precision (53 bits). // See: https://github.com/anthropics/claude-code/issues/13893 try { const stats = await stat(currentDir, { bigint: true }) if (stats.isDirectory()) { const dirKey = stats.dev !== undefined && stats.ino !== undefined ? `${stats.dev}:${stats.ino}` // Unix/Linux: device + inode : await realpath(currentDir) // Windows: canonical path
if (visitedDirs.has(dirKey)) { logForDebugging( `Skipping already visited directory (circular symlink): ${currentDir}`, ) return } visitedDirs.add(dirKey) } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) logForDebugging(`Failed to stat directory ${currentDir}: ${errorMessage}`) return }
try { const entries = await readdir(currentDir, { withFileTypes: true })
for (const entry of entries) { if (signal.aborted) { break }
const fullPath = join(currentDir, entry.name)
try { // Handle symlinks: isFile() and isDirectory() return false for symlinks if (entry.isSymbolicLink()) { try { const stats = await stat(fullPath) // stat() follows symlinks if (stats.isDirectory()) { await walk(fullPath) } else if (stats.isFile() && entry.name.endsWith('.md')) { files.push(fullPath) } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) logForDebugging( `Failed to follow symlink ${fullPath}: ${errorMessage}`, ) } } else if (entry.isDirectory()) { await walk(fullPath) } else if (entry.isFile() && entry.name.endsWith('.md')) { files.push(fullPath) } } catch (error) { // Skip files/directories we can't access const errorMessage = error instanceof Error ? error.message : String(error) logForDebugging(`Failed to access ${fullPath}: ${errorMessage}`) } } } catch (error) { // If readdir fails (e.g., permission denied), log and continue const errorMessage = error instanceof Error ? error.message : String(error) logForDebugging(`Failed to read directory ${currentDir}: ${errorMessage}`) } }
await walk(dir) return files}
/** * Generic function to load markdown files from specified directories * @param dir Directory (eg. "~/.claude/commands") * @returns Array of parsed markdown files with metadata */async function loadMarkdownFiles(dir: string): Promise< { filePath: string frontmatter: FrontmatterData content: string }[]> { // File search strategy: // - Default: ripgrep (faster, battle-tested) // - Fallback: native Node.js (when CLAUDE_CODE_USE_NATIVE_FILE_SEARCH is set) // // Why both? Ripgrep has poor startup performance in native builds. const useNative = isEnvTruthy(process.env.CLAUDE_CODE_USE_NATIVE_FILE_SEARCH) const signal = AbortSignal.timeout(3000) let files: string[] try { files = useNative ? await findMarkdownFilesNative(dir, signal) : await ripGrep( ['--files', '--hidden', '--follow', '--no-ignore', '--glob', '*.md'], dir, signal, ) } catch (e: unknown) { // Handle missing/inaccessible dir directly instead of pre-checking // existence (TOCTOU). findMarkdownFilesNative already catches internally; // ripGrep rejects on inaccessible target paths. if (isFsInaccessible(e)) return [] throw e }
const results = await Promise.all( files.map(async filePath => { try { const rawContent = await readFile(filePath, { encoding: 'utf-8' }) const { frontmatter, content } = parseFrontmatter(rawContent, filePath)
return { filePath, frontmatter, content, } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) logForDebugging( `Failed to read/parse markdown file: ${filePath}: ${errorMessage}`, ) return null } }), )
return results.filter(_ => _ !== null)}