source dump of claude code
at main 278 lines 11 kB view raw
1import memoize from 'lodash-es/memoize.js' 2import { homedir } from 'os' 3import { isAbsolute, join, normalize, sep } from 'path' 4import { 5 getIsNonInteractiveSession, 6 getProjectRoot, 7} from '../bootstrap/state.js' 8import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' 9import { 10 getClaudeConfigHomeDir, 11 isEnvDefinedFalsy, 12 isEnvTruthy, 13} from '../utils/envUtils.js' 14import { findCanonicalGitRoot } from '../utils/git.js' 15import { sanitizePath } from '../utils/path.js' 16import { 17 getInitialSettings, 18 getSettingsForSource, 19} from '../utils/settings/settings.js' 20 21/** 22 * Whether auto-memory features are enabled (memdir, agent memory, past session search). 23 * Enabled by default. Priority chain (first defined wins): 24 * 1. CLAUDE_CODE_DISABLE_AUTO_MEMORY env var (1/true → OFF, 0/false → ON) 25 * 2. CLAUDE_CODE_SIMPLE (--bare) → OFF 26 * 3. CCR without persistent storage → OFF (no CLAUDE_CODE_REMOTE_MEMORY_DIR) 27 * 4. autoMemoryEnabled in settings.json (supports project-level opt-out) 28 * 5. Default: enabled 29 */ 30export function isAutoMemoryEnabled(): boolean { 31 const envVal = process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY 32 if (isEnvTruthy(envVal)) { 33 return false 34 } 35 if (isEnvDefinedFalsy(envVal)) { 36 return true 37 } 38 // --bare / SIMPLE: prompts.ts already drops the memory section from the 39 // system prompt via its SIMPLE early-return; this gate stops the other half 40 // (extractMemories turn-end fork, autoDream, /remember, /dream, team sync). 41 if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) { 42 return false 43 } 44 if ( 45 isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) && 46 !process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR 47 ) { 48 return false 49 } 50 const settings = getInitialSettings() 51 if (settings.autoMemoryEnabled !== undefined) { 52 return settings.autoMemoryEnabled 53 } 54 return true 55} 56 57/** 58 * Whether the extract-memories background agent will run this session. 59 * 60 * The main agent's prompt always has full save instructions regardless of 61 * this gate — when the main agent writes memories, the background agent 62 * skips that range (hasMemoryWritesSince in extractMemories.ts); when it 63 * doesn't, the background agent catches anything missed. 64 * 65 * Callers must also gate on feature('EXTRACT_MEMORIES') — that check cannot 66 * live inside this helper because feature() only tree-shakes when used 67 * directly in an `if` condition. 68 */ 69export function isExtractModeActive(): boolean { 70 if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail', false)) { 71 return false 72 } 73 return ( 74 !getIsNonInteractiveSession() || 75 getFeatureValue_CACHED_MAY_BE_STALE('tengu_slate_thimble', false) 76 ) 77} 78 79/** 80 * Returns the base directory for persistent memory storage. 81 * Resolution order: 82 * 1. CLAUDE_CODE_REMOTE_MEMORY_DIR env var (explicit override, set in CCR) 83 * 2. ~/.claude (default config home) 84 */ 85export function getMemoryBaseDir(): string { 86 if (process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR) { 87 return process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR 88 } 89 return getClaudeConfigHomeDir() 90} 91 92const AUTO_MEM_DIRNAME = 'memory' 93const AUTO_MEM_ENTRYPOINT_NAME = 'MEMORY.md' 94 95/** 96 * Normalize and validate a candidate auto-memory directory path. 97 * 98 * SECURITY: Rejects paths that would be dangerous as a read-allowlist root 99 * or that normalize() doesn't fully resolve: 100 * - relative (!isAbsolute): "../foo" — would be interpreted relative to CWD 101 * - root/near-root (length < 3): "/" → "" after strip; "/a" too short 102 * - Windows drive-root (C: regex): "C:\" → "C:" after strip 103 * - UNC paths (\\server\share): network paths — opaque trust boundary 104 * - null byte: survives normalize(), can truncate in syscalls 105 * 106 * Returns the normalized path with exactly one trailing separator, 107 * or undefined if the path is unset/empty/rejected. 108 */ 109function validateMemoryPath( 110 raw: string | undefined, 111 expandTilde: boolean, 112): string | undefined { 113 if (!raw) { 114 return undefined 115 } 116 let candidate = raw 117 // Settings.json paths support ~/ expansion (user-friendly). The env var 118 // override does not (it's set programmatically by Cowork/SDK, which should 119 // always pass absolute paths). Bare "~", "~/", "~/.", "~/..", etc. are NOT 120 // expanded — they would make isAutoMemPath() match all of $HOME or its 121 // parent (same class of danger as "/" or "C:\"). 122 if ( 123 expandTilde && 124 (candidate.startsWith('~/') || candidate.startsWith('~\\')) 125 ) { 126 const rest = candidate.slice(2) 127 // Reject trivial remainders that would expand to $HOME or an ancestor. 128 // normalize('') = '.', normalize('.') = '.', normalize('foo/..') = '.', 129 // normalize('..') = '..', normalize('foo/../..') = '..' 130 const restNorm = normalize(rest || '.') 131 if (restNorm === '.' || restNorm === '..') { 132 return undefined 133 } 134 candidate = join(homedir(), rest) 135 } 136 // normalize() may preserve a trailing separator; strip before adding 137 // exactly one to match the trailing-sep contract of getAutoMemPath() 138 const normalized = normalize(candidate).replace(/[/\\]+$/, '') 139 if ( 140 !isAbsolute(normalized) || 141 normalized.length < 3 || 142 /^[A-Za-z]:$/.test(normalized) || 143 normalized.startsWith('\\\\') || 144 normalized.startsWith('//') || 145 normalized.includes('\0') 146 ) { 147 return undefined 148 } 149 return (normalized + sep).normalize('NFC') 150} 151 152/** 153 * Direct override for the full auto-memory directory path via env var. 154 * When set, getAutoMemPath()/getAutoMemEntrypoint() return this path directly 155 * instead of computing `{base}/projects/{sanitized-cwd}/memory/`. 156 * 157 * Used by Cowork to redirect memory to a space-scoped mount where the 158 * per-session cwd (which contains the VM process name) would otherwise 159 * produce a different project-key for every session. 160 */ 161function getAutoMemPathOverride(): string | undefined { 162 return validateMemoryPath( 163 process.env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE, 164 false, 165 ) 166} 167 168/** 169 * Settings.json override for the full auto-memory directory path. 170 * Supports ~/ expansion for user convenience. 171 * 172 * SECURITY: projectSettings (.claude/settings.json committed to the repo) is 173 * intentionally excluded — a malicious repo could otherwise set 174 * autoMemoryDirectory: "~/.ssh" and gain silent write access to sensitive 175 * directories via the filesystem.ts write carve-out (which fires when 176 * isAutoMemPath() matches and hasAutoMemPathOverride() is false). This follows 177 * the same pattern as hasSkipDangerousModePermissionPrompt() etc. 178 */ 179function getAutoMemPathSetting(): string | undefined { 180 const dir = 181 getSettingsForSource('policySettings')?.autoMemoryDirectory ?? 182 getSettingsForSource('flagSettings')?.autoMemoryDirectory ?? 183 getSettingsForSource('localSettings')?.autoMemoryDirectory ?? 184 getSettingsForSource('userSettings')?.autoMemoryDirectory 185 return validateMemoryPath(dir, true) 186} 187 188/** 189 * Check if CLAUDE_COWORK_MEMORY_PATH_OVERRIDE is set to a valid override. 190 * Use this as a signal that the SDK caller has explicitly opted into 191 * the auto-memory mechanics — e.g. to decide whether to inject the 192 * memory prompt when a custom system prompt replaces the default. 193 */ 194export function hasAutoMemPathOverride(): boolean { 195 return getAutoMemPathOverride() !== undefined 196} 197 198/** 199 * Returns the canonical git repo root if available, otherwise falls back to 200 * the stable project root. Uses findCanonicalGitRoot so all worktrees of the 201 * same repo share one auto-memory directory (anthropics/claude-code#24382). 202 */ 203function getAutoMemBase(): string { 204 return findCanonicalGitRoot(getProjectRoot()) ?? getProjectRoot() 205} 206 207/** 208 * Returns the auto-memory directory path. 209 * 210 * Resolution order: 211 * 1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE env var (full-path override, used by Cowork) 212 * 2. autoMemoryDirectory in settings.json (trusted sources only: policy/local/user) 213 * 3. <memoryBase>/projects/<sanitized-git-root>/memory/ 214 * where memoryBase is resolved by getMemoryBaseDir() 215 * 216 * Memoized: render-path callers (collapseReadSearchGroups → isAutoManagedMemoryFile) 217 * fire per tool-use message per Messages re-render; each miss costs 218 * getSettingsForSource × 4 → parseSettingsFile (realpathSync + readFileSync). 219 * Keyed on projectRoot so tests that change its mock mid-block recompute; 220 * env vars / settings.json / CLAUDE_CONFIG_DIR are session-stable in 221 * production and covered by per-test cache.clear. 222 */ 223export const getAutoMemPath = memoize( 224 (): string => { 225 const override = getAutoMemPathOverride() ?? getAutoMemPathSetting() 226 if (override) { 227 return override 228 } 229 const projectsDir = join(getMemoryBaseDir(), 'projects') 230 return ( 231 join(projectsDir, sanitizePath(getAutoMemBase()), AUTO_MEM_DIRNAME) + sep 232 ).normalize('NFC') 233 }, 234 () => getProjectRoot(), 235) 236 237/** 238 * Returns the daily log file path for the given date (defaults to today). 239 * Shape: <autoMemPath>/logs/YYYY/MM/YYYY-MM-DD.md 240 * 241 * Used by assistant mode (feature('KAIROS')): rather than maintaining 242 * MEMORY.md as a live index, the agent appends to a date-named log file 243 * as it works. A separate nightly /dream skill distills these logs into 244 * topic files + MEMORY.md. 245 */ 246export function getAutoMemDailyLogPath(date: Date = new Date()): string { 247 const yyyy = date.getFullYear().toString() 248 const mm = (date.getMonth() + 1).toString().padStart(2, '0') 249 const dd = date.getDate().toString().padStart(2, '0') 250 return join(getAutoMemPath(), 'logs', yyyy, mm, `${yyyy}-${mm}-${dd}.md`) 251} 252 253/** 254 * Returns the auto-memory entrypoint (MEMORY.md inside the auto-memory dir). 255 * Follows the same resolution order as getAutoMemPath(). 256 */ 257export function getAutoMemEntrypoint(): string { 258 return join(getAutoMemPath(), AUTO_MEM_ENTRYPOINT_NAME) 259} 260 261/** 262 * Check if an absolute path is within the auto-memory directory. 263 * 264 * When CLAUDE_COWORK_MEMORY_PATH_OVERRIDE is set, this matches against the 265 * env-var override directory. Note that a true return here does NOT imply 266 * write permission in that case — the filesystem.ts write carve-out is gated 267 * on !hasAutoMemPathOverride() (it exists to bypass DANGEROUS_DIRECTORIES). 268 * 269 * The settings.json autoMemoryDirectory DOES get the write carve-out: it's the 270 * user's explicit choice from a trusted settings source (projectSettings is 271 * excluded — see getAutoMemPathSetting), and hasAutoMemPathOverride() remains 272 * false for it. 273 */ 274export function isAutoMemPath(absolutePath: string): boolean { 275 // SECURITY: Normalize to prevent path traversal bypasses via .. segments 276 const normalizedPath = normalize(absolutePath) 277 return normalizedPath.startsWith(getAutoMemPath()) 278}