From f112a73c065c546aa6b8e6a94f6edee8bb75d6b9 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:05:41 +0000 Subject: [PATCH] fix(core): load config across git boundaries (#36577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 Co-authored-by: opencode-agent[bot] --- packages/core/src/config.ts | 1 - packages/core/test/config/config.test.ts | 5 +++- packages/opencode/src/config/config.ts | 4 +-- packages/opencode/src/config/paths.ts | 14 ++++++--- packages/opencode/test/config/config.test.ts | 31 -------------------- packages/web/src/content/docs/config.mdx | 2 +- 6 files changed, 17 insertions(+), 40 deletions(-) diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index e12f7848d6..55a1641f1c 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -200,7 +200,6 @@ const layer = Layer.effect( .up({ targets: [".opencode", ".claude", ".agents", ...names.toReversed()], start: location.directory, - stop: location.project.directory, }) .pipe(Effect.orDie) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index e6c0a6b915..94c84c3b02 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -876,7 +876,7 @@ describe("Config", () => { ), ) - it.live("loads global, ancestor, and .opencode configuration up to the project boundary", () => + it.live("loads global and ancestor configuration across the project boundary", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), @@ -936,6 +936,7 @@ describe("Config", () => { ]) expect(documents.map((document) => document.info.$schema)).toEqual([ "global", + "outside", "root", "parent", "directory", @@ -951,6 +952,8 @@ describe("Config", () => { AbsolutePath.make(path.join(root, ".agents")), "global", AbsolutePath.make(global), + "outside", + AbsolutePath.make(path.join(tmp.path, "opencode.json")), "root", AbsolutePath.make(path.join(root, "opencode.json")), "parent", diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index e3823056dc..86238f1a84 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -404,7 +404,7 @@ const layer = Layer.effect( } if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { - for (const file of yield* ConfigPaths.files("opencode", ctx.directory).pipe(Effect.orDie)) { + for (const file of yield* ConfigPaths.files("opencode", ctx.directory, ctx.worktree).pipe(Effect.orDie)) { yield* merge(file, yield* loadFile(file, authEnv), "local") } } @@ -413,7 +413,7 @@ const layer = Layer.effect( result.mode = result.mode || {} result.plugin = result.plugin || [] - const directories = yield* ConfigPaths.directories(ctx.directory) + const directories = yield* ConfigPaths.directories(ctx.directory, ctx.worktree) if (Flag.OPENCODE_CONFIG_DIR) { yield* Effect.logDebug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR }) diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index f9e91693aa..11d90f1292 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -7,23 +7,29 @@ import { unique } from "remeda" import * as Effect from "effect/Effect" import { FSUtil } from "@opencode-ai/core/fs-util" -export const files = Effect.fn("ConfigPaths.projectFiles")(function* (name: string, directory: string) { +export const files = Effect.fn("ConfigPaths.projectFiles")(function* ( + name: string, + directory: string, + worktree?: string, +) { const afs = yield* FSUtil.Service return (yield* afs.up({ targets: [`${name}.jsonc`, `${name}.json`], start: directory, + stop: worktree, })).toReversed() }) -export const directories = Effect.fn("ConfigPaths.directories")(function* (directory: string) { +export const directories = Effect.fn("ConfigPaths.directories")(function* (directory: string, worktree?: string) { const afs = yield* FSUtil.Service return unique([ Global.Path.config, ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG - ? (yield* afs.up({ + ? yield* afs.up({ targets: [".opencode"], start: directory, - })).toReversed() + stop: worktree, + }) : []), ...(yield* afs.up({ targets: [".opencode"], diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index f29c7b57d4..ac408ea812 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -999,37 +999,6 @@ it.instance("resolves scoped npm plugins in config", () => }), ) -it.effect("loads shared config above a child git repository", () => - Effect.gen(function* () { - const root = yield* tmpdirScoped() - const global = yield* tmpdirScoped() - const workspace = path.join(root, "workspace") - const project = path.join(workspace, "project") - - yield* writeConfigEffect(path.join(workspace, ".opencode"), { - model: "shared/model", - username: "shared-user", - }) - yield* writeConfigEffect(path.join(project, ".opencode"), { username: "project-user" }) - yield* Effect.promise(async () => { - const child = Bun.spawn(["git", "init"], { cwd: project, stdout: "ignore", stderr: "ignore" }) - await child.exited - }) - - yield* withGlobalConfigDir( - global, - withInstanceDir( - project, - Effect.gen(function* () { - const config = yield* Config.use.get() - expect(config.model).toBe("shared/model") - expect(config.username).toBe("project-user") - }), - ), - ) - }), -) - it.effect("merges plugin arrays from global and local configs", () => withConfigTree( { diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index fe44817280..fbf41d4fe9 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -116,7 +116,7 @@ For project-specific TUI settings, add `tui.json` alongside it. Place project specific config in the root of your project. ::: -When OpenCode starts up, it looks for config files in the current directory and every ancestor directory. Ancestor configs are loaded first, so configs closer to the current directory can override them. This traversal continues across Git repository boundaries. +When OpenCode starts up, it first looks for a config file in the current directory, then traverses up to the nearest Git directory. This is also safe to be checked into Git and uses the same schema as the global one.