managedEnv.ts 7.7 KB

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