From e06962aa091927571dd67a010c5130952bb4fcb1 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 12 Feb 2026 12:03:03 -0500 Subject: [PATCH 1/2] fix: enable token substitution in OPENCODE_CONFIG_CONTENT Route OPENCODE_CONFIG_CONTENT through load() to enable {env:} and {file:} token substitution. Uses the env var name as the path for clearer error messages instead of a generic placeholder. Fixes #13219 --- packages/opencode/src/config/config.ts | 10 ++- packages/opencode/test/config/config.test.ts | 65 ++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 8f0f583ea3d6..4b5bcef59202 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -175,8 +175,14 @@ export namespace Config { } // Inline config content overrides all non-managed config sources. - if (Flag.OPENCODE_CONFIG_CONTENT) { - result = mergeConfigConcatArrays(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT)) + // Route through load() to enable {env:} and {file:} token substitution. + // Use a path within Instance.directory so relative {file:} paths resolve correctly. + // The filename "OPENCODE_CONFIG_CONTENT" appears in error messages for clarity. + if (process.env.OPENCODE_CONFIG_CONTENT) { + result = mergeConfigConcatArrays( + result, + await load(process.env.OPENCODE_CONFIG_CONTENT, path.join(Instance.directory, "OPENCODE_CONFIG_CONTENT")), + ) log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") } diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 91b87f6498c4..331e05d5a7b3 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1800,3 +1800,68 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => { } }) }) + +// OPENCODE_CONFIG_CONTENT should support {env:} and {file:} token substitution +// just like file-based config sources do. +describe("OPENCODE_CONFIG_CONTENT token substitution", () => { + test("substitutes {env:} tokens in OPENCODE_CONFIG_CONTENT", async () => { + const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] + const originalTestVar = process.env["TEST_CONFIG_VAR"] + process.env["TEST_CONFIG_VAR"] = "test_api_key_12345" + process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ + $schema: "https://opencode.ai/config.json", + theme: "{env:TEST_CONFIG_VAR}", + }) + + try { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.theme).toBe("test_api_key_12345") + }, + }) + } finally { + if (originalEnv !== undefined) { + process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv + } else { + delete process.env["OPENCODE_CONFIG_CONTENT"] + } + if (originalTestVar !== undefined) { + process.env["TEST_CONFIG_VAR"] = originalTestVar + } else { + delete process.env["TEST_CONFIG_VAR"] + } + } + }) + + test("substitutes {file:} tokens in OPENCODE_CONFIG_CONTENT", async () => { + const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"] + + try { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write(path.join(dir, "api_key.txt"), "secret_key_from_file") + process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({ + $schema: "https://opencode.ai/config.json", + theme: "{file:./api_key.txt}", + }) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.theme).toBe("secret_key_from_file") + }, + }) + } finally { + if (originalEnv !== undefined) { + process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv + } else { + delete process.env["OPENCODE_CONFIG_CONTENT"] + } + } + }) +}) From dff8bbf2b5148ff2a542e53d908c77022d9ca5e4 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 12 Feb 2026 17:17:41 -0500 Subject: [PATCH 2/2] refactor: make OPENCODE_CONFIG_CONTENT a dynamic Flag getter Converts OPENCODE_CONFIG_CONTENT to a dynamic getter on the Flag object, matching the pattern used for OPENCODE_CONFIG_DIR and OPENCODE_CLIENT. This ensures env var changes are reflected at access time. --- packages/opencode/src/config/config.ts | 4 ++-- packages/opencode/src/flag/flag.ts | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 4b5bcef59202..f4d7a840fea7 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -178,10 +178,10 @@ export namespace Config { // Route through load() to enable {env:} and {file:} token substitution. // Use a path within Instance.directory so relative {file:} paths resolve correctly. // The filename "OPENCODE_CONFIG_CONTENT" appears in error messages for clarity. - if (process.env.OPENCODE_CONFIG_CONTENT) { + if (Flag.OPENCODE_CONFIG_CONTENT) { result = mergeConfigConcatArrays( result, - await load(process.env.OPENCODE_CONFIG_CONTENT, path.join(Instance.directory, "OPENCODE_CONFIG_CONTENT")), + await load(Flag.OPENCODE_CONFIG_CONTENT, path.join(Instance.directory, "OPENCODE_CONFIG_CONTENT")), ) log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") } diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index b11058b34058..557f11f49227 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -8,7 +8,7 @@ export namespace Flag { export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"] export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"] export declare const OPENCODE_CONFIG_DIR: string | undefined - export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"] + export declare const OPENCODE_CONFIG_CONTENT: string | undefined export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE") export const OPENCODE_DISABLE_PRUNE = truthy("OPENCODE_DISABLE_PRUNE") export const OPENCODE_DISABLE_TERMINAL_TITLE = truthy("OPENCODE_DISABLE_TERMINAL_TITLE") @@ -91,3 +91,14 @@ Object.defineProperty(Flag, "OPENCODE_CLIENT", { enumerable: true, configurable: false, }) + +// Dynamic getter for OPENCODE_CONFIG_CONTENT +// This must be evaluated at access time, not module load time, +// because external tooling may set this env var at runtime +Object.defineProperty(Flag, "OPENCODE_CONFIG_CONTENT", { + get() { + return process.env["OPENCODE_CONFIG_CONTENT"] + }, + enumerable: true, + configurable: false, +})