/ utils / managedEnv.ts
managedEnv.ts
  1  import { isRemoteManagedSettingsEligible } from '../services/remoteManagedSettings/syncCache.js'
  2  import { clearCACertsCache } from './caCerts.js'
  3  import { getGlobalConfig } from './config.js'
  4  import { isEnvTruthy } from './envUtils.js'
  5  import {
  6    isProviderManagedEnvVar,
  7    SAFE_ENV_VARS,
  8  } from './managedEnvConstants.js'
  9  import { clearMTLSCache } from './mtls.js'
 10  import { clearProxyCache, configureGlobalAgents } from './proxy.js'
 11  import { isSettingSourceEnabled } from './settings/constants.js'
 12  import {
 13    getSettings_DEPRECATED,
 14    getSettingsForSource,
 15  } from './settings/settings.js'
 16  
 17  /**
 18   * `claude ssh` remote: ANTHROPIC_UNIX_SOCKET routes auth through a -R forwarded
 19   * socket to a local proxy, and the launcher sets a handful of placeholder auth
 20   * env vars that the remote's ~/.claude settings.env MUST NOT clobber (see
 21   * isAnthropicAuthEnabled). Strip them from any settings-sourced env object.
 22   */
 23  function withoutSSHTunnelVars(
 24    env: Record<string, string> | undefined,
 25  ): Record<string, string> {
 26    if (!env || !process.env.ANTHROPIC_UNIX_SOCKET) return env || {}
 27    const {
 28      ANTHROPIC_UNIX_SOCKET: _1,
 29      ANTHROPIC_BASE_URL: _2,
 30      ANTHROPIC_API_KEY: _3,
 31      ANTHROPIC_AUTH_TOKEN: _4,
 32      CLAUDE_CODE_OAUTH_TOKEN: _5,
 33      ...rest
 34    } = env
 35    return rest
 36  }
 37  
 38  /**
 39   * When the host owns inference routing (sets
 40   * CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST in spawn env), strip
 41   * provider-selection / model-default vars from settings-sourced env so a
 42   * user's ~/.claude/settings.json can't redirect requests away from the
 43   * host-configured provider.
 44   */
 45  function withoutHostManagedProviderVars(
 46    env: Record<string, string> | undefined,
 47  ): Record<string, string> {
 48    if (!env) return {}
 49    if (!isEnvTruthy(process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST)) {
 50      return env
 51    }
 52    const out: Record<string, string> = {}
 53    for (const [key, value] of Object.entries(env)) {
 54      if (!isProviderManagedEnvVar(key)) {
 55        out[key] = value
 56      }
 57    }
 58    return out
 59  }
 60  
 61  /**
 62   * Snapshot of env keys present before any settings.env is applied — for CCD,
 63   * these are the keys the desktop host set to orchestrate the subprocess.
 64   * Settings must not override them (OTEL_LOGS_EXPORTER=console would corrupt
 65   * the stdio JSON-RPC transport). Keys added LATER by user/project settings
 66   * are not in this set, so mid-session settings.json changes still apply.
 67   * Lazy-captured on first applySafeConfigEnvironmentVariables() call.
 68   */
 69  let ccdSpawnEnvKeys: Set<string> | null | undefined
 70  
 71  function withoutCcdSpawnEnvKeys(
 72    env: Record<string, string> | undefined,
 73  ): Record<string, string> {
 74    if (!env || !ccdSpawnEnvKeys) return env || {}
 75    const out: Record<string, string> = {}
 76    for (const [key, value] of Object.entries(env)) {
 77      if (!ccdSpawnEnvKeys.has(key)) out[key] = value
 78    }
 79    return out
 80  }
 81  
 82  /**
 83   * Compose the strip filters applied to every settings-sourced env object.
 84   */
 85  function filterSettingsEnv(
 86    env: Record<string, string> | undefined,
 87  ): Record<string, string> {
 88    return withoutCcdSpawnEnvKeys(
 89      withoutHostManagedProviderVars(withoutSSHTunnelVars(env)),
 90    )
 91  }
 92  
 93  /**
 94   * Trusted setting sources whose env vars can be applied before the trust dialog.
 95   *
 96   * - userSettings (~/.claude/settings.json): controlled by the user, not project-specific
 97   * - flagSettings (--settings CLI flag or SDK inline settings): explicitly passed by the user
 98   * - policySettings (managed settings from enterprise API or local managed-settings.json):
 99   *   controlled by IT/admin (highest priority, cannot be overridden)
100   *
101   * Project-scoped sources (projectSettings, localSettings) are excluded because they live
102   * inside the project directory and could be committed by a malicious actor to redirect
103   * traffic (e.g., ANTHROPIC_BASE_URL) to an attacker-controlled server.
104   */
105  const TRUSTED_SETTING_SOURCES = [
106    'userSettings',
107    'flagSettings',
108    'policySettings',
109  ] as const
110  
111  /**
112   * Apply environment variables from trusted sources to process.env.
113   * Called before the trust dialog so that user/enterprise env vars like
114   * ANTHROPIC_BASE_URL take effect during first-run/onboarding.
115   *
116   * For trusted sources (user settings, managed settings, CLI flags), ALL env vars
117   * are applied — including ones like ANTHROPIC_BASE_URL that would be dangerous
118   * from project-scoped settings.
119   *
120   * For project-scoped sources (projectSettings, localSettings), only safe env vars
121   * from the SAFE_ENV_VARS allowlist are applied. These are applied after trust is
122   * fully established via applyConfigEnvironmentVariables().
123   */
124  export function applySafeConfigEnvironmentVariables(): void {
125    // Capture CCD spawn-env keys before any settings.env is applied (once).
126    if (ccdSpawnEnvKeys === undefined) {
127      ccdSpawnEnvKeys =
128        process.env.CLAUDE_CODE_ENTRYPOINT === 'claude-desktop'
129          ? new Set(Object.keys(process.env))
130          : null
131    }
132  
133    // Global config (~/.claude.json) is user-controlled. In CCD mode,
134    // filterSettingsEnv strips keys that were in the spawn env snapshot so
135    // the desktop host's operational vars (OTEL, etc.) are not overridden.
136    Object.assign(process.env, filterSettingsEnv(getGlobalConfig().env))
137  
138    // Apply ALL env vars from trusted setting sources, policySettings last.
139    // Gate on isSettingSourceEnabled so SDK settingSources: [] (isolation mode)
140    // doesn't get clobbered by ~/.claude/settings.json env (gh#217). policy/flag
141    // sources are always enabled, so this only ever filters userSettings.
142    for (const source of TRUSTED_SETTING_SOURCES) {
143      if (source === 'policySettings') continue
144      if (!isSettingSourceEnabled(source)) continue
145      Object.assign(
146        process.env,
147        filterSettingsEnv(getSettingsForSource(source)?.env),
148      )
149    }
150  
151    // Compute remote-managed-settings eligibility now, with userSettings and
152    // flagSettings env applied. Eligibility reads CLAUDE_CODE_USE_BEDROCK,
153    // ANTHROPIC_BASE_URL — both settable via settings.env.
154    // getSettingsForSource('policySettings') below consults the remote cache,
155    // which guards on this. The two-phase structure makes the ordering
156    // dependency visible: non-policy env → eligibility → policy env.
157    isRemoteManagedSettingsEligible()
158  
159    Object.assign(
160      process.env,
161      filterSettingsEnv(getSettingsForSource('policySettings')?.env),
162    )
163  
164    // Apply only safe env vars from the fully-merged settings (which includes
165    // project-scoped sources). For safe vars that also exist in trusted sources,
166    // the merged value (which may come from a higher-priority project source)
167    // will overwrite the trusted value — this is acceptable since these vars are
168    // in the safe allowlist. Only policySettings values are guaranteed to survive
169    // unchanged (it has the highest merge priority in both loops) — except
170    // provider-routing vars, which filterSettingsEnv strips from every source
171    // when CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST is set.
172    const settingsEnv = filterSettingsEnv(getSettings_DEPRECATED()?.env)
173    for (const [key, value] of Object.entries(settingsEnv)) {
174      if (SAFE_ENV_VARS.has(key.toUpperCase())) {
175        process.env[key] = value
176      }
177    }
178  }
179  
180  /**
181   * Apply environment variables from settings to process.env.
182   * This applies ALL environment variables (except provider-routing vars when
183   * CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST is set — see filterSettingsEnv) and
184   * should only be called after trust is established. This applies potentially
185   * dangerous environment variables such as LD_PRELOAD, PATH, etc.
186   */
187  export function applyConfigEnvironmentVariables(): void {
188    Object.assign(process.env, filterSettingsEnv(getGlobalConfig().env))
189  
190    Object.assign(process.env, filterSettingsEnv(getSettings_DEPRECATED()?.env))
191  
192    // Clear caches so agents are rebuilt with the new env vars
193    clearCACertsCache()
194    clearMTLSCache()
195    clearProxyCache()
196  
197    // Reconfigure proxy/mTLS agents to pick up any proxy env vars from settings
198    configureGlobalAgents()
199  }