Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
46 kB · 1479 lines
TypeScript
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480/** * Files are loaded in the following order: * * 1. Managed memory (eg. /etc/claude-code/CLAUDE.md) - Global instructions for all users * 2. User memory (~/.claude/CLAUDE.md) - Private global instructions for all projects * 3. Project memory (CLAUDE.md, .claude/CLAUDE.md, and .claude/rules/*.md in project roots) - Instructions checked into the codebase * 4. Local memory (CLAUDE.local.md in project roots) - Private project-specific instructions * * Files are loaded in reverse order of priority, i.e. the latest files are highest priority * with the model paying more attention to them. * * File discovery: * - User memory is loaded from the user's home directory * - Project and Local files are discovered by traversing from the current directory up to root * - Files closer to the current directory have higher priority (loaded later) * - CLAUDE.md, .claude/CLAUDE.md, and all .md files in .claude/rules/ are checked in each directory for Project memory * * Memory @include directive: * - Memory files can include other files using @ notation * - Syntax: @path, @./relative/path, @~/home/path, or @/absolute/path * - @path (without prefix) is treated as a relative path (same as @./path) * - Works in leaf text nodes only (not inside code blocks or code strings) * - Included files are added as separate entries before the including file * - Circular references are prevented by tracking processed files * - Non-existent files are silently ignored */
import { feature } from 'bun:bundle'import ignore from 'ignore'import memoize from 'lodash-es/memoize.js'import { Lexer } from 'marked'import { basename, dirname, extname, isAbsolute, join, parse, relative, sep,} from 'path'import picomatch from 'picomatch'import { logEvent } from 'src/services/analytics/index.js'import { getAdditionalDirectoriesForClaudeMd, getOriginalCwd,} from '../bootstrap/state.js'import { truncateEntrypointContent } from '../memdir/memdir.js'import { getAutoMemEntrypoint, isAutoMemoryEnabled } from '../memdir/paths.js'import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'import { getCurrentProjectConfig, getManagedClaudeRulesDir, getMemoryPath, getUserClaudeRulesDir,} from './config.js'import { logForDebugging } from './debug.js'import { logForDiagnosticsNoPII } from './diagLogs.js'import { getClaudeConfigHomeDir, isEnvTruthy } from './envUtils.js'import { getErrnoCode } from './errors.js'import { normalizePathForComparison } from './file.js'import { cacheKeys, type FileStateCache } from './fileStateCache.js'import { parseFrontmatter, splitPathInFrontmatter,} from './frontmatterParser.js'import { getFsImplementation, safeResolvePath } from './fsOperations.js'import { findCanonicalGitRoot, findGitRoot } from './git.js'import { executeInstructionsLoadedHooks, hasInstructionsLoadedHook, type InstructionsLoadReason, type InstructionsMemoryType,} from './hooks.js'import type { MemoryType } from './memory/types.js'import { expandPath } from './path.js'import { pathInWorkingPath } from './permissions/filesystem.js'import { isSettingSourceEnabled } from './settings/constants.js'import { getInitialSettings } from './settings/settings.js'
/* eslint-disable @typescript-eslint/no-require-imports */const teamMemPaths = feature('TEAMMEM') ? (require('../memdir/teamMemPaths.js') as typeof import('../memdir/teamMemPaths.js')) : null/* eslint-enable @typescript-eslint/no-require-imports */
let hasLoggedInitialLoad = false
const MEMORY_INSTRUCTION_PROMPT = 'Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.'// Recommended max character count for a memory fileexport const MAX_MEMORY_CHARACTER_COUNT = 40000
// File extensions that are allowed for @include directives// This prevents binary files (images, PDFs, etc.) from being loaded into memoryconst TEXT_FILE_EXTENSIONS = new Set([ // Markdown and text '.md', '.txt', '.text', // Data formats '.json', '.yaml', '.yml', '.toml', '.xml', '.csv', // Web '.html', '.htm', '.css', '.scss', '.sass', '.less', // JavaScript/TypeScript '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.mts', '.cts', // Python '.py', '.pyi', '.pyw', // Ruby '.rb', '.erb', '.rake', // Go '.go', // Rust '.rs', // Java/Kotlin/Scala '.java', '.kt', '.kts', '.scala', // C/C++ '.c', '.cpp', '.cc', '.cxx', '.h', '.hpp', '.hxx', // C# '.cs', // Swift '.swift', // Shell '.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd', // Config '.env', '.ini', '.cfg', '.conf', '.config', '.properties', // Database '.sql', '.graphql', '.gql', // Protocol '.proto', // Frontend frameworks '.vue', '.svelte', '.astro', // Templating '.ejs', '.hbs', '.pug', '.jade', // Other languages '.php', '.pl', '.pm', '.lua', '.r', '.R', '.dart', '.ex', '.exs', '.erl', '.hrl', '.clj', '.cljs', '.cljc', '.edn', '.hs', '.lhs', '.elm', '.ml', '.mli', '.f', '.f90', '.f95', '.for', // Build files '.cmake', '.make', '.makefile', '.gradle', '.sbt', // Documentation '.rst', '.adoc', '.asciidoc', '.org', '.tex', '.latex', // Lock files (often text-based) '.lock', // Misc '.log', '.diff', '.patch',])
export type MemoryFileInfo = { path: string type: MemoryType content: string parent?: string // Path of the file that included this one globs?: string[] // Glob patterns for file paths this rule applies to // True when auto-injection transformed `content` (stripped HTML comments, // stripped frontmatter, truncated MEMORY.md) such that it no longer matches // the bytes on disk. When set, `rawContent` holds the unmodified disk bytes // so callers can cache a `isPartialView` readFileState entry — presence in // cache provides dedup + change detection, but Edit/Write still require an // explicit Read before proceeding. contentDiffersFromDisk?: boolean rawContent?: string}
function pathInOriginalCwd(path: string): boolean { return pathInWorkingPath(path, getOriginalCwd())}
/** * Parses raw content to extract both content and glob patterns from frontmatter * @param rawContent Raw file content with frontmatter * @returns Object with content and globs (undefined if no paths or match-all pattern) */function parseFrontmatterPaths(rawContent: string): { content: string paths?: string[]} { const { frontmatter, content } = parseFrontmatter(rawContent)
if (!frontmatter.paths) { return { content } }
const patterns = splitPathInFrontmatter(frontmatter.paths) .map(pattern => { // Remove /** suffix - ignore library treats 'path' as matching both // the path itself and everything inside it return pattern.endsWith('/**') ? pattern.slice(0, -3) : pattern }) .filter((p: string) => p.length > 0)
// If all patterns are ** (match-all), treat as no globs (undefined) // This means the file applies to all paths if (patterns.length === 0 || patterns.every((p: string) => p === '**')) { return { content } }
return { content, paths: patterns }}
/** * Strip block-level HTML comments (<!-- ... -->) from markdown content. * * Uses the marked lexer to identify comments at the block level only, so * comments inside inline code spans and fenced code blocks are preserved. * Inline HTML comments inside a paragraph are also left intact; the intended * use case is authorial notes that occupy their own lines. * * Unclosed comments (`<!--` with no matching `-->`) are left in place so a * typo doesn't silently swallow the rest of the file. */export function stripHtmlComments(content: string): { content: string stripped: boolean} { if (!content.includes('<!--')) { return { content, stripped: false } } // gfm:false is fine here — html-block detection is a CommonMark rule. return stripHtmlCommentsFromTokens(new Lexer({ gfm: false }).lex(content))}
function stripHtmlCommentsFromTokens(tokens: ReturnType<Lexer['lex']>): { content: string stripped: boolean} { let result = '' let stripped = false
// A well-formed HTML comment span. Non-greedy so multiple comments on the // same line are matched independently; [\s\S] to span newlines. const commentSpan = /<!--[\s\S]*?-->/g
for (const token of tokens) { if (token.type === 'html') { const trimmed = token.raw.trimStart() if (trimmed.startsWith('<!--') && trimmed.includes('-->')) { // Per CommonMark, a type-2 HTML block ends at the *line* containing // `-->`, so text after `-->` on that line is part of this token. // Strip only the comment spans and keep any residual content. const residue = token.raw.replace(commentSpan, '') stripped = true if (residue.trim().length > 0) { // Residual content exists (e.g. `<!-- note --> Use bun`): keep it. result += residue } continue } } result += token.raw }
return { content: result, stripped }}
/** * Parses raw memory file content into a MemoryFileInfo. Pure function — no I/O. * * When includeBasePath is given, @include paths are resolved in the same lex * pass and returned alongside the parsed file (so processMemoryFile doesn't * need to lex the same content a second time). */function parseMemoryFileContent( rawContent: string, filePath: string, type: MemoryType, includeBasePath?: string,): { info: MemoryFileInfo | null; includePaths: string[] } { // Skip non-text files to prevent loading binary data (images, PDFs, etc.) into memory const ext = extname(filePath).toLowerCase() if (ext && !TEXT_FILE_EXTENSIONS.has(ext)) { logForDebugging(`Skipping non-text file in @include: ${filePath}`) return { info: null, includePaths: [] } }
const { content: withoutFrontmatter, paths } = parseFrontmatterPaths(rawContent)
// Lex once so strip and @include-extract share the same tokens. gfm:false // is required by extract (so ~/path doesn't tokenize as strikethrough) and // doesn't affect strip (html blocks are a CommonMark rule). const hasComment = withoutFrontmatter.includes('<!--') const tokens = hasComment || includeBasePath !== undefined ? new Lexer({ gfm: false }).lex(withoutFrontmatter) : undefined
// Only rebuild via tokens when a comment actually needs stripping — // marked normalises \r\n during lex, so round-tripping a CRLF file // through token.raw would spuriously flip contentDiffersFromDisk. const strippedContent = hasComment && tokens ? stripHtmlCommentsFromTokens(tokens).content : withoutFrontmatter
const includePaths = tokens && includeBasePath !== undefined ? extractIncludePathsFromTokens(tokens, includeBasePath) : []
// Truncate MEMORY.md entrypoints to the line AND byte caps let finalContent = strippedContent if (type === 'AutoMem' || type === 'TeamMem') { finalContent = truncateEntrypointContent(strippedContent).content }
// Covers frontmatter strip, HTML comment strip, and MEMORY.md truncation const contentDiffersFromDisk = finalContent !== rawContent return { info: { path: filePath, type, content: finalContent, globs: paths, contentDiffersFromDisk, rawContent: contentDiffersFromDisk ? rawContent : undefined, }, includePaths, }}
function handleMemoryFileReadError(error: unknown, filePath: string): void { const code = getErrnoCode(error) // ENOENT = file doesn't exist, EISDIR = is a directory — both expected if (code === 'ENOENT' || code === 'EISDIR') { return } // Log permission errors (EACCES) as they're actionable if (code === 'EACCES') { // Don't log the full file path to avoid PII/security issues logEvent('tengu_claude_md_permission_error', { is_access_error: 1, has_home_dir: filePath.includes(getClaudeConfigHomeDir()) ? 1 : 0, }) }}
/** * Used by processMemoryFile → getMemoryFiles so the event loop stays * responsive during the directory walk (many readFile attempts, most * ENOENT). When includeBasePath is given, @include paths are resolved in * the same lex pass and returned alongside the parsed file. */async function safelyReadMemoryFileAsync( filePath: string, type: MemoryType, includeBasePath?: string,): Promise<{ info: MemoryFileInfo | null; includePaths: string[] }> { try { const fs = getFsImplementation() const rawContent = await fs.readFile(filePath, { encoding: 'utf-8' }) return parseMemoryFileContent(rawContent, filePath, type, includeBasePath) } catch (error) { handleMemoryFileReadError(error, filePath) return { info: null, includePaths: [] } }}
type MarkdownToken = { type: string text?: string href?: string tokens?: MarkdownToken[] raw?: string items?: MarkdownToken[]}
// Extract @path include references from pre-lexed tokens and resolve to// absolute paths. Skips html tokens so @paths inside block comments are// ignored — the caller may pass pre-strip tokens.function extractIncludePathsFromTokens( tokens: ReturnType<Lexer['lex']>, basePath: string,): string[] { const absolutePaths = new Set<string>()
// Extract @paths from a text string and add resolved paths to absolutePaths. function extractPathsFromText(textContent: string) { const includeRegex = /(?:^|\s)@((?:[^\s\\]|\\ )+)/g let match while ((match = includeRegex.exec(textContent)) !== null) { let path = match[1] if (!path) continue
// Strip fragment identifiers (#heading, #section-name, etc.) const hashIndex = path.indexOf('#') if (hashIndex !== -1) { path = path.substring(0, hashIndex) } if (!path) continue
// Unescape the spaces in the path path = path.replace(/\\ /g, ' ')
// Accept @path, @./path, @~/path, or @/path if (path) { const isValidPath = path.startsWith('./') || path.startsWith('~/') || (path.startsWith('/') && path !== '/') || (!path.startsWith('@') && !path.match(/^[#%^&*()]+/) && path.match(/^[a-zA-Z0-9._-]/))
if (isValidPath) { const resolvedPath = expandPath(path, dirname(basePath)) absolutePaths.add(resolvedPath) } } } }
// Recursively process elements to find text nodes function processElements(elements: MarkdownToken[]) { for (const element of elements) { if (element.type === 'code' || element.type === 'codespan') { continue }
// For html tokens that contain comments, strip the comment spans and // check the residual for @paths (e.g. `<!-- note --> @./file.md`). // Other html tokens (non-comment tags) are skipped entirely. if (element.type === 'html') { const raw = element.raw || '' const trimmed = raw.trimStart() if (trimmed.startsWith('<!--') && trimmed.includes('-->')) { const commentSpan = /<!--[\s\S]*?-->/g const residue = raw.replace(commentSpan, '') if (residue.trim().length > 0) { extractPathsFromText(residue) } } continue }
// Process text nodes if (element.type === 'text') { extractPathsFromText(element.text || '') }
// Recurse into children tokens if (element.tokens) { processElements(element.tokens) }
// Special handling for list structures if (element.items) { processElements(element.items) } } }
processElements(tokens as MarkdownToken[]) return [...absolutePaths]}
const MAX_INCLUDE_DEPTH = 5
/** * Checks whether a CLAUDE.md file path is excluded by the claudeMdExcludes setting. * Only applies to User, Project, and Local memory types. * Managed, AutoMem, and TeamMem types are never excluded. * * Matches both the original path and the realpath-resolved path to handle symlinks * (e.g., /tmp -> /private/tmp on macOS). */function isClaudeMdExcluded(filePath: string, type: MemoryType): boolean { if (type !== 'User' && type !== 'Project' && type !== 'Local') { return false }
const patterns = getInitialSettings().claudeMdExcludes if (!patterns || patterns.length === 0) { return false }
const matchOpts = { dot: true } const normalizedPath = filePath.replaceAll('\\', '/')
// Build an expanded pattern list that includes realpath-resolved versions of // absolute patterns. This handles symlinks like /tmp -> /private/tmp on macOS: // the user writes "/tmp/project/CLAUDE.md" in their exclude, but the system // resolves the CWD to "/private/tmp/project/...", so the file path uses the // real path. By resolving the patterns too, both sides match. const expandedPatterns = resolveExcludePatterns(patterns).filter( p => p.length > 0, ) if (expandedPatterns.length === 0) { return false }
return picomatch.isMatch(normalizedPath, expandedPatterns, matchOpts)}
/** * Expands exclude patterns by resolving symlinks in absolute path prefixes. * For each absolute pattern (starting with /), tries to resolve the longest * existing directory prefix via realpathSync and adds the resolved version. * Glob patterns (containing *) have their static prefix resolved. */function resolveExcludePatterns(patterns: string[]): string[] { const fs = getFsImplementation() const expanded: string[] = patterns.map(p => p.replaceAll('\\', '/'))
for (const normalized of expanded) { // Only resolve absolute patterns — glob-only patterns like "**/*.md" don't have // a filesystem prefix to resolve if (!normalized.startsWith('/')) { continue }
// Find the static prefix before any glob characters const globStart = normalized.search(/[*?{[]/) const staticPrefix = globStart === -1 ? normalized : normalized.slice(0, globStart) const dirToResolve = dirname(staticPrefix)
try { // sync IO: called from sync context (isClaudeMdExcluded -> processMemoryFile -> getMemoryFiles) const resolvedDir = fs.realpathSync(dirToResolve).replaceAll('\\', '/') if (resolvedDir !== dirToResolve) { const resolvedPattern = resolvedDir + normalized.slice(dirToResolve.length) expanded.push(resolvedPattern) } } catch { // Directory doesn't exist; skip resolution for this pattern } }
return expanded}
/** * Recursively processes a memory file and all its @include references * Returns an array of MemoryFileInfo objects with includes first, then main file */export async function processMemoryFile( filePath: string, type: MemoryType, processedPaths: Set<string>, includeExternal: boolean, depth: number = 0, parent?: string,): Promise<MemoryFileInfo[]> { // Skip if already processed or max depth exceeded. // Normalize paths for comparison to handle Windows drive letter casing // differences (e.g., C:\Users vs c:\Users). const normalizedPath = normalizePathForComparison(filePath) if (processedPaths.has(normalizedPath) || depth >= MAX_INCLUDE_DEPTH) { return [] }
// Skip if path is excluded by claudeMdExcludes setting if (isClaudeMdExcluded(filePath, type)) { return [] }
// Resolve symlink path early for @import resolution const { resolvedPath, isSymlink } = safeResolvePath( getFsImplementation(), filePath, )
processedPaths.add(normalizedPath) if (isSymlink) { processedPaths.add(normalizePathForComparison(resolvedPath)) }
const { info: memoryFile, includePaths: resolvedIncludePaths } = await safelyReadMemoryFileAsync(filePath, type, resolvedPath) if (!memoryFile || !memoryFile.content.trim()) { return [] }
// Add parent information if (parent) { memoryFile.parent = parent }
const result: MemoryFileInfo[] = []
// Add the main file first (parent before children) result.push(memoryFile)
for (const resolvedIncludePath of resolvedIncludePaths) { const isExternal = !pathInOriginalCwd(resolvedIncludePath) if (isExternal && !includeExternal) { continue }
// Recursively process included files with this file as parent const includedFiles = await processMemoryFile( resolvedIncludePath, type, processedPaths, includeExternal, depth + 1, filePath, // Pass current file as parent ) result.push(...includedFiles) }
return result}
/** * Processes all .md files in the .claude/rules/ directory and its subdirectories * @param rulesDir The path to the rules directory * @param type Type of memory file (User, Project, Local) * @param processedPaths Set of already processed file paths * @param includeExternal Whether to include external files * @param conditionalRule If true, only include files with frontmatter paths; if false, only include files without frontmatter paths * @param visitedDirs Set of already visited directory real paths (for cycle detection) * @returns Array of MemoryFileInfo objects */export async function processMdRules({ rulesDir, type, processedPaths, includeExternal, conditionalRule, visitedDirs = new Set(),}: { rulesDir: string type: MemoryType processedPaths: Set<string> includeExternal: boolean conditionalRule: boolean visitedDirs?: Set<string>}): Promise<MemoryFileInfo[]> { if (visitedDirs.has(rulesDir)) { return [] }
try { const fs = getFsImplementation()
const { resolvedPath: resolvedRulesDir, isSymlink } = safeResolvePath( fs, rulesDir, )
visitedDirs.add(rulesDir) if (isSymlink) { visitedDirs.add(resolvedRulesDir) }
const result: MemoryFileInfo[] = [] let entries: import('fs').Dirent[] try { entries = await fs.readdir(resolvedRulesDir) } catch (e: unknown) { const code = getErrnoCode(e) if (code === 'ENOENT' || code === 'EACCES' || code === 'ENOTDIR') { return [] } throw e }
for (const entry of entries) { const entryPath = join(rulesDir, entry.name) const { resolvedPath: resolvedEntryPath, isSymlink } = safeResolvePath( fs, entryPath, )
// Use Dirent methods for non-symlinks to avoid extra stat calls. // For symlinks, we need stat to determine what the target is. const stats = isSymlink ? await fs.stat(resolvedEntryPath) : null const isDirectory = stats ? stats.isDirectory() : entry.isDirectory() const isFile = stats ? stats.isFile() : entry.isFile()
if (isDirectory) { result.push( ...(await processMdRules({ rulesDir: resolvedEntryPath, type, processedPaths, includeExternal, conditionalRule, visitedDirs, })), ) } else if (isFile && entry.name.endsWith('.md')) { const files = await processMemoryFile( resolvedEntryPath, type, processedPaths, includeExternal, ) result.push( ...files.filter(f => (conditionalRule ? f.globs : !f.globs)), ) } }
return result } catch (error) { if (error instanceof Error && error.message.includes('EACCES')) { logEvent('tengu_claude_rules_md_permission_error', { is_access_error: 1, has_home_dir: rulesDir.includes(getClaudeConfigHomeDir()) ? 1 : 0, }) } return [] }}
export const getMemoryFiles = memoize( async (forceIncludeExternal: boolean = false): Promise<MemoryFileInfo[]> => { const startTime = Date.now() logForDiagnosticsNoPII('info', 'memory_files_started')
const result: MemoryFileInfo[] = [] const processedPaths = new Set<string>() const config = getCurrentProjectConfig() const includeExternal = forceIncludeExternal || config.hasClaudeMdExternalIncludesApproved || false
// Process Managed file first (always loaded - policy settings) const managedClaudeMd = getMemoryPath('Managed') result.push( ...(await processMemoryFile( managedClaudeMd, 'Managed', processedPaths, includeExternal, )), ) // Process Managed .claude/rules/*.md files const managedClaudeRulesDir = getManagedClaudeRulesDir() result.push( ...(await processMdRules({ rulesDir: managedClaudeRulesDir, type: 'Managed', processedPaths, includeExternal, conditionalRule: false, })), )
// Process User file (only if userSettings is enabled) if (isSettingSourceEnabled('userSettings')) { const userClaudeMd = getMemoryPath('User') result.push( ...(await processMemoryFile( userClaudeMd, 'User', processedPaths, true, // User memory can always include external files )), ) // Process User ~/.claude/rules/*.md files const userClaudeRulesDir = getUserClaudeRulesDir() result.push( ...(await processMdRules({ rulesDir: userClaudeRulesDir, type: 'User', processedPaths, includeExternal: true, conditionalRule: false, })), ) }
// Then process Project and Local files const dirs: string[] = [] const originalCwd = getOriginalCwd() let currentDir = originalCwd
while (currentDir !== parse(currentDir).root) { dirs.push(currentDir) currentDir = dirname(currentDir) }
// When running from a git worktree nested inside its main repo (e.g., // .claude/worktrees/<name>/ from `claude -w`), the upward walk passes // through both the worktree root and the main repo root. Both contain // checked-in files like CLAUDE.md and .claude/rules/*.md, so the same // content gets loaded twice. Skip Project-type (checked-in) files from // directories above the worktree but within the main repo — the worktree // already has its own checkout. CLAUDE.local.md is gitignored so it only // exists in the main repo and is still loaded. // See: https://github.com/anthropics/claude-code/issues/29599 const gitRoot = findGitRoot(originalCwd) const canonicalRoot = findCanonicalGitRoot(originalCwd) const isNestedWorktree = gitRoot !== null && canonicalRoot !== null && normalizePathForComparison(gitRoot) !== normalizePathForComparison(canonicalRoot) && pathInWorkingPath(gitRoot, canonicalRoot)
// Process from root downward to CWD for (const dir of dirs.reverse()) { // In a nested worktree, skip checked-in files from the main repo's // working tree (dirs inside canonicalRoot but outside the worktree). const skipProject = isNestedWorktree && pathInWorkingPath(dir, canonicalRoot) && !pathInWorkingPath(dir, gitRoot)
// Try reading CLAUDE.md (Project) - only if projectSettings is enabled if (isSettingSourceEnabled('projectSettings') && !skipProject) { const projectPath = join(dir, 'CLAUDE.md') result.push( ...(await processMemoryFile( projectPath, 'Project', processedPaths, includeExternal, )), )
// Try reading .claude/CLAUDE.md (Project) const dotClaudePath = join(dir, '.claude', 'CLAUDE.md') result.push( ...(await processMemoryFile( dotClaudePath, 'Project', processedPaths, includeExternal, )), )
// Try reading .claude/rules/*.md files (Project) const rulesDir = join(dir, '.claude', 'rules') result.push( ...(await processMdRules({ rulesDir, type: 'Project', processedPaths, includeExternal, conditionalRule: false, })), ) }
// Try reading CLAUDE.local.md (Local) - only if localSettings is enabled if (isSettingSourceEnabled('localSettings')) { const localPath = join(dir, 'CLAUDE.local.md') result.push( ...(await processMemoryFile( localPath, 'Local', processedPaths, includeExternal, )), ) } }
// Process CLAUDE.md from additional directories (--add-dir) if env var is enabled // This is controlled by CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD and defaults to off // Note: we don't check isSettingSourceEnabled('projectSettings') here because --add-dir // is an explicit user action and the SDK defaults settingSources to [] when not specified if (isEnvTruthy(process.env.CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD)) { const additionalDirs = getAdditionalDirectoriesForClaudeMd() for (const dir of additionalDirs) { // Try reading CLAUDE.md from the additional directory const projectPath = join(dir, 'CLAUDE.md') result.push( ...(await processMemoryFile( projectPath, 'Project', processedPaths, includeExternal, )), )
// Try reading .claude/CLAUDE.md from the additional directory const dotClaudePath = join(dir, '.claude', 'CLAUDE.md') result.push( ...(await processMemoryFile( dotClaudePath, 'Project', processedPaths, includeExternal, )), )
// Try reading .claude/rules/*.md files from the additional directory const rulesDir = join(dir, '.claude', 'rules') result.push( ...(await processMdRules({ rulesDir, type: 'Project', processedPaths, includeExternal, conditionalRule: false, })), ) } }
// Memdir entrypoint (memory.md) - only if feature is on and file exists if (isAutoMemoryEnabled()) { const { info: memdirEntry } = await safelyReadMemoryFileAsync( getAutoMemEntrypoint(), 'AutoMem', ) if (memdirEntry) { const normalizedPath = normalizePathForComparison(memdirEntry.path) if (!processedPaths.has(normalizedPath)) { processedPaths.add(normalizedPath) result.push(memdirEntry) } } }
// Team memory entrypoint - only if feature is on and file exists if (feature('TEAMMEM') && teamMemPaths!.isTeamMemoryEnabled()) { const { info: teamMemEntry } = await safelyReadMemoryFileAsync( teamMemPaths!.getTeamMemEntrypoint(), 'TeamMem', ) if (teamMemEntry) { const normalizedPath = normalizePathForComparison(teamMemEntry.path) if (!processedPaths.has(normalizedPath)) { processedPaths.add(normalizedPath) result.push(teamMemEntry) } } }
const totalContentLength = result.reduce( (sum, f) => sum + f.content.length, 0, )
logForDiagnosticsNoPII('info', 'memory_files_completed', { duration_ms: Date.now() - startTime, file_count: result.length, total_content_length: totalContentLength, })
const typeCounts: Record<string, number> = {} for (const f of result) { typeCounts[f.type] = (typeCounts[f.type] ?? 0) + 1 }
if (!hasLoggedInitialLoad) { hasLoggedInitialLoad = true logEvent('tengu_claudemd__initial_load', { file_count: result.length, total_content_length: totalContentLength, user_count: typeCounts['User'] ?? 0, project_count: typeCounts['Project'] ?? 0, local_count: typeCounts['Local'] ?? 0, managed_count: typeCounts['Managed'] ?? 0, automem_count: typeCounts['AutoMem'] ?? 0, ...(feature('TEAMMEM') ? { teammem_count: typeCounts['TeamMem'] ?? 0 } : {}), duration_ms: Date.now() - startTime, }) }
// Fire InstructionsLoaded hook for each instruction file loaded // (fire-and-forget, audit/observability only). // AutoMem/TeamMem are intentionally excluded — they're a separate // memory system, not "instructions" in the CLAUDE.md/rules sense. // Gated on !forceIncludeExternal: the forceIncludeExternal=true variant // is only used by getExternalClaudeMdIncludes() for approval checks, not // for building context — firing the hook there would double-fire on startup. // The one-shot flag is consumed on every !forceIncludeExternal cache miss // (NOT gated on hasInstructionsLoadedHook) so the flag is released even // when no hook is configured — otherwise a mid-session hook registration // followed by a direct .cache.clear() would spuriously fire with a stale // 'session_start' reason. if (!forceIncludeExternal) { const eagerLoadReason = consumeNextEagerLoadReason() if (eagerLoadReason !== undefined && hasInstructionsLoadedHook()) { for (const file of result) { if (!isInstructionsMemoryType(file.type)) continue const loadReason = file.parent ? 'include' : eagerLoadReason void executeInstructionsLoadedHooks( file.path, file.type, loadReason, { globs: file.globs, parentFilePath: file.parent, }, ) } } }
return result },)
function isInstructionsMemoryType( type: MemoryType,): type is InstructionsMemoryType { return ( type === 'User' || type === 'Project' || type === 'Local' || type === 'Managed' )}
// Load reason to report for top-level (non-included) files on the next eager// getMemoryFiles() pass. Set to 'compact' by resetGetMemoryFilesCache when// compaction clears the cache, so the InstructionsLoaded hook reports the// reload correctly instead of misreporting it as 'session_start'. One-shot:// reset to 'session_start' after being read.let nextEagerLoadReason: InstructionsLoadReason = 'session_start'
// Whether the InstructionsLoaded hook should fire on the next cache miss.// true initially (for session_start), consumed after firing, re-enabled only// by resetGetMemoryFilesCache(). Callers that only need cache invalidation// for correctness (e.g. worktree enter/exit, settings sync, /memory dialog)// should use clearMemoryFileCaches() instead to avoid spurious hook fires.let shouldFireHook = true
function consumeNextEagerLoadReason(): InstructionsLoadReason | undefined { if (!shouldFireHook) return undefined shouldFireHook = false const reason = nextEagerLoadReason nextEagerLoadReason = 'session_start' return reason}
/** * Clears the getMemoryFiles memoize cache * without firing the InstructionsLoaded hook. * * Use this for cache invalidation that is purely for correctness (e.g. * worktree enter/exit, settings sync, /memory dialog). For events that * represent instructions actually being reloaded into context (e.g. * compaction), use resetGetMemoryFilesCache() instead. */export function clearMemoryFileCaches(): void { // ?.cache because tests spyOn this, which replaces the memoize wrapper. getMemoryFiles.cache?.clear?.()}
export function resetGetMemoryFilesCache( reason: InstructionsLoadReason = 'session_start',): void { nextEagerLoadReason = reason shouldFireHook = true clearMemoryFileCaches()}
export function getLargeMemoryFiles(files: MemoryFileInfo[]): MemoryFileInfo[] { return files.filter(f => f.content.length > MAX_MEMORY_CHARACTER_COUNT)}
/** * When tengu_moth_copse is on, the findRelevantMemories prefetch surfaces * memory files via attachments, so the MEMORY.md index is no longer injected * into the system prompt. Callsites that care about "what's actually in * context" (context builder, /context viz) should filter through this. */export function filterInjectedMemoryFiles( files: MemoryFileInfo[],): MemoryFileInfo[] { const skipMemoryIndex = getFeatureValue_CACHED_MAY_BE_STALE( 'tengu_moth_copse', false, ) if (!skipMemoryIndex) return files return files.filter(f => f.type !== 'AutoMem' && f.type !== 'TeamMem')}
export const getClaudeMds = ( memoryFiles: MemoryFileInfo[], filter?: (type: MemoryType) => boolean,): string => { const memories: string[] = [] const skipProjectLevel = getFeatureValue_CACHED_MAY_BE_STALE( 'tengu_paper_halyard', false, )
for (const file of memoryFiles) { if (filter && !filter(file.type)) continue if (skipProjectLevel && (file.type === 'Project' || file.type === 'Local')) continue if (file.content) { const description = file.type === 'Project' ? ' (project instructions, checked into the codebase)' : file.type === 'Local' ? " (user's private project instructions, not checked in)" : feature('TEAMMEM') && file.type === 'TeamMem' ? ' (shared team memory, synced across the organization)' : file.type === 'AutoMem' ? " (user's auto-memory, persists across conversations)" : " (user's private global instructions for all projects)"
const content = file.content.trim() if (feature('TEAMMEM') && file.type === 'TeamMem') { memories.push( `Contents of ${file.path}${description}:\n\n<team-memory-content source="shared">\n${content}\n</team-memory-content>`, ) } else { memories.push(`Contents of ${file.path}${description}:\n\n${content}`) } } }
if (memories.length === 0) { return '' }
return `${MEMORY_INSTRUCTION_PROMPT}\n\n${memories.join('\n\n')}`}
/** * Gets managed and user conditional rules that match the target path. * This is the first phase of nested memory loading. * * @param targetPath The target file path to match against glob patterns * @param processedPaths Set of already processed file paths (will be mutated) * @returns Array of MemoryFileInfo objects for matching conditional rules */export async function getManagedAndUserConditionalRules( targetPath: string, processedPaths: Set<string>,): Promise<MemoryFileInfo[]> { const result: MemoryFileInfo[] = []
// Process Managed conditional .claude/rules/*.md files const managedClaudeRulesDir = getManagedClaudeRulesDir() result.push( ...(await processConditionedMdRules( targetPath, managedClaudeRulesDir, 'Managed', processedPaths, false, )), )
if (isSettingSourceEnabled('userSettings')) { // Process User conditional .claude/rules/*.md files const userClaudeRulesDir = getUserClaudeRulesDir() result.push( ...(await processConditionedMdRules( targetPath, userClaudeRulesDir, 'User', processedPaths, true, )), ) }
return result}
/** * Gets memory files for a single nested directory (between CWD and target). * Loads CLAUDE.md, unconditional rules, and conditional rules for that directory. * * @param dir The directory to process * @param targetPath The target file path (for conditional rule matching) * @param processedPaths Set of already processed file paths (will be mutated) * @returns Array of MemoryFileInfo objects */export async function getMemoryFilesForNestedDirectory( dir: string, targetPath: string, processedPaths: Set<string>,): Promise<MemoryFileInfo[]> { const result: MemoryFileInfo[] = []
// Process project memory files (CLAUDE.md and .claude/CLAUDE.md) if (isSettingSourceEnabled('projectSettings')) { const projectPath = join(dir, 'CLAUDE.md') result.push( ...(await processMemoryFile( projectPath, 'Project', processedPaths, false, )), ) const dotClaudePath = join(dir, '.claude', 'CLAUDE.md') result.push( ...(await processMemoryFile( dotClaudePath, 'Project', processedPaths, false, )), ) }
// Process local memory file (CLAUDE.local.md) if (isSettingSourceEnabled('localSettings')) { const localPath = join(dir, 'CLAUDE.local.md') result.push( ...(await processMemoryFile(localPath, 'Local', processedPaths, false)), ) }
const rulesDir = join(dir, '.claude', 'rules')
// Process project unconditional .claude/rules/*.md files, which were not eagerly loaded // Use a separate processedPaths set to avoid marking conditional rule files as processed const unconditionalProcessedPaths = new Set(processedPaths) result.push( ...(await processMdRules({ rulesDir, type: 'Project', processedPaths: unconditionalProcessedPaths, includeExternal: false, conditionalRule: false, })), )
// Process project conditional .claude/rules/*.md files result.push( ...(await processConditionedMdRules( targetPath, rulesDir, 'Project', processedPaths, false, )), )
// processedPaths must be seeded with unconditional paths for subsequent directories for (const path of unconditionalProcessedPaths) { processedPaths.add(path) }
return result}
/** * Gets conditional rules for a CWD-level directory (from root up to CWD). * Only processes conditional rules since unconditional rules are already loaded eagerly. * * @param dir The directory to process * @param targetPath The target file path (for conditional rule matching) * @param processedPaths Set of already processed file paths (will be mutated) * @returns Array of MemoryFileInfo objects */export async function getConditionalRulesForCwdLevelDirectory( dir: string, targetPath: string, processedPaths: Set<string>,): Promise<MemoryFileInfo[]> { const rulesDir = join(dir, '.claude', 'rules') return processConditionedMdRules( targetPath, rulesDir, 'Project', processedPaths, false, )}
/** * Processes all .md files in the .claude/rules/ directory and its subdirectories, * filtering to only include files with frontmatter paths that match the target path * @param targetPath The file path to match against frontmatter glob patterns * @param rulesDir The path to the rules directory * @param type Type of memory file (User, Project, Local) * @param processedPaths Set of already processed file paths * @param includeExternal Whether to include external files * @returns Array of MemoryFileInfo objects that match the target path */export async function processConditionedMdRules( targetPath: string, rulesDir: string, type: MemoryType, processedPaths: Set<string>, includeExternal: boolean,): Promise<MemoryFileInfo[]> { const conditionedRuleMdFiles = await processMdRules({ rulesDir, type, processedPaths, includeExternal, conditionalRule: true, })
// Filter to only include files whose globs patterns match the targetPath return conditionedRuleMdFiles.filter(file => { if (!file.globs || file.globs.length === 0) { return false }
// For Project rules: glob patterns are relative to the directory containing .claude // For Managed/User rules: glob patterns are relative to the original CWD const baseDir = type === 'Project' ? dirname(dirname(rulesDir)) // Parent of .claude : getOriginalCwd() // Project root for managed/user rules
const relativePath = isAbsolute(targetPath) ? relative(baseDir, targetPath) : targetPath // ignore() throws on empty strings, paths escaping the base (../), // and absolute paths (Windows cross-drive relative() returns absolute). // Files outside baseDir can't match baseDir-relative globs anyway. if ( !relativePath || relativePath.startsWith('..') || isAbsolute(relativePath) ) { return false } return ignore().add(file.globs).ignores(relativePath) })}
export type ExternalClaudeMdInclude = { path: string parent: string}
export function getExternalClaudeMdIncludes( files: MemoryFileInfo[],): ExternalClaudeMdInclude[] { const externals: ExternalClaudeMdInclude[] = [] for (const file of files) { if (file.type !== 'User' && file.parent && !pathInOriginalCwd(file.path)) { externals.push({ path: file.path, parent: file.parent }) } } return externals}
export function hasExternalClaudeMdIncludes(files: MemoryFileInfo[]): boolean { return getExternalClaudeMdIncludes(files).length > 0}
export async function shouldShowClaudeMdExternalIncludesWarning(): Promise<boolean> { const config = getCurrentProjectConfig() if ( config.hasClaudeMdExternalIncludesApproved || config.hasClaudeMdExternalIncludesWarningShown ) { return false }
return hasExternalClaudeMdIncludes(await getMemoryFiles(true))}
/** * Check if a file path is a memory file (CLAUDE.md, CLAUDE.local.md, or .claude/rules/*.md) */export function isMemoryFilePath(filePath: string): boolean { const name = basename(filePath)
// CLAUDE.md or CLAUDE.local.md anywhere if (name === 'CLAUDE.md' || name === 'CLAUDE.local.md') { return true }
// .md files in .claude/rules/ directories if ( name.endsWith('.md') && filePath.includes(`${sep}.claude${sep}rules${sep}`) ) { return true }
return false}
/** * Get all memory file paths from both standard discovery and readFileState. * Combines: * - getMemoryFiles() paths (CWD upward to root) * - readFileState paths matching memory patterns (includes child directories) */export function getAllMemoryFilePaths( files: MemoryFileInfo[], readFileState: FileStateCache,): string[] { const paths = new Set<string>() for (const file of files) { if (file.content.trim().length > 0) { paths.add(file.path) } }
// Add memory files from readFileState (includes child directories) for (const filePath of cacheKeys(readFileState)) { if (isMemoryFilePath(filePath)) { paths.add(filePath) } }
return Array.from(paths)}