import { isRemoteManagedSettingsEligible } from '../services/remoteManagedSettings/syncCache.js' import { clearCACertsCache } from './caCerts.js' import { getGlobalConfig } from './config.js' import { isEnvTruthy } from './envUtils.js' import { isProviderManagedEnvVar, SAFE_ENV_VARS, } from './managedEnvConstants.js' import { clearMTLSCache } from './mtls.js' import { clearProxyCache, configureGlobalAgents } from './proxy.js' import { isSettingSourceEnabled } from './settings/constants.js' import { getSettings_DEPRECATED, getSettingsForSource, } from './settings/settings.js' /** * `claude ssh` remote: ANTHROPIC_UNIX_SOCKET routes auth through a -R forwarded * socket to a local proxy, and the launcher sets a handful of placeholder auth * env vars that the remote's ~/.claude settings.env MUST NOT clobber (see * isAnthropicAuthEnabled). Strip them from any settings-sourced env object. */ function withoutSSHTunnelVars( env: Record | undefined, ): Record { if (!env || !process.env.ANTHROPIC_UNIX_SOCKET) return env || {} const { ANTHROPIC_UNIX_SOCKET: _1, ANTHROPIC_BASE_URL: _2, ANTHROPIC_API_KEY: _3, ANTHROPIC_AUTH_TOKEN: _4, CLAUDE_CODE_OAUTH_TOKEN: _5, ...rest } = env return rest } /** * When the host owns inference routing (sets * CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST in spawn env), strip * provider-selection / model-default vars from settings-sourced env so a * user's ~/.claude/settings.json can't redirect requests away from the * host-configured provider. */ function withoutHostManagedProviderVars( env: Record | undefined, ): Record { if (!env) return {} if (!isEnvTruthy(process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST)) { return env } const out: Record = {} for (const [key, value] of Object.entries(env)) { if (!isProviderManagedEnvVar(key)) { out[key] = value } } return out } /** * Snapshot of env keys present before any settings.env is applied — for CCD, * these are the keys the desktop host set to orchestrate the subprocess. * Settings must not override them (OTEL_LOGS_EXPORTER=console would corrupt * the stdio JSON-RPC transport). Keys added LATER by user/project settings * are not in this set, so mid-session settings.json changes still apply. * Lazy-captured on first applySafeConfigEnvironmentVariables() call. */ let ccdSpawnEnvKeys: Set | null | undefined function withoutCcdSpawnEnvKeys( env: Record | undefined, ): Record { if (!env || !ccdSpawnEnvKeys) return env || {} const out: Record = {} for (const [key, value] of Object.entries(env)) { if (!ccdSpawnEnvKeys.has(key)) out[key] = value } return out } /** * Compose the strip filters applied to every settings-sourced env object. */ function filterSettingsEnv( env: Record | undefined, ): Record { return withoutCcdSpawnEnvKeys( withoutHostManagedProviderVars(withoutSSHTunnelVars(env)), ) } /** * Trusted setting sources whose env vars can be applied before the trust dialog. * * - userSettings (~/.claude/settings.json): controlled by the user, not project-specific * - flagSettings (--settings CLI flag or SDK inline settings): explicitly passed by the user * - policySettings (managed settings from enterprise API or local managed-settings.json): * controlled by IT/admin (highest priority, cannot be overridden) * * Project-scoped sources (projectSettings, localSettings) are excluded because they live * inside the project directory and could be committed by a malicious actor to redirect * traffic (e.g., ANTHROPIC_BASE_URL) to an attacker-controlled server. */ const TRUSTED_SETTING_SOURCES = [ 'userSettings', 'flagSettings', 'policySettings', ] as const /** * Apply environment variables from trusted sources to process.env. * Called before the trust dialog so that user/enterprise env vars like * ANTHROPIC_BASE_URL take effect during first-run/onboarding. * * For trusted sources (user settings, managed settings, CLI flags), ALL env vars * are applied — including ones like ANTHROPIC_BASE_URL that would be dangerous * from project-scoped settings. * * For project-scoped sources (projectSettings, localSettings), only safe env vars * from the SAFE_ENV_VARS allowlist are applied. These are applied after trust is * fully established via applyConfigEnvironmentVariables(). */ export function applySafeConfigEnvironmentVariables(): void { // Capture CCD spawn-env keys before any settings.env is applied (once). if (ccdSpawnEnvKeys === undefined) { ccdSpawnEnvKeys = process.env.CLAUDE_CODE_ENTRYPOINT === 'claude-desktop' ? new Set(Object.keys(process.env)) : null } // Global config (~/.claude.json) is user-controlled. In CCD mode, // filterSettingsEnv strips keys that were in the spawn env snapshot so // the desktop host's operational vars (OTEL, etc.) are not overridden. Object.assign(process.env, filterSettingsEnv(getGlobalConfig().env)) // Apply ALL env vars from trusted setting sources, policySettings last. // Gate on isSettingSourceEnabled so SDK settingSources: [] (isolation mode) // doesn't get clobbered by ~/.claude/settings.json env (gh#217). policy/flag // sources are always enabled, so this only ever filters userSettings. for (const source of TRUSTED_SETTING_SOURCES) { if (source === 'policySettings') continue if (!isSettingSourceEnabled(source)) continue Object.assign( process.env, filterSettingsEnv(getSettingsForSource(source)?.env), ) } // Compute remote-managed-settings eligibility now, with userSettings and // flagSettings env applied. Eligibility reads CLAUDE_CODE_USE_BEDROCK, // ANTHROPIC_BASE_URL — both settable via settings.env. // getSettingsForSource('policySettings') below consults the remote cache, // which guards on this. The two-phase structure makes the ordering // dependency visible: non-policy env → eligibility → policy env. isRemoteManagedSettingsEligible() Object.assign( process.env, filterSettingsEnv(getSettingsForSource('policySettings')?.env), ) // Apply only safe env vars from the fully-merged settings (which includes // project-scoped sources). For safe vars that also exist in trusted sources, // the merged value (which may come from a higher-priority project source) // will overwrite the trusted value — this is acceptable since these vars are // in the safe allowlist. Only policySettings values are guaranteed to survive // unchanged (it has the highest merge priority in both loops) — except // provider-routing vars, which filterSettingsEnv strips from every source // when CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST is set. const settingsEnv = filterSettingsEnv(getSettings_DEPRECATED()?.env) for (const [key, value] of Object.entries(settingsEnv)) { if (SAFE_ENV_VARS.has(key.toUpperCase())) { process.env[key] = value } } } /** * Apply environment variables from settings to process.env. * This applies ALL environment variables (except provider-routing vars when * CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST is set — see filterSettingsEnv) and * should only be called after trust is established. This applies potentially * dangerous environment variables such as LD_PRELOAD, PATH, etc. */ export function applyConfigEnvironmentVariables(): void { Object.assign(process.env, filterSettingsEnv(getGlobalConfig().env)) Object.assign(process.env, filterSettingsEnv(getSettings_DEPRECATED()?.env)) // Clear caches so agents are rebuilt with the new env vars clearCACertsCache() clearMTLSCache() clearProxyCache() // Reconfigure proxy/mTLS agents to pick up any proxy env vars from settings configureGlobalAgents() }