Skip to content

bug(config): outer .opencode/ directories override inner config #22

@Astro-Han

Description

@Astro-Han

Symptom

When multiple .opencode/ directories exist in a project tree (e.g. <worktree>/.opencode/ and <workspace>/.opencode/), the outer directory's opencode.json / pawwork.json overrides the inner one at merge time. The expected invariant across the rest of the config system is "innermost wins".

Location

  • packages/opencode/src/config/config.ts:1425-1442loadAll iterates directories with last-wins merge
  • packages/opencode/src/config/paths.tsConfigPaths.directories() concatenates Filesystem.up(...) outputs
  • packages/opencode/src/util/filesystem.ts:264-277Filesystem.up yields from start (innermost) → parents

Trace

// directories() returns:
[
  Global.Path.config,                            // global (correctly merged first)
  ...Array.fromAsync(Filesystem.up({             // PROJECT: start → root
    start: ctx.directory,                        // innermost first in the array
    stop: ctx.worktree,                          // outermost last in the array
  })),
  ...Array.fromAsync(Filesystem.up({             // HOME (single)
    start: Global.Path.home, stop: Global.Path.home,
  })),
  ...(Flag.OPENCODE_CONFIG_DIR ? [...] : []),
]

Merge loop iterates this list in order; merge() is last-wins. So within the project segment:

  • Innermost .opencode/ is merged first → overwritten by each outer dir
  • Outermost .opencode/ is the final write → wins

This is the opposite of the "innermost wins" convention used by the project-root pawwork.json/opencode.json cascade (fixed in PR #18 via rootFirst: true in findUp).

Why not fixed in PR #18

PR #18 only added pawwork.json / pawwork.jsonc to the filename candidate array at line 1435. The directory iteration order was not modified. The bug is pre-existing upstream behavior and would affect anyone with multiple .opencode/ directories in their tree (common when working inside a monorepo subproject or a git worktree).

Suggested fix

Either:

  1. Reverse the project segment order in ConfigPaths.directories(): Array.fromAsync(up(...)).then(r => r.reverse()) so outermost loads first, innermost last — then merge last-wins gives innermost victory.
  2. Add rootFirst to Filesystem.up (mirror findUp API) and flip the project walk to start at worktree-root and end at ctx.directory.

Option (1) is a one-line caller fix. Option (2) is a small API extension that could benefit other callers.

Also verify directories() global segment (Global.Path.config) should remain at index 0 — it should be overridden by project dirs, so global-first + last-wins is correct there.

Acceptance

  • With <workspace>/.opencode/pawwork.json setting model: A and <workspace>/subpkg/.opencode/pawwork.json setting model: B, running from subpkg/ should load model: B.
  • Regression test fixture in packages/opencode/test/config/ if tests cover this area.

Scope

Not part of PR #18 (brand/scope cleanup). File separately because the fix touches config cascade semantics and deserves its own review + regression coverage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium prioritybugSomething isn't workingupstreamTracked upstream or vendor behavior

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions