π File detail
utils/sessionEnvironment.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes getSessionEnvDirPath, getHookEnvFilePath, clearCwdEnvFiles, invalidateSessionEnvCache, and getSessionEnvironmentScript β mainly functions, hooks, or classes. Dependencies touch Node filesystem and Node path helpers. It composes internal code from bootstrap, debug, envUtils, errors, and platform (relative imports). What the file header says: Cache states: undefined = not yet loaded (need to check disk) null = checked disk, no files exist (don't check again) string = loaded and cached (use cached value).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Cache states: undefined = not yet loaded (need to check disk) null = checked disk, no files exist (don't check again) string = loaded and cached (use cached value)
π€ Exports (heuristic)
getSessionEnvDirPathgetHookEnvFilePathclearCwdEnvFilesinvalidateSessionEnvCachegetSessionEnvironmentScript
π External import roots
Package roots from from "β¦" (relative paths omitted).
fspath
π₯οΈ Source preview
import { mkdir, readdir, readFile, writeFile } from 'fs/promises'
import { join } from 'path'
import { getSessionId } from '../bootstrap/state.js'
import { logForDebugging } from './debug.js'
import { getClaudeConfigHomeDir } from './envUtils.js'
import { errorMessage, getErrnoCode } from './errors.js'
import { getPlatform } from './platform.js'
// Cache states:
// undefined = not yet loaded (need to check disk)
// null = checked disk, no files exist (don't check again)
// string = loaded and cached (use cached value)
let sessionEnvScript: string | null | undefined = undefined
export async function getSessionEnvDirPath(): Promise<string> {
const sessionEnvDir = join(
getClaudeConfigHomeDir(),
'session-env',
getSessionId(),
)
await mkdir(sessionEnvDir, { recursive: true })
return sessionEnvDir
}
export async function getHookEnvFilePath(
hookEvent: 'Setup' | 'SessionStart' | 'CwdChanged' | 'FileChanged',
hookIndex: number,
): Promise<string> {
const prefix = hookEvent.toLowerCase()
return join(await getSessionEnvDirPath(), `${prefix}-hook-${hookIndex}.sh`)
}
export async function clearCwdEnvFiles(): Promise<void> {
try {
const dir = await getSessionEnvDirPath()
const files = await readdir(dir)
await Promise.all(
files
.filter(
f =>
(f.startsWith('filechanged-hook-') ||
f.startsWith('cwdchanged-hook-')) &&
HOOK_ENV_REGEX.test(f),
)
.map(f => writeFile(join(dir, f), '')),
)
} catch (e: unknown) {
const code = getErrnoCode(e)
if (code !== 'ENOENT') {
logForDebugging(`Failed to clear cwd env files: ${errorMessage(e)}`)
}
}
}
export function invalidateSessionEnvCache(): void {
logForDebugging('Invalidating session environment cache')
sessionEnvScript = undefined
}
export async function getSessionEnvironmentScript(): Promise<string | null> {
if (getPlatform() === 'windows') {
logForDebugging('Session environment not yet supported on Windows')
return null
}
if (sessionEnvScript !== undefined) {
return sessionEnvScript
}
const scripts: string[] = []
// Check for CLAUDE_ENV_FILE passed from parent process (e.g., HFI trajectory runner)
// This allows venv/conda activation to persist across shell commands
const envFile = process.env.CLAUDE_ENV_FILE
if (envFile) {
try {
const envScript = (await readFile(envFile, 'utf8')).trim()
if (envScript) {
scripts.push(envScript)
logForDebugging(
`Session environment loaded from CLAUDE_ENV_FILE: ${envFile} (${envScript.length} chars)`,
)
}
} catch (e: unknown) {
const code = getErrnoCode(e)
if (code !== 'ENOENT') {
logForDebugging(`Failed to read CLAUDE_ENV_FILE: ${errorMessage(e)}`)
}
}
}
// Load hook environment files from session directory
const sessionEnvDir = await getSessionEnvDirPath()
try {
const files = await readdir(sessionEnvDir)
// We are sorting the hook env files by the order in which they are listed
// in the settings.json file so that the resulting env is deterministic
const hookFiles = files
.filter(f => HOOK_ENV_REGEX.test(f))
.sort(sortHookEnvFiles)
for (const file of hookFiles) {
const filePath = join(sessionEnvDir, file)
try {
const content = (await readFile(filePath, 'utf8')).trim()
if (content) {
scripts.push(content)
}
} catch (e: unknown) {
const code = getErrnoCode(e)
if (code !== 'ENOENT') {
logForDebugging(
`Failed to read hook file ${filePath}: ${errorMessage(e)}`,
)
}
}
}
if (hookFiles.length > 0) {
logForDebugging(
`Session environment loaded from ${hookFiles.length} hook file(s)`,
)
}
} catch (e: unknown) {
const code = getErrnoCode(e)
if (code !== 'ENOENT') {
logForDebugging(
`Failed to load session environment from hooks: ${errorMessage(e)}`,
)
}
}
if (scripts.length === 0) {
logForDebugging('No session environment scripts found')
sessionEnvScript = null
return sessionEnvScript
}
sessionEnvScript = scripts.join('\n')
logForDebugging(
`Session environment script ready (${sessionEnvScript.length} chars total)`,
)
return sessionEnvScript
}
const HOOK_ENV_PRIORITY: Record<string, number> = {
setup: 0,
sessionstart: 1,
cwdchanged: 2,
filechanged: 3,
}
const HOOK_ENV_REGEX =
/^(setup|sessionstart|cwdchanged|filechanged)-hook-(\d+)\.sh$/
function sortHookEnvFiles(a: string, b: string): number {
const aMatch = a.match(HOOK_ENV_REGEX)
const bMatch = b.match(HOOK_ENV_REGEX)
const aType = aMatch?.[1] || ''
const bType = bMatch?.[1] || ''
if (aType !== bType) {
return (HOOK_ENV_PRIORITY[aType] ?? 99) - (HOOK_ENV_PRIORITY[bType] ?? 99)
}
const aIndex = parseInt(aMatch?.[2] || '0', 10)
const bIndex = parseInt(bMatch?.[2] || '0', 10)
return aIndex - bIndex
}