From 052f887a9a7aaf79d9f1a560f9b686d59faa8348 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 17 Jan 2026 20:59:42 -0600 Subject: [PATCH] core: prevent env variables in config from being replaced with actual values When opencode.json was missing a $schema, the config loader would add it and write the file back - but with env variables like {env:API_KEY} replaced with their actual secret values. This made it impossible to safely commit opencode.json to version control. Now the original config text is preserved when adding $schema, keeping variable placeholders intact. --- packages/opencode/src/config/config.ts | 5 ++- packages/opencode/test/config/config.test.ts | 38 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 1574c644d..5a2e086bf 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1115,6 +1115,7 @@ export namespace Config { } async function load(text: string, configFilepath: string) { + const original = text text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => { return process.env[varName] || "" }) @@ -1184,7 +1185,9 @@ export namespace Config { if (parsed.success) { if (!parsed.data.$schema) { parsed.data.$schema = "https://opencode.ai/config.json" - await Bun.write(configFilepath, JSON.stringify(parsed.data, null, 2)).catch(() => {}) + // Write the $schema to the original text to preserve variables like {env:VAR} + const updated = original.replace(/^\s*\{/, '{\n "$schema": "https://opencode.ai/config.json",') + await Bun.write(configFilepath, updated).catch(() => {}) } const data = parsed.data if (data.plugin) { diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 86cadca5d..0463d29d7 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -127,6 +127,44 @@ test("handles environment variable substitution", async () => { } }) +test("preserves env variables when adding $schema to config", async () => { + const originalEnv = process.env["PRESERVE_VAR"] + process.env["PRESERVE_VAR"] = "secret_value" + + try { + await using tmp = await tmpdir({ + init: async (dir) => { + // Config without $schema - should trigger auto-add + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + theme: "{env:PRESERVE_VAR}", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.theme).toBe("secret_value") + + // Read the file to verify the env variable was preserved + const content = await Bun.file(path.join(tmp.path, "opencode.json")).text() + expect(content).toContain("{env:PRESERVE_VAR}") + expect(content).not.toContain("secret_value") + expect(content).toContain("$schema") + }, + }) + } finally { + if (originalEnv !== undefined) { + process.env["PRESERVE_VAR"] = originalEnv + } else { + delete process.env["PRESERVE_VAR"] + } + } +}) + test("handles file inclusion substitution", async () => { await using tmp = await tmpdir({ init: async (dir) => {