200 lines
7.7 KiB
TypeScript
200 lines
7.7 KiB
TypeScript
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<string, string> | undefined,
|
|
): Record<string, string> {
|
|
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<string, string> | undefined,
|
|
): Record<string, string> {
|
|
if (!env) return {}
|
|
if (!isEnvTruthy(process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST)) {
|
|
return env
|
|
}
|
|
const out: Record<string, string> = {}
|
|
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<string> | null | undefined
|
|
|
|
function withoutCcdSpawnEnvKeys(
|
|
env: Record<string, string> | undefined,
|
|
): Record<string, string> {
|
|
if (!env || !ccdSpawnEnvKeys) return env || {}
|
|
const out: Record<string, string> = {}
|
|
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<string, string> | undefined,
|
|
): Record<string, string> {
|
|
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()
|
|
}
|