import { CLAUDE_CODE_ENV_CONFIG, type ClaudeCodeEditableEnvKey } from "../inbound/claude/claude-code-env.config" import { config as exportEnvClaudeConfig, type ExportEnvClaudeCodeConfig } from "../inbound/claude/export-env-claude" import { errorCode, readTextFile, writeTextFile } from "../core/bun-fs" import { bunPath as path, homeDir } from "../core/paths" import type { ProviderMode } from "./types" export const CLAUDE_ENV_FIXED = CLAUDE_CODE_ENV_CONFIG.lockedEnv export const CLAUDE_MODEL_ENV_KEYS = Object.keys(CLAUDE_CODE_ENV_CONFIG.editableEnvDefaults) as ClaudeCodeEditableEnvKey[] /** All canEdit keys from export-env-claude config (model keys + extra editable keys like CLAUDE_CODE_DISABLE_1M_CONTEXT) */ export const EXPORT_ENV_CAN_EDIT_KEYS = Object.keys(exportEnvClaudeConfig.codex.canEdit) as Array /** Extra canEdit keys that are not model keys (stored in draft.extraEnv) */ export const EXPORT_ENV_EXTRA_EDITABLE_KEYS = EXPORT_ENV_CAN_EDIT_KEYS.filter( (key) => !(CLAUDE_MODEL_ENV_KEYS as readonly string[]).includes(key), ) /** All editable keys in order: model keys first, then extra canEdit keys */ export const ALL_EDITABLE_KEYS: readonly string[] = [...CLAUDE_MODEL_ENV_KEYS, ...EXPORT_ENV_EXTRA_EDITABLE_KEYS] /** Static keys from export-env-claude config, excluding ANTHROPIC_BASE_URL (auto-generated by system) */ export const EXPORT_ENV_STATIC_KEYS = Object.keys(exportEnvClaudeConfig.codex.static).filter( (key) => key !== "ANTHROPIC_BASE_URL", ) as string[] /** * Static key-value pairs for display (excluding ANTHROPIC_BASE_URL). * Auth-related keys (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY) are sourced from * CLAUDE_ENV_FIXED so that the displayed values always match what gets persisted * via managedEnvironmentEntries(). Other static keys fall back to export-env-claude config. */ export const EXPORT_ENV_STATIC_ENTRIES: Array<{ key: string; value: string }> = EXPORT_ENV_STATIC_KEYS.map((key) => ({ key, value: (CLAUDE_ENV_FIXED as Record)[key] ?? (exportEnvClaudeConfig.codex.static as Record)[key], })) export const CLAUDE_ENV_KEYS = [ "ANTHROPIC_BASE_URL", ...Object.keys(CLAUDE_ENV_FIXED), ...CLAUDE_MODEL_ENV_KEYS, ] as const export type ClaudeModelEnvKey = ClaudeCodeEditableEnvKey export interface ClaudeEnvironmentDraft extends Record { extraEnv: Record unsetEnv: string[] } export type ClaudeSettingsScope = "user" | "project" | "local" export type ShellKind = "posix" | "powershell" export type ShellDetection = { kind: ShellKind; name: string } | { kind: "unsupported"; name: string; reason: string } export interface ClaudeEnvironmentRunOptions { authFile?: string persist?: boolean settingsFile?: string apiPassword?: string } interface ClaudeSettingsFile { env?: Record [key: string]: unknown } export function exportEnvConfigForProvider(providerMode: ProviderMode = "codex") { return exportEnvClaudeConfig[providerMode] ?? exportEnvClaudeConfig.codex } export function defaultClaudeEnvironment(providerMode: ProviderMode = "codex"): ClaudeEnvironmentDraft { const defaults = modelEnvDefaultsForProvider(providerMode) return { ANTHROPIC_MODEL: modelEnvValue("ANTHROPIC_MODEL", defaults.ANTHROPIC_MODEL, providerMode), ANTHROPIC_DEFAULT_OPUS_MODEL: modelEnvValue("ANTHROPIC_DEFAULT_OPUS_MODEL", defaults.ANTHROPIC_DEFAULT_OPUS_MODEL, providerMode), ANTHROPIC_DEFAULT_SONNET_MODEL: modelEnvValue("ANTHROPIC_DEFAULT_SONNET_MODEL", defaults.ANTHROPIC_DEFAULT_SONNET_MODEL, providerMode), ANTHROPIC_DEFAULT_HAIKU_MODEL: modelEnvValue("ANTHROPIC_DEFAULT_HAIKU_MODEL", defaults.ANTHROPIC_DEFAULT_HAIKU_MODEL, providerMode), extraEnv: extraEnvDefaultsForProvider(providerMode), unsetEnv: [...CLAUDE_CODE_ENV_CONFIG.defaultUnsetEnv], } } export function claudeEnvironmentConfigPath(authFile: string) { return path.join(path.dirname(authFile), ".claude-env.json") } export function claudeSettingsPath(file?: string) { return file ?? path.join(homeDir(), ".claude", "settings.json") } export function claudeSettingsPathForScope(scope: ClaudeSettingsScope, cwd = process.cwd()) { if (scope === "project") return path.join(cwd, ".claude", "settings.json") if (scope === "local") return path.join(cwd, ".claude", "settings.local.json") return claudeSettingsPath() } export function claudeSettingsScopeLabel(scope: ClaudeSettingsScope) { if (scope === "project") return ".claude/settings.json" if (scope === "local") return ".claude/settings.local.json" return "~/.claude/settings.json" } export function recommendedClaudeEnvironment(providerMode: ProviderMode = "codex"): ClaudeEnvironmentDraft { const rec = exportEnvConfigForProvider(providerMode) return { ANTHROPIC_MODEL: rec.canEdit.ANTHROPIC_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL: rec.canEdit.ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL: rec.canEdit.ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_HAIKU_MODEL: rec.canEdit.ANTHROPIC_DEFAULT_HAIKU_MODEL, extraEnv: extraEnvDefaultsForProvider(providerMode), unsetEnv: [...CLAUDE_CODE_ENV_CONFIG.defaultUnsetEnv], } } export async function readClaudeSettingsEnvAsDraft(settingsFile: string, providerMode: ProviderMode = "codex"): Promise { const settings = await readClaudeSettingsFile(settingsFile) const env = settings.env ?? {} const defaults = defaultClaudeEnvironment(providerMode) // Read model keys from settings file const modelValues = { ANTHROPIC_MODEL: typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL : defaults.ANTHROPIC_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL: typeof env.ANTHROPIC_DEFAULT_OPUS_MODEL === "string" ? env.ANTHROPIC_DEFAULT_OPUS_MODEL : defaults.ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL: typeof env.ANTHROPIC_DEFAULT_SONNET_MODEL === "string" ? env.ANTHROPIC_DEFAULT_SONNET_MODEL : defaults.ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_HAIKU_MODEL: typeof env.ANTHROPIC_DEFAULT_HAIKU_MODEL === "string" ? env.ANTHROPIC_DEFAULT_HAIKU_MODEL : defaults.ANTHROPIC_DEFAULT_HAIKU_MODEL, } // Read extra editable keys from settings file, falling back to defaults const extraEnv: Record = { ...defaults.extraEnv } for (const key of EXPORT_ENV_EXTRA_EDITABLE_KEYS) { if (typeof env[key] === "string") { extraEnv[key] = env[key] as string } } return { ...modelValues, extraEnv, unsetEnv: [...defaults.unsetEnv], } } export async function readClaudeEnvironmentConfig(authFile: string, providerMode: ProviderMode = "codex"): Promise { try { return normalizeClaudeEnvironment(JSON.parse(await readTextFile(claudeEnvironmentConfigPath(authFile))) as Partial, providerMode) } catch { return defaultClaudeEnvironment(providerMode) } } export async function writeClaudeEnvironmentConfig(authFile: string, draft: ClaudeEnvironmentDraft, providerMode: ProviderMode = "codex") { await writeTextFile(claudeEnvironmentConfigPath(authFile), `${JSON.stringify(normalizeClaudeEnvironment(draft, providerMode), null, 2)}\n`) } export function claudeEnvironmentExports(draft: ClaudeEnvironmentDraft, baseUrl: string, apiPassword?: string) { return claudeEnvironmentPreviewLines(draft, baseUrl, apiPassword) } export function claudeEnvironmentCommands(draft: ClaudeEnvironmentDraft, baseUrl: string, _shell: ShellKind | ShellDetection = detectShell(), apiPassword?: string) { return claudeEnvironmentPreviewLines(draft, baseUrl, apiPassword) } export function claudeEnvironmentPowerShellCommands(draft: ClaudeEnvironmentDraft, baseUrl: string, apiPassword?: string) { return claudeEnvironmentPreviewLines(draft, baseUrl, apiPassword) } export function applyClaudeEnvironment(_draft: ClaudeEnvironmentDraft, _baseUrl: string) { return } export function unsetClaudeEnvironment(_draft: ClaudeEnvironmentDraft = defaultClaudeEnvironment()) { return } export function claudeEnvironmentUnsetCommands(draft: ClaudeEnvironmentDraft = defaultClaudeEnvironment(), _shell: ShellKind | ShellDetection = detectShell()) { return managedEnvironmentKeys(draft).map((key) => key) } export async function echoClaudeEnvironment(draft: ClaudeEnvironmentDraft, baseUrl: string, shell: ShellKind | ShellDetection = detectShell(), apiPassword?: string) { const output = claudeEnvironmentCommands(draft, baseUrl, shell, apiPassword).join("\n") return echoShellOutput(output, shell) } export async function runClaudeEnvironmentSet(draft: ClaudeEnvironmentDraft, baseUrl: string, shell: ShellKind | ShellDetection = detectShell(), options?: ClaudeEnvironmentRunOptions) { if (options?.persist !== false) await persistClaudeEnvironment(draft, baseUrl, shell, options) return appendPersistenceNote(formatManagedEnvironment(managedEnvironmentEntries(draft, baseUrl, options?.apiPassword), options?.settingsFile), options) } export async function echoClaudeEnvironmentUnset(draft: ClaudeEnvironmentDraft = defaultClaudeEnvironment(), shell: ShellKind | ShellDetection = detectShell()) { const output = claudeEnvironmentUnsetCommands(draft, shell).join("\n") return echoShellOutput(output, shell) } export async function runClaudeEnvironmentUnset(draft: ClaudeEnvironmentDraft = defaultClaudeEnvironment(), shell: ShellKind | ShellDetection = detectShell(), options?: ClaudeEnvironmentRunOptions) { if (options?.persist !== false) await persistClaudeEnvironmentUnset(draft, shell, options) return appendPersistenceNote(formatManagedEnvironment([], options?.settingsFile), options) } export async function persistClaudeEnvironment(draft: ClaudeEnvironmentDraft, baseUrl: string, _shell: ShellKind | ShellDetection, options?: ClaudeEnvironmentRunOptions) { const settings = await readClaudeSettingsFile(options?.settingsFile) const nextEnv = { ...settings.env, ...Object.fromEntries(managedEnvironmentEntries(draft, baseUrl, options?.apiPassword)), } unsetKeysForSet(draft, baseUrl, options?.apiPassword).forEach((key) => { delete nextEnv[key] }) await writeClaudeSettingsFile({ ...settings, env: nextEnv }, options?.settingsFile) } export async function persistClaudeEnvironmentUnset(draft: ClaudeEnvironmentDraft = defaultClaudeEnvironment(), _shell: ShellKind | ShellDetection, options?: ClaudeEnvironmentRunOptions) { const settings = await readClaudeSettingsFile(options?.settingsFile) const nextEnv = { ...settings.env } managedEnvironmentKeys(draft).forEach((key) => { delete nextEnv[key] }) await writeClaudeSettingsFile({ ...settings, env: nextEnv }, options?.settingsFile) } async function echoShellOutput(output: string, shell: ShellKind | ShellDetection) { const kind = typeof shell === "string" ? shell : shell.kind if (typeof shell !== "string" && shell.kind === "unsupported") throw new Error(shell.reason) if (kind === "unsupported") throw new Error("Unsupported shell") return output.trimEnd() } export function detectShell(env: Record = process.env, platform = process.platform): ShellDetection { const override = env.CODEX2CLAUDECODE_SHELL?.toLowerCase() if (override === "posix" || override === "powershell") return { kind: override, name: override } if (override) return { kind: "unsupported", name: override, reason: `Unsupported shell: ${override}` } if (platform === "win32") { const processName = (env.PSModulePath || env.POWERSHELL_DISTRIBUTION_CHANNEL ? "powershell" : env.ComSpec || "cmd").toLowerCase() if (processName.includes("powershell") || processName.includes("pwsh")) return { kind: "powershell", name: "PowerShell" } return { kind: "unsupported", name: "cmd", reason: "Unsupported shell: cmd. Use PowerShell or set CODEX2CLAUDECODE_SHELL=powershell." } } const shell = (env.SHELL ?? "sh").split("/").pop()?.toLowerCase() ?? "sh" if (["sh", "bash", "zsh", "dash", "ksh"].includes(shell)) return { kind: "posix", name: shell } return { kind: "unsupported", name: shell, reason: `Unsupported shell: ${shell}. Supported shells: sh, bash, zsh, dash, ksh, PowerShell.` } } function normalizeClaudeEnvironment(input: Partial, providerMode: ProviderMode = "codex"): ClaudeEnvironmentDraft { const defaults = defaultClaudeEnvironment(providerMode) const extraEnv = { ...defaults.extraEnv, ...normalizeStringMap(input.extraEnv), } const unsetEnv = [...new Set([...defaults.unsetEnv, ...normalizeStringList(input.unsetEnv)])].filter((key) => !(key in extraEnv)) return { ANTHROPIC_MODEL: input.ANTHROPIC_MODEL ?? defaults.ANTHROPIC_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL: input.ANTHROPIC_DEFAULT_OPUS_MODEL ?? defaults.ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL: input.ANTHROPIC_DEFAULT_SONNET_MODEL ?? defaults.ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_HAIKU_MODEL: input.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? defaults.ANTHROPIC_DEFAULT_HAIKU_MODEL, extraEnv, unsetEnv, } } function modelEnvDefaultsForProvider(providerMode: ProviderMode) { if (providerMode === "codex") return CLAUDE_CODE_ENV_CONFIG.editableEnvDefaults const canEdit = exportEnvConfigForProvider(providerMode).canEdit return { ANTHROPIC_MODEL: canEdit.ANTHROPIC_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL: canEdit.ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL: canEdit.ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_HAIKU_MODEL: canEdit.ANTHROPIC_DEFAULT_HAIKU_MODEL, } } function extraEnvDefaultsForProvider(providerMode: ProviderMode) { const rec = exportEnvConfigForProvider(providerMode) return { ...CLAUDE_CODE_ENV_CONFIG.defaultExtraEnv, ...extraEditableEnvDefaultsForProvider(providerMode), NODE_TLS_REJECT_UNAUTHORIZED: rec.static.NODE_TLS_REJECT_UNAUTHORIZED, } } function extraEditableEnvDefaultsForProvider(providerMode: ProviderMode) { const canEdit = exportEnvConfigForProvider(providerMode).canEdit return Object.fromEntries(EXPORT_ENV_EXTRA_EDITABLE_KEYS.map((key) => [key, canEdit[key]] as const)) } function modelEnvValue(key: ClaudeCodeEditableEnvKey, fallback: string, providerMode: ProviderMode) { if (providerMode === "kiro") return fallback return process.env[key] ?? fallback } function claudeEnvironmentPreviewLines(draft: ClaudeEnvironmentDraft, baseUrl: string, apiPassword?: string) { return [ `Target file: ${claudeSettingsPath()}`, ...managedEnvironmentEntries(draft, baseUrl, apiPassword).map(([key, value]) => key === "ANTHROPIC_AUTH_TOKEN" || key === "ANTHROPIC_API_KEY" ? `${key} = [redacted]` : `${key} = ${JSON.stringify(value)}`, ), ...unsetKeysForSet(draft, baseUrl, apiPassword).map((key) => `delete ${key}`), ] } function managedEnvironmentEntries(draft: ClaudeEnvironmentDraft, baseUrl: string, apiPassword?: string): Array<[string, string]> { const normalized = normalizeClaudeEnvironment(draft) const authValue = apiPassword || CLAUDE_ENV_FIXED.ANTHROPIC_AUTH_TOKEN return [ ["ANTHROPIC_BASE_URL", baseUrl], ["ANTHROPIC_AUTH_TOKEN", authValue], ["ANTHROPIC_API_KEY", authValue], ...CLAUDE_MODEL_ENV_KEYS.map((key) => [key, normalized[key]] as [string, string]), ...Object.entries(normalized.extraEnv), ] } function managedEnvironmentKeys(draft: ClaudeEnvironmentDraft) { const normalized = normalizeClaudeEnvironment(draft) return [...new Set([...CLAUDE_ENV_KEYS, ...Object.keys(normalized.extraEnv), ...normalized.unsetEnv])] } function unsetKeysForSet(draft: ClaudeEnvironmentDraft, baseUrl: string, apiPassword?: string) { const managedKeys = new Set(managedEnvironmentEntries(draft, baseUrl, apiPassword).map(([key]) => key)) return normalizeClaudeEnvironment(draft).unsetEnv.filter((key) => !managedKeys.has(key)) } function normalizeStringMap(input: unknown) { if (!input || typeof input !== "object" || Array.isArray(input)) return {} return Object.fromEntries( Object.entries(input) .map(([key, value]) => [key.trim(), value] as const) .filter(([key, value]) => key && typeof value === "string"), ) } function normalizeStringList(input: unknown) { if (!Array.isArray(input)) return [] return [...new Set(input.filter((value): value is string => typeof value === "string").map((value) => value.trim()).filter(Boolean))] } async function readClaudeSettingsFile(file?: string) { const settingsFile = claudeSettingsPath(file) try { const parsed = JSON.parse(await readTextFile(settingsFile)) as ClaudeSettingsFile return { ...parsed, env: normalizeSettingsEnv(parsed.env), } } catch (error) { if (errorCode(error) === "ENOENT") { return { env: {} } satisfies ClaudeSettingsFile } throw error } } async function writeClaudeSettingsFile(settings: ClaudeSettingsFile, file?: string) { const settingsFile = claudeSettingsPath(file) await writeTextFile(settingsFile, `${JSON.stringify({ ...settings, env: normalizeSettingsEnv(settings.env) }, null, 2)}\n`) } function normalizeSettingsEnv(env: unknown) { if (!env || typeof env !== "object" || Array.isArray(env)) return {} as Record return { ...env } as Record } function formatManagedEnvironment(entries: Array<[string, string]>, settingsFile?: string) { if (!entries.length) return `Updated ${claudeSettingsPath(settingsFile)} env object.` return [ `Updated ${claudeSettingsPath(settingsFile)} env object:`, ...entries.map(([key, value]) => key === "ANTHROPIC_AUTH_TOKEN" || key === "ANTHROPIC_API_KEY" ? `${key}=[redacted]` : `${key}=${value}`, ), ].join("\n") } function appendPersistenceNote(output: string, options?: ClaudeEnvironmentRunOptions) { if (options?.persist === false) return output return `${output}\n\nSaved to ${claudeSettingsPath(options?.settingsFile)}.` }