diff --git a/packages/opencode/src/config/variable.ts b/packages/opencode/src/config/variable.ts index 52c449538fa1..af3d1ecd0f3f 100644 --- a/packages/opencode/src/config/variable.ts +++ b/packages/opencode/src/config/variable.ts @@ -34,7 +34,8 @@ function dir(input: ParseSource) { export async function substitute(input: SubstituteInput) { const missing = input.missing ?? "error" let text = input.text.replace(/\{env:([^}]+)\}/g, (_, varName) => { - return (input.env?.[varName] ?? process.env[varName]) || "" + const value = (input.env?.[varName] ?? process.env[varName]) || "" + return JSON.stringify(value).slice(1, -1) }) const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g)) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 02ace5366880..479c2d6f6aba 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -515,6 +515,28 @@ it.instance("handles environment variable substitution", () => ), ) +it.instance("escapes environment variable substitution inside JSON keys", () => + withProcessEnv( + "TEST_WIN_PATH", + "C:\\Users\\me\\AppData\\Roaming\\MyApp\\config", + Effect.gen(function* () { + const test = yield* TestInstance + yield* writeConfigEffect(test.directory, { + $schema: "https://opencode.ai/config.json", + permission: { + external_directory: { + "{env:TEST_WIN_PATH}/**": "allow", + }, + }, + }) + const config = yield* Config.use.get() + expect(config.permission?.external_directory).toEqual({ + "C:\\Users\\me\\AppData\\Roaming\\MyApp\\config/**": "allow", + }) + }), + ), +) + it.instance("preserves env variables when adding $schema to config", () => withProcessEnv( "PRESERVE_VAR",