Skip to content

Commit 29671c1

Browse files
authored
fix: token substitution in OPENCODE_CONFIG_CONTENT (#13384)
1 parent f66624f commit 29671c1

3 files changed

Lines changed: 84 additions & 2 deletions

File tree

packages/opencode/src/config/config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,14 @@ export namespace Config {
175175
}
176176

177177
// Inline config content overrides all non-managed config sources.
178+
// Route through load() to enable {env:} and {file:} token substitution.
179+
// Use a path within Instance.directory so relative {file:} paths resolve correctly.
180+
// The filename "OPENCODE_CONFIG_CONTENT" appears in error messages for clarity.
178181
if (Flag.OPENCODE_CONFIG_CONTENT) {
179-
result = mergeConfigConcatArrays(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
182+
result = mergeConfigConcatArrays(
183+
result,
184+
await load(Flag.OPENCODE_CONFIG_CONTENT, path.join(Instance.directory, "OPENCODE_CONFIG_CONTENT")),
185+
)
180186
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
181187
}
182188

packages/opencode/src/flag/flag.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export namespace Flag {
88
export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"]
99
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
1010
export declare const OPENCODE_CONFIG_DIR: string | undefined
11-
export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"]
11+
export declare const OPENCODE_CONFIG_CONTENT: string | undefined
1212
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
1313
export const OPENCODE_DISABLE_PRUNE = truthy("OPENCODE_DISABLE_PRUNE")
1414
export const OPENCODE_DISABLE_TERMINAL_TITLE = truthy("OPENCODE_DISABLE_TERMINAL_TITLE")
@@ -94,3 +94,14 @@ Object.defineProperty(Flag, "OPENCODE_CLIENT", {
9494
enumerable: true,
9595
configurable: false,
9696
})
97+
98+
// Dynamic getter for OPENCODE_CONFIG_CONTENT
99+
// This must be evaluated at access time, not module load time,
100+
// because external tooling may set this env var at runtime
101+
Object.defineProperty(Flag, "OPENCODE_CONFIG_CONTENT", {
102+
get() {
103+
return process.env["OPENCODE_CONFIG_CONTENT"]
104+
},
105+
enumerable: true,
106+
configurable: false,
107+
})

packages/opencode/test/config/config.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,3 +1800,68 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
18001800
}
18011801
})
18021802
})
1803+
1804+
// OPENCODE_CONFIG_CONTENT should support {env:} and {file:} token substitution
1805+
// just like file-based config sources do.
1806+
describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
1807+
test("substitutes {env:} tokens in OPENCODE_CONFIG_CONTENT", async () => {
1808+
const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"]
1809+
const originalTestVar = process.env["TEST_CONFIG_VAR"]
1810+
process.env["TEST_CONFIG_VAR"] = "test_api_key_12345"
1811+
process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({
1812+
$schema: "https://opencode.ai/config.json",
1813+
theme: "{env:TEST_CONFIG_VAR}",
1814+
})
1815+
1816+
try {
1817+
await using tmp = await tmpdir()
1818+
await Instance.provide({
1819+
directory: tmp.path,
1820+
fn: async () => {
1821+
const config = await Config.get()
1822+
expect(config.theme).toBe("test_api_key_12345")
1823+
},
1824+
})
1825+
} finally {
1826+
if (originalEnv !== undefined) {
1827+
process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv
1828+
} else {
1829+
delete process.env["OPENCODE_CONFIG_CONTENT"]
1830+
}
1831+
if (originalTestVar !== undefined) {
1832+
process.env["TEST_CONFIG_VAR"] = originalTestVar
1833+
} else {
1834+
delete process.env["TEST_CONFIG_VAR"]
1835+
}
1836+
}
1837+
})
1838+
1839+
test("substitutes {file:} tokens in OPENCODE_CONFIG_CONTENT", async () => {
1840+
const originalEnv = process.env["OPENCODE_CONFIG_CONTENT"]
1841+
1842+
try {
1843+
await using tmp = await tmpdir({
1844+
init: async (dir) => {
1845+
await Bun.write(path.join(dir, "api_key.txt"), "secret_key_from_file")
1846+
process.env["OPENCODE_CONFIG_CONTENT"] = JSON.stringify({
1847+
$schema: "https://opencode.ai/config.json",
1848+
theme: "{file:./api_key.txt}",
1849+
})
1850+
},
1851+
})
1852+
await Instance.provide({
1853+
directory: tmp.path,
1854+
fn: async () => {
1855+
const config = await Config.get()
1856+
expect(config.theme).toBe("secret_key_from_file")
1857+
},
1858+
})
1859+
} finally {
1860+
if (originalEnv !== undefined) {
1861+
process.env["OPENCODE_CONFIG_CONTENT"] = originalEnv
1862+
} else {
1863+
delete process.env["OPENCODE_CONFIG_CONTENT"]
1864+
}
1865+
}
1866+
})
1867+
})

0 commit comments

Comments
 (0)