/ utils / envUtils.ts
envUtils.ts
  1  import memoize from 'lodash-es/memoize.js'
  2  import { homedir } from 'os'
  3  import { join } from 'path'
  4  
  5  // Memoized: 150+ callers, many on hot paths. Keyed off CLAUDE_CONFIG_DIR so
  6  // tests that change the env var get a fresh value without explicit cache.clear.
  7  export const getClaudeConfigHomeDir = memoize(
  8    (): string => {
  9      return (
 10        process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude')
 11      ).normalize('NFC')
 12    },
 13    () => process.env.CLAUDE_CONFIG_DIR,
 14  )
 15  
 16  export function getTeamsDir(): string {
 17    return join(getClaudeConfigHomeDir(), 'teams')
 18  }
 19  
 20  /**
 21   * Check if NODE_OPTIONS contains a specific flag.
 22   * Splits on whitespace and checks for exact match to avoid false positives.
 23   */
 24  export function hasNodeOption(flag: string): boolean {
 25    const nodeOptions = process.env.NODE_OPTIONS
 26    if (!nodeOptions) {
 27      return false
 28    }
 29    return nodeOptions.split(/\s+/).includes(flag)
 30  }
 31  
 32  export function isEnvTruthy(envVar: string | boolean | undefined): boolean {
 33    if (!envVar) return false
 34    if (typeof envVar === 'boolean') return envVar
 35    const normalizedValue = envVar.toLowerCase().trim()
 36    return ['1', 'true', 'yes', 'on'].includes(normalizedValue)
 37  }
 38  
 39  export function isEnvDefinedFalsy(
 40    envVar: string | boolean | undefined,
 41  ): boolean {
 42    if (envVar === undefined) return false
 43    if (typeof envVar === 'boolean') return !envVar
 44    if (!envVar) return false
 45    const normalizedValue = envVar.toLowerCase().trim()
 46    return ['0', 'false', 'no', 'off'].includes(normalizedValue)
 47  }
 48  
 49  /**
 50   * --bare / CLAUDE_CODE_SIMPLE — skip hooks, LSP, plugin sync, skill dir-walk,
 51   * attribution, background prefetches, and ALL keychain/credential reads.
 52   * Auth is strictly ANTHROPIC_API_KEY env or apiKeyHelper from --settings.
 53   * Explicit CLI flags (--plugin-dir, --add-dir, --mcp-config) still honored.
 54   * ~30 gates across the codebase.
 55   *
 56   * Checks argv directly (in addition to the env var) because several gates
 57   * run before main.tsx's action handler sets CLAUDE_CODE_SIMPLE=1 from --bare
 58   * — notably startKeychainPrefetch() at main.tsx top-level.
 59   */
 60  export function isBareMode(): boolean {
 61    return (
 62      isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE) ||
 63      process.argv.includes('--bare')
 64    )
 65  }
 66  
 67  /**
 68   * Parses an array of environment variable strings into a key-value object
 69   * @param envVars Array of strings in KEY=VALUE format
 70   * @returns Object with key-value pairs
 71   */
 72  export function parseEnvVars(
 73    rawEnvArgs: string[] | undefined,
 74  ): Record<string, string> {
 75    const parsedEnv: Record<string, string> = {}
 76  
 77    // Parse individual env vars
 78    if (rawEnvArgs) {
 79      for (const envStr of rawEnvArgs) {
 80        const [key, ...valueParts] = envStr.split('=')
 81        if (!key || valueParts.length === 0) {
 82          throw new Error(
 83            `Invalid environment variable format: ${envStr}, environment variables should be added as: -e KEY1=value1 -e KEY2=value2`,
 84          )
 85        }
 86        parsedEnv[key] = valueParts.join('=')
 87      }
 88    }
 89    return parsedEnv
 90  }
 91  
 92  /**
 93   * Get the AWS region with fallback to default
 94   * Matches the Anthropic Bedrock SDK's region behavior
 95   */
 96  export function getAWSRegion(): string {
 97    return process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1'
 98  }
 99  
