Skip to content

fix(config): keep OPENCODE_CONFIG_DIR additive for the global config layer - #62315

Open
abdo342003 wants to merge 1 commit into
anomalyco:devfrom
abdo342003:fix/config-dir-additive
Open

abdo342003 wants to merge 1 commit into
anomalyco:devfrom
abdo342003:fix/config-dir-additive

Conversation

@abdo342003

@abdo342003 abdo342003 commented Nov 6, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #32825
Related: #28658 (the V1 report of the same bug, closed in the V1 sweep)

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Setting OPENCODE_CONFIG_DIR currently replaces the XDG global config directory instead of adding to it, so
once the variable is set ~/.config/opencode stops contributing entirely: its opencode.json(c) is never
merged, AGENTS.md is never read, and the directory entries that feed commands, skills and plugins only
come from the override. #47468 fixed the AGENTS.md half on V1 and asked for a V2 port — this is that port,
extended to every place V2 reads the directory.

Global gains configDirs (override first, Global.Path.config behind it), produced by
Global.configDirectory(value), which only treats a directory additively when it actually is
OPENCODE_CONFIG_DIR. A directory that isn't the env var — what tests and embedded hosts pass — keeps the
old replace behavior, so explicit configuration is unchanged.

From there: ConfigDiscovery exposes the list as Sources.globalDirs, config loading merges it lowest
priority first so the override still wins conflicts, and the watch plan watches every global directory. The
instruction plugin resolves the global AGENTS.md first-match-wins across the list (both files stay
watched) while relative instructions patterns scan all of them. The TUI theme source and opencode plugin list iterate the same list. Writes still target the primary directory.

How did you verify your code works?

Unit tests for Global.configDirectory (override / equal-to-XDG / unrelated directory / unset) and for
discover (globalDirs ordering, global: false → empty). An integration test in config.test.ts checks
that the override document outranks the XDG one, that the XDG document still applies once the override's is
removed, and that both directories get directory entries and directory watches. An instruction test checks
that the XDG AGENTS.md loads when the override has none, and that the override's file wins once it exists.

bun run typecheck and bun run lint are clean, and the util/core/server/tui/cli suites pass — the only
failures on this machine (worktree PATH, GitLab OAuth needing port 8080, upsell art, stats palette)
reproduce on a clean dev checkout too.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #46757, #58141

Public API: OPENCODE_CONFIG_DIR now adds to ~/.config/opencode instead of replacing it (behaviour change; setups that used it to isolate config will now also load the XDG layer)

…layer

OPENCODE_CONFIG_DIR replaced the XDG global config directory outright, so
setting it dropped everything behind it: the global opencode.json(c) never
merged, ~/.config/opencode/AGENTS.md was never read, and the directory
entries feeding commands, skills and plugins only came from the override
(anomalyco#28658, anomalyco#32825). anomalyco#47468 asked for this to be ported to V2.

Global now carries configDirs (override first, XDG default behind it),
produced by configDirectory() which only treats a directory additively
when it actually is OPENCODE_CONFIG_DIR -- explicit directories that are
not the env var (tests, embedded hosts) keep replacing as before.
Discovery exposes the list as Sources.globalDirs, config loading merges
it lowest-priority-first so the override wins conflicts, the watch plan
watches every global directory, and the instruction plugin resolves the
global AGENTS.md first-match-wins across the list while relative
instructions patterns scan all of them. The TUI theme source and
plugin list iterate the same list; writes still target the primary
directory.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes the reported problem. I ran serve from source in each checkout with a throwaway HOME. With OPENCODE_CONFIG_DIR set, the base lists only the override directory as a config source. With this change, the XDG opencode.json and directory are listed too, and the override loads after them, so it wins conflicts. The util and core tests for the change pass.

Public API: OPENCODE_CONFIG_DIR changes from replacing ~/.config/opencode to adding to it, which matches the docs. A user who set it to isolate their config from ~/.config/opencode will now also get the XDG layer: settings, commands, skills, plugins and the fallback AGENTS.md. Internal additions are Global.Interface.configDirs, Global.configDirectory(), ConfigDiscovery.Sources.globalDirs and ConfigDiscovery.globalDirectories(). createThemeSource now takes a list of directories. Writes (plugin add/remove, mcp add --global, cli.json) still target the override directory.

Concerns:

  • TUI plugins are still missed. packages/tui/src/app.tsx:231 still calls localPluginDirectories(process.cwd(), global.config). With the override set, TUI plugins in ~/.config/opencode/plugins are still not loaded, while opencode plugin list now lists them. It should iterate global.configDirs like plugin/list.ts does.
  • How additive mode is chosen. It depends on matching the env var, and routes.ts gains a new env fallback (see the inline comments).

The override still ranks below project .opencode directories, while the docs say it is loaded after them. This was already the case on the base and is not changed here.

Related: #46757, #58141

* hosts) replaces it exactly as before.
*/
export const configDirectory = (value: string | undefined): Partial<Interface> =>
value && value === process.env.OPENCODE_CONFIG_DIR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additive mode is chosen by comparing the value with process.env.OPENCODE_CONFIG_DIR. So Global.configDirectory(x) behaves differently depending on an ambient env var: an embedding host that passes an explicit directory that happens to equal the env var gets additive mode, and one that passes a different directory doesn't. Nothing normalizes the paths either. It would be more predictable to make the mode explicit, e.g. configDirectory({ override }) or a separate extraConfig input that the CLI and server pass, and keep util free of env sniffing.

FileSystemSearch.node.replace(FileSystemSearch.configured({ fff: options.fs?.fff })),
Global.node.replace(Global.layerWith(options.config?.directory ? { config: options.config.directory } : {})),
Global.node.replace(
Global.layerWith(Global.configDirectory(options.config?.directory ?? process.env.OPENCODE_CONFIG_DIR)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a new fallback to process.env.OPENCODE_CONFIG_DIR when the caller doesn't set config.directory. Before, makeRoutes ignored the env var in that case (layerWith({}) → XDG only). The CLI server already passes directory: process.env.OPENCODE_CONFIG_DIR (server-process.ts), so the fallback isn't needed for the fix. It does change behaviour for other hosts that embed the server and leave config.directory unset. Can it be dropped?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: OPENCODE_CONFIG_DIR replaces global config in v2 services

1 participant