Skip to content
12 changes: 11 additions & 1 deletion packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,18 @@ export namespace Config {
}

// Inline config content overrides all non-managed config sources.
// 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 (Flag.OPENCODE_CONFIG_CONTENT) {
result = merge(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
// Parse and ensure $schema is present to prevent Bun.write() from creating a file
const parsed = JSON.parse(Flag.OPENCODE_CONFIG_CONTENT)
if (!parsed.$schema) parsed.$schema = "https://opencode.ai/config.json"

result = merge(
result,
await load(JSON.stringify(parsed), path.join(Instance.directory, "OPENCODE_CONFIG_CONTENT")),
)
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
}

Expand Down
13 changes: 12 additions & 1 deletion packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -95,3 +95,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,
})
65 changes: 65 additions & 0 deletions packages/opencode/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
})
})