From c39b9bf44c05cd5dedec7b40e22112084dc37386 Mon Sep 17 00:00:00 2001 From: Zero Liu Date: Wed, 20 May 2026 22:58:57 -0700 Subject: [PATCH] feat(agent-mode): per-backend env variable overrides (#2499) Adds an "Environment variables" editor to each Agent Mode backend (Claude, Codex, opencode) so users can inject vars like CLAUDE_CONFIG_DIR, CODEX_HOME, XDG_CONFIG_HOME, or HTTPS_PROXY without polluting the parent shell. Overrides are merged onto process.env at spawn (Codex, opencode) or per prompt() (Claude SDK), and persisted through a shared sanitizer that enforces POSIX-style names, drops control chars, and caps the record at 64 entries. The editor debounces commits to avoid rewriting settings on every keystroke. Also relocates BinaryPathSetting into src/agentMode/backends/shared/ to honor the agent-mode layer boundaries. Co-authored-by: Claude Opus 4.7 (1M context) --- .../backends/claude/ClaudeSettingsPanel.tsx | 10 +- src/agentMode/backends/claude/descriptor.ts | 1 + src/agentMode/backends/codex/CodexBackend.ts | 3 +- .../backends/codex/CodexSettingsPanel.tsx | 41 +++-- .../backends/opencode/OpencodeBackend.ts | 4 + .../opencode/OpencodeSettingsPanel.tsx | 12 +- .../backends/shared/BinaryInstallContent.tsx | 2 +- .../backends/shared}/BinaryPathSetting.tsx | 0 .../backends/shared/EnvOverridesSetting.tsx | 174 ++++++++++++++++++ .../shared/SimpleBackendSettingsPanel.tsx | 2 +- .../backends/shared/simpleBinaryBackend.ts | 7 +- src/agentMode/sdk/ClaudeSdkBackendProcess.ts | 11 ++ src/settings/model.test.ts | 89 ++++++++- src/settings/model.ts | 42 +++++ 14 files changed, 375 insertions(+), 23 deletions(-) rename src/{components/agent => agentMode/backends/shared}/BinaryPathSetting.tsx (100%) create mode 100644 src/agentMode/backends/shared/EnvOverridesSetting.tsx diff --git a/src/agentMode/backends/claude/ClaudeSettingsPanel.tsx b/src/agentMode/backends/claude/ClaudeSettingsPanel.tsx index 0cca95de..c90e87ef 100644 --- a/src/agentMode/backends/claude/ClaudeSettingsPanel.tsx +++ b/src/agentMode/backends/claude/ClaudeSettingsPanel.tsx @@ -1,4 +1,5 @@ -import { BinaryPathSetting } from "@/components/agent/BinaryPathSetting"; +import { BinaryPathSetting } from "@/agentMode/backends/shared/BinaryPathSetting"; +import { EnvOverridesSetting } from "@/agentMode/backends/shared/EnvOverridesSetting"; import { Button } from "@/components/ui/button"; import { SettingItem } from "@/components/ui/setting-item"; import type CopilotPlugin from "@/main"; @@ -149,6 +150,13 @@ export const ClaudeSettingsPanel: React.FC = () => { checked={Boolean(settings.agentMode?.backends?.claude?.enableThinking)} onCheckedChange={(checked) => updateClaudeFields({ enableThinking: checked })} /> + + updateClaudeFields({ envOverrides: next })} + hintExamples={["CLAUDE_CONFIG_DIR", "HTTPS_PROXY"]} + /> ); }; diff --git a/src/agentMode/backends/claude/descriptor.ts b/src/agentMode/backends/claude/descriptor.ts index b5c7a4de..5ab25300 100644 --- a/src/agentMode/backends/claude/descriptor.ts +++ b/src/agentMode/backends/claude/descriptor.ts @@ -191,6 +191,7 @@ export const ClaudeBackendDescriptor: BackendDescriptor = { descriptor: args.descriptor, askUserQuestion: (questions) => openAskUserQuestionModal(args.app, questions), getEnableThinking: () => Boolean(getSettings().agentMode?.backends?.claude?.enableThinking), + getEnvOverrides: () => getSettings().agentMode?.backends?.claude?.envOverrides, isPlanModePlanFilePath: isClaudePlanModePlanFilePath, getDefaultModelId: () => getSettings().agentMode?.backends?.claude?.defaultModel?.baseModelId, // Spawn-time skill-creation directive: read the current diff --git a/src/agentMode/backends/codex/CodexBackend.ts b/src/agentMode/backends/codex/CodexBackend.ts index d9546551..ac3d67a8 100644 --- a/src/agentMode/backends/codex/CodexBackend.ts +++ b/src/agentMode/backends/codex/CodexBackend.ts @@ -23,7 +23,8 @@ export class CodexBackend implements AcpBackend { async buildSpawnDescriptor(_ctx: { vaultBasePath: string }): Promise { const descriptor = buildSimpleSpawnDescriptor( getSettings().agentMode?.backends?.codex?.binaryPath, - "Codex binary path not configured. Open Agent Mode settings and set the path to codex-acp." + "Codex binary path not configured. Open Agent Mode settings and set the path to codex-acp.", + getSettings().agentMode?.backends?.codex?.envOverrides ); // Spawn-time skill-creation directive: forwarded into codex's // `developer_instructions` config field via codex-acp's `-c key=value` diff --git a/src/agentMode/backends/codex/CodexSettingsPanel.tsx b/src/agentMode/backends/codex/CodexSettingsPanel.tsx index 1468a78c..17cfb606 100644 --- a/src/agentMode/backends/codex/CodexSettingsPanel.tsx +++ b/src/agentMode/backends/codex/CodexSettingsPanel.tsx @@ -1,5 +1,6 @@ +import { EnvOverridesSetting } from "@/agentMode/backends/shared/EnvOverridesSetting"; import type CopilotPlugin from "@/main"; -import { getSettings } from "@/settings/model"; +import { getSettings, useSettingsValue } from "@/settings/model"; import type { App } from "obsidian"; import React from "react"; import { SimpleBackendSettingsPanel } from "@/agentMode/backends/shared/SimpleBackendSettingsPanel"; @@ -11,16 +12,28 @@ interface Props { app: App; } -export const CodexSettingsPanel: React.FC = ({ app }) => ( - getSettings().agentMode?.backends?.codex?.binaryPath ?? ""} - persistPath={(path) => updateCodexFields({ binaryPath: path })} - openInstallModal={() => new CodexInstallModal(app).open()} - /> -); +export const CodexSettingsPanel: React.FC = ({ app }) => { + const settings = useSettingsValue(); + return ( + <> + getSettings().agentMode?.backends?.codex?.binaryPath ?? ""} + persistPath={(path) => updateCodexFields({ binaryPath: path })} + openInstallModal={() => new CodexInstallModal(app).open()} + /> + + updateCodexFields({ envOverrides: next })} + hintExamples={["CODEX_HOME", "OPENAI_BASE_URL"]} + /> + + ); +}; diff --git a/src/agentMode/backends/opencode/OpencodeBackend.ts b/src/agentMode/backends/opencode/OpencodeBackend.ts index 22ab778b..1f22f0f8 100644 --- a/src/agentMode/backends/opencode/OpencodeBackend.ts +++ b/src/agentMode/backends/opencode/OpencodeBackend.ts @@ -83,6 +83,7 @@ export class OpencodeBackend implements AcpBackend { } const config = await buildOpencodeConfig(); + const envOverrides = getSettings().agentMode?.backends?.opencode?.envOverrides ?? {}; return { command: binaryPath, @@ -90,6 +91,9 @@ export class OpencodeBackend implements AcpBackend { env: { ...process.env, OPENCODE_CONFIG_CONTENT: JSON.stringify(config), + // User overrides last — they can replace OPENCODE_CONFIG_CONTENT + // intentionally if they need to point opencode at a different config. + ...envOverrides, }, }; } diff --git a/src/agentMode/backends/opencode/OpencodeSettingsPanel.tsx b/src/agentMode/backends/opencode/OpencodeSettingsPanel.tsx index 99558b94..32bc4710 100644 --- a/src/agentMode/backends/opencode/OpencodeSettingsPanel.tsx +++ b/src/agentMode/backends/opencode/OpencodeSettingsPanel.tsx @@ -1,11 +1,12 @@ import { OpencodeInstallModal } from "@/agentMode/backends/opencode/OpencodeInstallModal"; -import { BinaryPathSetting } from "@/components/agent/BinaryPathSetting"; +import { BinaryPathSetting } from "@/agentMode/backends/shared/BinaryPathSetting"; +import { EnvOverridesSetting } from "@/agentMode/backends/shared/EnvOverridesSetting"; import { ConfirmModal } from "@/components/modals/ConfirmModal"; import { Button } from "@/components/ui/button"; import { SettingItem } from "@/components/ui/setting-item"; import { logError } from "@/logger"; import type CopilotPlugin from "@/main"; -import { useSettingsValue } from "@/settings/model"; +import { updateAgentModeBackendFields, useSettingsValue } from "@/settings/model"; import type { App } from "obsidian"; import { Notice } from "obsidian"; import React from "react"; @@ -146,6 +147,13 @@ export const OpencodeSettingsPanel: React.FC = ({ plugin, app }) => { )} )} + + updateAgentModeBackendFields("opencode", { envOverrides: next })} + hintExamples={["XDG_CONFIG_HOME", "HTTPS_PROXY"]} + /> ); }; diff --git a/src/agentMode/backends/shared/BinaryInstallContent.tsx b/src/agentMode/backends/shared/BinaryInstallContent.tsx index 7e7d9299..e6ad4aa5 100644 --- a/src/agentMode/backends/shared/BinaryInstallContent.tsx +++ b/src/agentMode/backends/shared/BinaryInstallContent.tsx @@ -1,5 +1,5 @@ import { ReactModal } from "@/components/modals/ReactModal"; -import { BinaryPathSetting } from "@/components/agent/BinaryPathSetting"; +import { BinaryPathSetting } from "./BinaryPathSetting"; import { Button } from "@/components/ui/button"; import { logError } from "@/logger"; import { validateExecutableFile } from "@/utils/detectBinary"; diff --git a/src/components/agent/BinaryPathSetting.tsx b/src/agentMode/backends/shared/BinaryPathSetting.tsx similarity index 100% rename from src/components/agent/BinaryPathSetting.tsx rename to src/agentMode/backends/shared/BinaryPathSetting.tsx diff --git a/src/agentMode/backends/shared/EnvOverridesSetting.tsx b/src/agentMode/backends/shared/EnvOverridesSetting.tsx new file mode 100644 index 00000000..94022460 --- /dev/null +++ b/src/agentMode/backends/shared/EnvOverridesSetting.tsx @@ -0,0 +1,174 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { ENV_VAR_NAME_RE } from "@/settings/model"; +import { debounce } from "@/utils/debounce"; +import { Plus, Trash2 } from "lucide-react"; +import React from "react"; + +interface Props { + value: Record | undefined; + onChange: (next: Record | undefined) => void; + /** Backend label woven into the description, e.g. "Claude" or "opencode". */ + backendDisplayName: string; + /** + * Two example var names shown in the description and as the first row's + * placeholder. Cosmetic only — no validation is performed against this list. + */ + hintExamples: [string, string]; +} + +interface Row { + id: string; + name: string; + value: string; +} + +let rowIdCounter = 0; +function nextRowId(): string { + rowIdCounter += 1; + return `row-${rowIdCounter}`; +} + +function recordToRows(record: Record | undefined): Row[] { + if (!record) return []; + return Object.entries(record).map(([name, value]) => ({ + id: nextRowId(), + name, + value, + })); +} + +function rowsToRecord(rows: Row[]): Record | undefined { + const out: Record = {}; + for (const row of rows) { + const name = row.name.trim(); + if (!name) continue; + out[name] = row.value; + } + return Object.keys(out).length > 0 ? out : undefined; +} + +const COMMIT_DEBOUNCE_MS = 400; + +/** + * Per-agent environment variable editor. Renders a labeled list of + * `name` / `value` rows plus an "Add variable" button. Values are stored + * verbatim — `~` is not expanded and whitespace is preserved. + * + * Persistence is debounced (`COMMIT_DEBOUNCE_MS`) to avoid rewriting + * settings on every keystroke; the pending commit is flushed on unmount so + * the last edit always lands. The parent's `value` is consulted only at + * mount — external reloads while the editor is open are uncommon. + * + * Validation is permissive: invalid names surface an inline warning but + * don't block typing. `sanitizeEnvOverrides` drops anything malformed at + * save time, so the persisted shape is always safe. + */ +export const EnvOverridesSetting: React.FC = ({ + value, + onChange, + backendDisplayName, + hintExamples, +}) => { + const [rows, setRows] = React.useState(() => recordToRows(value)); + + const onChangeRef = React.useRef(onChange); + React.useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + + const debouncedCommit = React.useMemo( + () => + debounce((next: Row[]): void => { + onChangeRef.current(rowsToRecord(next)); + }, COMMIT_DEBOUNCE_MS), + [] + ); + + React.useEffect(() => () => debouncedCommit.flush(), [debouncedCommit]); + + const commit = (next: Row[]): void => { + setRows(next); + debouncedCommit(next); + }; + + const updateRow = (id: string, patch: Partial>): void => { + commit(rows.map((r) => (r.id === id ? { ...r, ...patch } : r))); + }; + + const removeRow = (id: string): void => { + commit(rows.filter((r) => r.id !== id)); + }; + + const addRow = (): void => { + // Empty rows are visible locally but excluded from the committed record + // by `rowsToRecord` — so this purposely doesn't call `commit`. + setRows((prev) => [...prev, { id: nextRowId(), name: "", value: "" }]); + }; + + const [hintA, hintB] = hintExamples; + + return ( +
+
+
Environment variables
+
+ Set values for {backendDisplayName}, like {hintA} or {hintB}. +
+
+
+ {rows.map((row) => { + const trimmed = row.name.trim(); + const nameInvalid = trimmed.length > 0 && !ENV_VAR_NAME_RE.test(trimmed); + return ( +
+
+ updateRow(row.id, { name: e.target.value })} + /> + updateRow(row.id, { value: e.target.value })} + /> + +
+ {nameInvalid && ( +
+ Name must start with a letter or underscore and contain only letters, digits, and + underscores. +
+ )} +
+ ); + })} +
+ +
+
+
+ ); +}; diff --git a/src/agentMode/backends/shared/SimpleBackendSettingsPanel.tsx b/src/agentMode/backends/shared/SimpleBackendSettingsPanel.tsx index a7382938..c990da47 100644 --- a/src/agentMode/backends/shared/SimpleBackendSettingsPanel.tsx +++ b/src/agentMode/backends/shared/SimpleBackendSettingsPanel.tsx @@ -1,4 +1,4 @@ -import { BinaryPathSetting } from "@/components/agent/BinaryPathSetting"; +import { BinaryPathSetting } from "./BinaryPathSetting"; import { Button } from "@/components/ui/button"; import { SettingItem } from "@/components/ui/setting-item"; import { useSettingsValue } from "@/settings/model"; diff --git a/src/agentMode/backends/shared/simpleBinaryBackend.ts b/src/agentMode/backends/shared/simpleBinaryBackend.ts index 1af804eb..9d042480 100644 --- a/src/agentMode/backends/shared/simpleBinaryBackend.ts +++ b/src/agentMode/backends/shared/simpleBinaryBackend.ts @@ -9,11 +9,13 @@ import type { BackendDescriptor, BackendProcess, InstallState } from "@/agentMod * Build a spawn descriptor for a backend whose only configuration is a * user-provided binary path (no managed install, no extra args). Auth is * inherited from the user's environment / login state — no API key - * injection. + * injection. `envOverrides` is merged last so user values can override even + * the augmented `PATH`. */ export function buildSimpleSpawnDescriptor( binaryPath: string | undefined, - configErrorMessage: string + configErrorMessage: string, + envOverrides?: Record ): AcpSpawnDescriptor { if (!binaryPath) throw new Error(configErrorMessage); return { @@ -22,6 +24,7 @@ export function buildSimpleSpawnDescriptor( env: { ...process.env, PATH: augmentPathForNodeShebang(binaryPath, process.env.PATH), + ...(envOverrides ?? {}), }, }; } diff --git a/src/agentMode/sdk/ClaudeSdkBackendProcess.ts b/src/agentMode/sdk/ClaudeSdkBackendProcess.ts index 3b9755fb..581f0eb8 100644 --- a/src/agentMode/sdk/ClaudeSdkBackendProcess.ts +++ b/src/agentMode/sdk/ClaudeSdkBackendProcess.ts @@ -127,6 +127,11 @@ export interface ClaudeSdkBackendProcessOptions { * Empty string / undefined disables the append. */ getSkillCreationDirective?: () => string | undefined; + /** + * User-defined env vars merged onto `process.env` for the spawned `claude` + * CLI. Read per `prompt()` so settings edits apply on the next turn. + */ + getEnvOverrides?: () => Record | undefined; } /** @@ -282,6 +287,12 @@ export class ClaudeSdkBackendProcess implements BackendProcess { if (this.opts.getEnableThinking?.()) { options.thinking = { type: "adaptive" }; } + const envOverrides = this.opts.getEnvOverrides?.(); + if (envOverrides && Object.keys(envOverrides).length > 0) { + // Options.env replaces (not merges with) the child env, so include + // process.env to preserve PATH and friends. + options.env = { ...process.env, ...envOverrides }; + } logSdkOutbound( "prompt", diff --git a/src/settings/model.test.ts b/src/settings/model.test.ts index 11ee4386..92f73b53 100644 --- a/src/settings/model.test.ts +++ b/src/settings/model.test.ts @@ -5,7 +5,12 @@ import { DEFAULT_SETTINGS, SEND_SHORTCUT, } from "@/constants"; -import { sanitizeQaExclusions, sanitizeSettings, CopilotSettings } from "@/settings/model"; +import { + sanitizeEnvOverrides, + sanitizeQaExclusions, + sanitizeSettings, + CopilotSettings, +} from "@/settings/model"; import { getEffectiveUserPrompt, getSystemPrompt } from "@/system-prompts/systemPromptBuilder"; import * as systemPromptsState from "@/system-prompts/state"; import * as settingsModel from "@/settings/model"; @@ -262,6 +267,88 @@ describe("sanitizeSettings - agentMode shape migration", () => { }); }); +describe("sanitizeEnvOverrides", () => { + it("returns undefined for non-objects", () => { + expect(sanitizeEnvOverrides(undefined)).toBeUndefined(); + expect(sanitizeEnvOverrides(null)).toBeUndefined(); + expect(sanitizeEnvOverrides("foo")).toBeUndefined(); + expect(sanitizeEnvOverrides(42)).toBeUndefined(); + expect(sanitizeEnvOverrides([1, 2])).toBeUndefined(); + }); + + it("returns undefined when no valid entries remain", () => { + expect(sanitizeEnvOverrides({})).toBeUndefined(); + expect(sanitizeEnvOverrides({ "": "v", "1FOO": "v", "BAR=BAZ": "v" })).toBeUndefined(); + }); + + it("keeps valid POSIX identifiers and string values", () => { + expect( + sanitizeEnvOverrides({ + CLAUDE_CONFIG_DIR: "/tmp/claude", + _PRIVATE: "x", + myVar2: "y", + }) + ).toEqual({ + CLAUDE_CONFIG_DIR: "/tmp/claude", + _PRIVATE: "x", + myVar2: "y", + }); + }); + + it("drops keys with leading digits, equals signs, whitespace, or invalid characters", () => { + expect( + sanitizeEnvOverrides({ + "1FOO": "v", + "FOO BAR": "v", + "FOO=BAR": "v", + "FOO-BAR": "v", + VALID: "v", + }) + ).toEqual({ VALID: "v" }); + }); + + it("drops entries whose value isn't a string or contains control chars", () => { + expect( + sanitizeEnvOverrides({ + OK: "fine", + NUM: 42, + NULLED: null, + UNDEF: undefined, + TABS: "ok\twith\ttabs", // tab is a control char — drop + NEWLINE: "ok\nnewline", // drop + }) + ).toEqual({ OK: "fine" }); + }); + + it("caps at 64 entries to bound persisted size", () => { + const big: Record = {}; + for (let i = 0; i < 100; i++) big[`VAR_${i}`] = String(i); + const sanitized = sanitizeEnvOverrides(big); + expect(sanitized && Object.keys(sanitized).length).toBe(64); + }); + + it("round-trips through sanitizeSettings on the Claude backend slice", () => { + const settings = { + ...DEFAULT_SETTINGS, + agentMode: { + enabled: true, + byok: {}, + mcpServers: [], + activeBackend: "claude", + backends: { + claude: { envOverrides: { CLAUDE_CONFIG_DIR: "/x", "BAD KEY": "y" } }, + }, + }, + } as unknown as CopilotSettings; + + const sanitized = sanitizeSettings(settings); + + expect(sanitized.agentMode.backends.claude?.envOverrides).toEqual({ + CLAUDE_CONFIG_DIR: "/x", + }); + }); +}); + describe("sanitizeSettings - legacy Miyo settings cleanup", () => { it("migrates legacy Miyo settings and strips obsolete remote vault path state", () => { const legacySettings = { diff --git a/src/settings/model.ts b/src/settings/model.ts index 9a958b4d..9c9bd1a4 100644 --- a/src/settings/model.ts +++ b/src/settings/model.ts @@ -297,6 +297,13 @@ export interface ClaudeBackendSettings { * surfaces reasoning chunks. Off by default (matches SDK default). */ enableThinking?: boolean; + /** + * User-defined environment variables merged on top of `process.env` when + * spawning the `claude` CLI. Used to redirect config dirs + * (`CLAUDE_CONFIG_DIR`), set proxies, or toggle vendor flags without + * polluting the parent shell environment. + */ + envOverrides?: Record; } /** Settings slice owned by the Codex backend. */ @@ -307,6 +314,8 @@ export interface CodexBackendSettings { defaultModel?: ModelSelection | null; /** Sparse user overrides; see `ClaudeBackendSettings.modelEnabledOverrides`. */ modelEnabledOverrides?: Record; + /** See `ClaudeBackendSettings.envOverrides`. Applied to the spawned `codex-acp` subprocess. */ + envOverrides?: Record; } /** Settings slice owned by the OpenCode backend. */ @@ -335,6 +344,8 @@ export interface OpencodeBackendSettings { probeSessionId?: string; /** Sparse user overrides; see `ClaudeBackendSettings.modelEnabledOverrides`. */ modelEnabledOverrides?: Record; + /** See `ClaudeBackendSettings.envOverrides`. Applied to the spawned `opencode` subprocess. */ + envOverrides?: Record; } export const settingsStore = createStore(); @@ -977,6 +988,34 @@ function sanitizeDefaultModel(raw: unknown): ModelSelection | undefined { return { baseModelId, effort }; } +/** + * Strict env-var key check: POSIX-style identifier. Rejects empty strings, + * leading digits, `=`, whitespace, dots, hyphens, and control chars. Shared + * with the UI editor (`EnvOverridesSetting`) so live validation matches what + * the sanitizer accepts. + */ +export const ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; + +/** + * Sanitize a user-supplied env-var override record. Drops entries whose key + * fails POSIX-identifier validation or whose value isn't a string. Caps the + * record at 64 entries to bound the persisted size. Returns `undefined` + * when the record is empty so the persisted settings shape stays clean. + */ +export function sanitizeEnvOverrides(raw: unknown): Record | undefined { + if (!raw || typeof raw !== "object") return undefined; + const out: Record = {}; + for (const [k, v] of Object.entries(raw as Record)) { + if (typeof k !== "string") continue; + if (!ENV_VAR_NAME_RE.test(k)) continue; + if (typeof v !== "string") continue; + if (CONTROL_CHAR_RE.test(v)) continue; + out[k] = v; + if (Object.keys(out).length >= 64) break; + } + return Object.keys(out).length > 0 ? out : undefined; +} + function sanitizeClaudeBackendSettings(raw: unknown): ClaudeBackendSettings { if (!raw || typeof raw !== "object") return {}; const r = raw as Record; @@ -984,6 +1023,7 @@ function sanitizeClaudeBackendSettings(raw: unknown): ClaudeBackendSettings { defaultModel: sanitizeDefaultModel(r.defaultModel), modelEnabledOverrides: sanitizeModelEnabledOverrides(r.modelEnabledOverrides), enableThinking: typeof r.enableThinking === "boolean" ? r.enableThinking : undefined, + envOverrides: sanitizeEnvOverrides(r.envOverrides), }; } @@ -994,6 +1034,7 @@ function sanitizeCodexBackendSettings(raw: unknown): CodexBackendSettings { binaryPath: nonEmptyString(r.binaryPath), defaultModel: sanitizeDefaultModel(r.defaultModel), modelEnabledOverrides: sanitizeModelEnabledOverrides(r.modelEnabledOverrides), + envOverrides: sanitizeEnvOverrides(r.envOverrides), }; } @@ -1016,6 +1057,7 @@ function sanitizeOpencodeBackendSettings(raw: unknown): OpencodeBackendSettings defaultModel: sanitizeDefaultModel(r.defaultModel), probeSessionId: nonEmptyString(r.probeSessionId), modelEnabledOverrides: sanitizeModelEnabledOverrides(r.modelEnabledOverrides), + envOverrides: sanitizeEnvOverrides(r.envOverrides), }; }