From 81ad426d03c8d99183ace0c9aee3a5cab650612a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Mart=C3=ADn?= Date: Mon, 6 Apr 2026 12:54:09 +0200 Subject: [PATCH] fix(config): load .env from config dir for {env:VAR} substitution substitute() only checked process.env for {env:VAR} tokens. If opencode is installed globally it never sees the project .env. Now reads .env from the config file directory as fallback. Shell variables still take priority over .env values. --- packages/opencode/src/config/paths.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index 82ccf3945fcd..c8a3458d98c2 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -4,6 +4,7 @@ import z from "zod" import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser" import { NamedError } from "@opencode-ai/util/error" import { Filesystem } from "@/util/filesystem" +import { Env } from "@/env" import { Flag } from "@/flag/flag" import { Global } from "@/global" @@ -74,10 +75,20 @@ export namespace ConfigPaths { return typeof input === "string" ? path.dirname(input) : input.dir } - /** Apply {env:VAR} and {file:path} substitutions to config text. */ + /** Apply {env:VAR} and {file:path} substitutions to config text. {env:VAR} resolves from shell environment first, then .env in the config directory. */ async function substitute(text: string, input: ParseSource, missing: "error" | "empty" = "error") { + const localEnv: Record = {} + const dotenvContent = await Filesystem.readText(path.join(dir(input), ".env")).catch(() => "") + for (const line of dotenvContent.split("\n")) { + const match = line.match(/^([^#=\s][^=]*)=(.*)$/) + if (match) { + const [, key, val] = match + localEnv[key.trim()] = val.trim().replace(/^["']|["']$/g, "") + } + } + text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => { - return process.env[varName] || "" + return Env.get(varName) ?? localEnv[varName] ?? "" }) const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g))