Browse Source

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.
Aiden Cline 1 month ago
parent
commit
052f887a9a

+ 4 - 1
packages/opencode/src/config/config.ts

@@ -1115,6 +1115,7 @@ export namespace Config {
   }
   }
 
 
   async function load(text: string, configFilepath: string) {
   async function load(text: string, configFilepath: string) {
+    const original = text
     text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
     text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
       return process.env[varName] || ""
       return process.env[varName] || ""
     })
     })
@@ -1184,7 +1185,9 @@ export namespace Config {
     if (parsed.success) {
     if (parsed.success) {
       if (!parsed.data.$schema) {
       if (!parsed.data.$schema) {
         parsed.data.$schema = "https://opencode.ai/config.json"
         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
       const data = parsed.data
       if (data.plugin) {
       if (data.plugin) {

+ 38 - 0
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 () => {
 test("handles file inclusion substitution", async () => {
   await using tmp = await tmpdir({
   await using tmp = await tmpdir({
     init: async (dir) => {
     init: async (dir) => {