import memoize from 'lodash-es/memoize.js' import { homedir } from 'os' import { join } from 'path' // Memoized: 150+ callers, many on hot paths. Keyed off CLAUDE_CONFIG_DIR so // tests that change the env var get a fresh value without explicit cache.clear. export const getClaudeConfigHomeDir = memoize( (): string => { return ( process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude') ).normalize('NFC') }, () => process.env.CLAUDE_CONFIG_DIR, ) export function getTeamsDir(): string { return join(getClaudeConfigHomeDir(), 'teams') } /** * Check if NODE_OPTIONS contains a specific flag. * Splits on whitespace and checks for exact match to avoid false positives. */ export function hasNodeOption(flag: string): boolean { const nodeOptions = process.env.NODE_OPTIONS if (!nodeOptions) { return false } return nodeOptions.split(/\s+/).includes(flag) } export function isEnvTruthy(envVar: string | boolean | undefined): boolean { if (!envVar) return false if (typeof envVar === 'boolean') return envVar const normalizedValue = envVar.toLowerCase().trim() return ['1', 'true', 'yes', 'on'].includes(normalizedValue) } export function isEnvDefinedFalsy( envVar: string | boolean | undefined, ): boolean { if (envVar === undefined) return false if (typeof envVar === 'boolean') return !envVar if (!envVar) return false const normalizedValue = envVar.toLowerCase().trim() return ['0', 'false', 'no', 'off'].includes(normalizedValue) } /** * --bare / CLAUDE_CODE_SIMPLE — skip hooks, LSP, plugin sync, skill dir-walk, * attribution, background prefetches, and ALL keychain/credential reads. * Auth is strictly ANTHROPIC_API_KEY env or apiKeyHelper from --settings. * Explicit CLI flags (--plugin-dir, --add-dir, --mcp-config) still honored. * ~30 gates across the codebase. * * Checks argv directly (in addition to the env var) because several gates * run before main.tsx's action handler sets CLAUDE_CODE_SIMPLE=1 from --bare * — notably startKeychainPrefetch() at main.tsx top-level. */ export function isBareMode(): boolean { return ( isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE) || process.argv.includes('--bare') ) } /** * Parses an array of environment variable strings into a key-value object * @param envVars Array of strings in KEY=VALUE format * @returns Object with key-value pairs */ export function parseEnvVars( rawEnvArgs: string[] | undefined, ): Record { const parsedEnv: Record = {} // Parse individual env vars if (rawEnvArgs) { for (const envStr of rawEnvArgs) { const [key, ...valueParts] = envStr.split('=') if (!key || valueParts.length === 0) { throw new Error( `Invalid environment variable format: ${envStr}, environment variables should be added as: -e KEY1=value1 -e KEY2=value2`, ) } parsedEnv[key] = valueParts.join('=') } } return parsedEnv } /** * Get the AWS region with fallback to default * Matches the Anthropic Bedrock SDK's region behavior */ export function getAWSRegion(): string { return process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1' } /** * Get the default Vertex AI region */ export function getDefaultVertexRegion(): string { return process.env.CLOUD_ML_REGION || 'us-east5' } /** * Check if bash commands should maintain project working directory (reset to original after each command) * @returns true if CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR is set to a truthy value */ export function shouldMaintainProjectWorkingDir(): boolean { return isEnvTruthy(process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR) } /** * Check if running on Homespace (ant-internal cloud environment) */ export function isRunningOnHomespace(): boolean { return ( process.env.USER_TYPE === 'ant' && isEnvTruthy(process.env.COO_RUNNING_ON_HOMESPACE) ) } /** * Conservative check for whether Claude Code is running inside a protected * (privileged or ASL3+) COO namespace or cluster. * * Conservative means: when signals are ambiguous, assume protected. We would * rather over-report protected usage than miss it. Unprotected environments * are homespace, namespaces on the open allowlist, and no k8s/COO signals * at all (laptop/local dev). * * Used for telemetry to measure auto-mode usage in sensitive environments. */ export function isInProtectedNamespace(): boolean { // USER_TYPE is build-time --define'd; in external builds this block is // DCE'd so the require() and namespace allowlist never appear in the bundle. if (process.env.USER_TYPE === 'ant') { /* eslint-disable @typescript-eslint/no-require-imports */ return ( require('./protectedNamespace.js') as typeof import('./protectedNamespace.js') ).checkProtectedNamespace() /* eslint-enable @typescript-eslint/no-require-imports */ } return false } // @[MODEL LAUNCH]: Add a Vertex region override env var for the new model. /** * Model prefix → env var for Vertex region overrides. * Order matters: more specific prefixes must come before less specific ones * (e.g., 'claude-opus-4-1' before 'claude-opus-4'). */ const VERTEX_REGION_OVERRIDES: ReadonlyArray<[string, string]> = [ ['claude-haiku-4-5', 'VERTEX_REGION_CLAUDE_HAIKU_4_5'], ['claude-3-5-haiku', 'VERTEX_REGION_CLAUDE_3_5_HAIKU'], ['claude-3-5-sonnet', 'VERTEX_REGION_CLAUDE_3_5_SONNET'], ['claude-3-7-sonnet', 'VERTEX_REGION_CLAUDE_3_7_SONNET'], ['claude-opus-4-1', 'VERTEX_REGION_CLAUDE_4_1_OPUS'], ['claude-opus-4', 'VERTEX_REGION_CLAUDE_4_0_OPUS'], ['claude-sonnet-4-6', 'VERTEX_REGION_CLAUDE_4_6_SONNET'], ['claude-sonnet-4-5', 'VERTEX_REGION_CLAUDE_4_5_SONNET'], ['claude-sonnet-4', 'VERTEX_REGION_CLAUDE_4_0_SONNET'], ] /** * Get the Vertex AI region for a specific model. * Different models may be available in different regions. */ export function getVertexRegionForModel( model: string | undefined, ): string | undefined { if (model) { const match = VERTEX_REGION_OVERRIDES.find(([prefix]) => model.startsWith(prefix), ) if (match) { return process.env[match[1]] || getDefaultVertexRegion() } } return getDefaultVertexRegion() }