sessionEnvironment.ts
utils/sessionEnvironment.ts
167
Lines
5053
Bytes
5
Exports
7
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with session-engine. It contains 167 lines, 7 detected imports, and 5 detected exports.
Important relationships
Detected exports
getSessionEnvDirPathgetHookEnvFilePathclearCwdEnvFilesinvalidateSessionEnvCachegetSessionEnvironmentScript
Keywords
logfordebuggingsessionenvscriptfilescodesessionenvironmentjoinsessionenvdirscriptserrormessage
Detected imports
fs/promisespath../bootstrap/state.js./debug.js./envUtils.js./errors.js./platform.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
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
}