100  /**
101   * Get the default Vertex AI region
102   */
103  export function getDefaultVertexRegion(): string {
104    return process.env.CLOUD_ML_REGION || 'us-east5'
105  }
106  
107  /**
108   * Check if bash commands should maintain project working directory (reset to original after each command)
109   * @returns true if CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR is set to a truthy value
110   */
111  export function shouldMaintainProjectWorkingDir(): boolean {
112    return isEnvTruthy(process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR)
113  }
114  
115  /**
116   * Check if running on Homespace (ant-internal cloud environment)
117   */
118  export function isRunningOnHomespace(): boolean {
119    return (
120      process.env.USER_TYPE === 'ant' &&
121      isEnvTruthy(process.env.COO_RUNNING_ON_HOMESPACE)
122    )
123  }
124  
125  /**
126   * Conservative check for whether Claude Code is running inside a protected
127   * (privileged or ASL3+) COO namespace or cluster.
128   *
129   * Conservative means: when signals are ambiguous, assume protected. We would
130   * rather over-report protected usage than miss it. Unprotected environments
131   * are homespace, namespaces on the open allowlist, and no k8s/COO signals
132   * at all (laptop/local dev).
133   *
134   * Used for telemetry to measure auto-mode usage in sensitive environments.
135   */
136  export function isInProtectedNamespace(): boolean {
137    // USER_TYPE is build-time --define'd; in external builds this block is
138    // DCE'd so the require() and namespace allowlist never appear in the bundle.
139    if (process.env.USER_TYPE === 'ant') {
140      /* eslint-disable @typescript-eslint/no-require-imports */
141      return (
142        require('./protectedNamespace.js') as typeof import('./protectedNamespace.js')
143      ).checkProtectedNamespace()
144      /* eslint-enable @typescript-eslint/no-require-imports */
145    }
146    return false
147  }
148  
149  // @[MODEL LAUNCH]: Add a Vertex region override env var for the new model.
150  /**
151   * Model prefix → env var for Vertex region overrides.
152   * Order matters: more specific prefixes must come before less specific ones
153   * (e.g., 'claude-opus-4-1' before 'claude-opus-4').
154   */
155  const VERTEX_REGION_OVERRIDES: ReadonlyArray<[string, string]> = [
156    ['claude-haiku-4-5', 'VERTEX_REGION_CLAUDE_HAIKU_4_5'],
157    ['claude-3-5-haiku', 'VERTEX_REGION_CLAUDE_3_5_HAIKU'],
158    ['claude-3-5-sonnet', 'VERTEX_REGION_CLAUDE_3_5_SONNET'],
159    ['claude-3-7-sonnet', 'VERTEX_REGION_CLAUDE_3_7_SONNET'],
160    ['claude-opus-4-1', 'VERTEX_REGION_CLAUDE_4_1_OPUS'],
161    ['claude-opus-4', 'VERTEX_REGION_CLAUDE_4_0_OPUS'],
162    ['claude-sonnet-4-6', 'VERTEX_REGION_CLAUDE_4_6_SONNET'],
163    ['claude-sonnet-4-5', 'VERTEX_REGION_CLAUDE_4_5_SONNET'],
164    ['claude-sonnet-4', 'VERTEX_REGION_CLAUDE_4_0_SONNET'],
165  ]
166  
167  /**
168   * Get the Vertex AI region for a specific model.
169   * Different models may be available in different regions.
170   */
171  export function getVertexRegionForModel(
172    model: string | undefined,
173  ): string | undefined {
174    if (model) {
175      const match = VERTEX_REGION_OVERRIDES.find(([prefix]) =>
176        model.startsWith(prefix),
177      )
178      if (match) {
179        return process.env[match[1]] || getDefaultVertexRegion()
180      }
181    }
182    return getDefaultVertexRegion()
183  }