You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG]: OPENCODE_CONFIG_DIR replaces global config in v2 services #32825
OPENCODE_CONFIG_DIR is resolved differently by the old app config loader and the v2/core config loader.
The old loader treats OPENCODE_CONFIG_DIR as an extra config directory. The v2/core path currently treats it as a replacement for the normal XDG global config directory through Global.config.
This can make config-dependent services disagree. For example, global references can appear in app-level config output, while v2/core services that build available references and reference permissions do not see the same config layers.
Plugins
None required.
OpenCode version
Observed on v1.17.8 and current dev before the fix.
Steps to reproduce
Put a config value used by v2/core services, such as references, in the normal global config directory.
Set OPENCODE_CONFIG_DIR to another config directory.
Start opencode or inspect config-backed reference behavior.
The old app config path treats the env directory as additive, but v2/core resolves it as the global directory and misses the normal global config layer.
This issue might be a duplicate of existing issues. Please check:
OPENCODE_CONFIG_DIR overrides global AGENTS.md path instead of adding to it #28658: Same root cause — OPENCODE_CONFIG_DIR replaces the global config directory in Global.config instead of being additive, causing config-dependent services to miss the normal XDG global config layer. That issue reports the same behavior for AGENTS.md loading; this issue reports it for references and v2/core services.
Audit update (2026-07-08): #32824 is the active fix, currently targeting dev. It explicitly covers this V2 config regression; confirm merge/port sequencing before closing.
We also use this feature in our assistant to ship tools etc, while leaving the user the option to have their own tools etc via the global dir. Would be nice to have this reverted to old behavior or at least have a new env var with the old behavior, maybe OPENCODE_EXTRA_CONF_DIR
I can still reproduce the issue on version 2.0.27. It's now broader than references and AGENTS.md: with OPENCODE_CONFIG_DIR set, ~/.config/opencode is not loaded at all (config, agents, commands, skills, plugins, MCP servers).
As far as I can tell, no single commit changed this on purpose:
test: use testEffect for instruction tests #25046 set Global.config to OPENCODE_CONFIG_DIR ?? Path.config in core. V1 still worked because packages/opencode used its own additive loader. Only core consumers broke (the global AGENTS.md, references). That is this issue.
44b6938b2a removed packages/opencode, and 9445b4d940 wires the variable through Global.layerWith({ config }) in the CLI and server. Since then, ConfigDiscovery treats it as the only global directory.
The V2 docs do not mention OPENCODE_CONFIG_DIR anymore, so I don't know if that change was intentional.
I would like to propose that OPENCODE_CONFIG_DIR is added back as an additional config source. We use it to deploy company wide OpenCode components to engineers. Other commenters here and on #47468 describe the same setup.
If OPENCODE_CONFIG_DIR is indeed intended to mean "replaces the original config dir" now, could we perhaps introduce a new env variable like OPENCODE_EXTRA_CONFIG_DIR with the previous semantics?
If a maintainer confirms the intended semantics, I am happy to bring a PR.
With OPENCODE_CONFIG_DIR set, Path.config (the real XDG global config dir) is discarded entirely -- the override replaces rather than adds to the global root.
Reproduction
On a config with ~131 models from custom providers:
# baseline (no override)
opencode models > baseline.txt # 131 models
# override set, fresh server
set OPENCODE_CONFIG_DIR=...\scratch
opencode models --standalone > override.txt
fc /n baseline.txt override.txt
Result: override.txt contains only the built-in openai/* defaults. Every custom provider defined in the global config is gone. Confirmed visually via /model in opencode --standalone as well -- the list collapses to built-ins only.
Two related observations from the code
packages/cli/src/server-process.ts:38 reads OPENCODE_CONFIG_DIR once at server startup:
So an already-running shared daemon won't pick up client-side env changes -- reproduction needs --standalone or a server restart to observe the drop.
PR fix(core): keep OPENCODE_CONFIG_DIR additive for global AGENTS.md #47468 (closed as V1-only) addressed the same class of issue for AGENTS.md only -- it left the ?? alone and added a Global.configDirs() helper so callers check both the override and the real global default (additive). It does not cover provider/model config, so I'm not proposing to port it as a fix for this bug. But the shape of the fix is probably the same: additive at the ConfigDiscovery layer, rather than replacing at the Path.config root.
Questions for maintainers
Intended semantics: should OPENCODE_CONFIG_DIRreplace the global config root (current behavior), or add to it? V1 docs describe additive; V2 docs don't mention the variable at all.
Reproduced on 2.0.32 and 2.1.4 (Linux x64, opencode serve --service behind basic auth). In current v2 the replacement is partial per resource type: agents/ and skills/ behave additively, but commands/ from the config dir replace the user-level set.
Setup: fake HOME (copy of the real ~/.config/opencode, containing commands/{deeper,explain,summarize}.md), OPENCODE_CONFIG_DIR=/tmp/rk3/config with commands/rk3-cmd.md + agent/rk3-agent.md. Query GET /api/command?location[directory]=<HOME> (twice — see caveat below):
binary
OPENCODE_CONFIG_DIR
commands returned
2.0.32
unset
deeper, explain, init, review, summarize
2.0.32
/tmp/rk3/config
init, review, rk3-cmd
2.1.4
/tmp/rk3/config
init, review, rk3-cmd
So with the env set, config-dir commands do load, but every ~/.config/opencode/commands/ entry silently disappears — while GET /api/agent on the same instance still lists both config-dir and built-in agents. Matches the Path.config override in global.ts flagged above, but the resource-type split (agents additive, commands exclusive) is new data.
Probe caveats that can misdiagnose this:
The firstGET /api/command for a given location[directory] returns {"data":[]} (lazy load); repeated a few seconds later it returns the full set.
Plain opencode serve (no --service) returns [] for every location even after warm-up, including built-ins — only background-service mode is a valid probe.
Impact: setups using OPENCODE_CONFIG_DIR as an overlay to ship a shared agent/skill/command set silently lose the user's own commands. Related headless angle in #49903.
Follow-up on the same setup: the replacement also covers the config file, not just resource directories — and the provider list under it follows a rule worth writing down. Reproduced on 2.1.4 (Linux x64, fake HOME, serve --service + basic auth, opencode debug config for the source list; all credentials fake).
1. The user-level config file is not a source at all.
So providers, permissions, default_agent and instructions in the user file stop applying silently. Confirmed by enforcement, not just the source list: a {"action":"read","resource":"*secrets*","effect":"deny"} rule that exists only in the user file does not stop the read; the same rule in the config-dir file produces Permission denied: read.
2. Providers that still get listed anyway — and it is not the credential story. The guess "a provider with a stored credential survives the override" is false:
provider shape (var set)
listed?
definition in force
custom id, defined only in the user file (inert once the var is set)
no
—
custom id, user-file definition + auth.json entry
no
—
auth.json entry, no definition
no
—
models.dev catalog id, no credential, no active definition
no
—
catalog id + credential
yes
the catalog's package/api
catalog id + credential + definition in a file that is not a source
yes
still the catalog's — that file's baseURL and model aliases are gone
catalog id + definition in OPENCODE_CONFIG
yes
the overlay's package/baseURL, and no credential needed
custom id, defined in the active config file (config dir or OPENCODE_CONFIG)
yes
that definition
built-ins (opencode, huggingface)
yes
auto-activation
i.e. listed = always-on built-ins ∪ (models.dev catalog id ∩ credential) ∪ ids defined in the active config file. On a real host this predicts the post-cutover list exactly: two catalog ids with stored credentials survive, two custom ids with stored credentials vanish.
The dangerous row is #6: the provider keeps answering requests while the operator's own definition of it is inert, because the catalog's api happens to match the baseURL the definition used. Nothing errors, so the loss of the custom baseURL/alias is invisible.
3. OPENCODE_CONFIG merges where OPENCODE_CONFIG_DIR replaces. As a third source it is additive: providers from it list alongside the config dir's, its permissions array concatenates with the config dir's (with a *secrets* deny in the config dir and a *probe-token* deny in the overlay, both fire — an overlay cannot weaken the shared policy), and its default_agent/instructions apply. So the layering workaround exists; the complaint is that the default direction is silent replacement in a variable whose name (CONFIG_DIR) reads as "add this directory".
Probe caveats that can misdiagnose this:
opencode debug config starts a managed service; with a live service already holding the default port it exits with Managed service port 49374 … is already in use. Run opencode service set port <p> per fake HOME first.
/api/provider returns {"data":[]} until the location warms — a cold read looks exactly like suppression (consistent with caveat 1 above).
Definitions that live only in a file which is not a source cannot be distinguished from suppressed ones by looking at /api/provider alone; the debug config source list is what makes the mechanism visible.
Impact: anyone using OPENCODE_CONFIG_DIR to ship a shared config silently loses their own providers/permissions/default_agent/instructions, and providers whose id matches models.dev keep working under different settings than the ones they wrote. Minimal ask: log one warning line naming the user-level file that was skipped (and ideally the ids whose definition was shadowed by a catalog entry).
+1, still reproducible on 2.2.0 (macOS arm64, desktop app + Homebrew CLI).
My setup: a managed setup script exports OPENCODE_CONFIG_DIR pointing to a separate folder for company-managed settings (providers, policies). A config sync tool writes my personal MCP servers to ~/.config/opencode/opencode.json. With OPENCODE_CONFIG_DIR set, none of the servers in ~/.config/opencode load, and nothing reports it. The only sign is that the servers' tools are missing in a session.
What made it hard to diagnose: an older 1.x binary earlier on my PATH still showed the servers as connected in opencode mcp list. opencode debug config from the 2.x binary was the only thing that showed ~/.config/opencode wasn't in the source list.
+1 to an additive option (like the OPENCODE_EXTRA_CONF_DIR suggested above) so managed and personal configs can coexist.
Thanks for the report. I reproduced this on v2.2.0, and it still happens on the latest dev (f43563b).
Setup: a global ~/.config/opencode/opencode.json and a separate directory with its own opencode.json, in a throwaway HOME. opencode debug config lists the global file and directory as sources. With OPENCODE_CONFIG_DIR pointing at the other directory, it lists only that directory: the global layer, and every setting in it, is no longer loaded.
The docs say the custom directory is loaded after the global config and can override it, so it should add to the global config rather than replace it. The cause seems to be that the global config directory itself is set to OPENCODE_CONFIG_DIR ?? <XDG config dir> (packages/util/src/global.ts), so config discovery never looks at ~/.config/opencode once the variable is set.
The script below reproduces it against an installed opencode. Related: #28658 (same root cause, for AGENTS.md); #58141 and #58300 (background service and per-config-dir isolation).
Reproduction script (repro.sh)
#!/usr/bin/env bash# Reproduces #32825: OPENCODE_CONFIG_DIR replaces ~/.config/opencode instead of adding to it.# Usage: OC="opencode" bash repro.sh (OC may be any command that runs the opencode CLI)set -u
OC=${OC:-opencode}
ROOT=$(mktemp -d /tmp/opencode/oc-cfgdir-XXXX)export HOME=$ROOT/home XDG_CONFIG_HOME=$ROOT/home/.config XDG_DATA_HOME=$ROOT/home/.local/share XDG_STATE_HOME=$ROOT/home/.local/state XDG_CACHE_HOME=$ROOT/home/.cache
mkdir -p $XDG_CONFIG_HOME/opencode/commands $ROOT/extra/commands $ROOT/project
echo'{"$schema":"https://opencode.ai/config.json","username":"from-global"}'>$XDG_CONFIG_HOME/opencode/opencode.json
echo'global command'>$XDG_CONFIG_HOME/opencode/commands/global-cmd.md
echo'{"$schema":"https://opencode.ai/config.json","share":"disabled"}'>$ROOT/extra/opencode.json
echo'extra command'>$ROOT/extra/commands/extra-cmd.md
cd$ROOT/project
echo"=== without OPENCODE_CONFIG_DIR ==="$OC debug config --standalone 2>&1| head -40
echo"=== with OPENCODE_CONFIG_DIR=$ROOT/extra ==="
OPENCODE_CONFIG_DIR=$ROOT/extra $OC debug config --standalone 2>&1| head -40
Activity
github-actions commentedon Jun 18, 2026
This issue might be a duplicate of existing issues. Please check:
OPENCODE_CONFIG_DIRreplaces the global config directory inGlobal.configinstead of being additive, causing config-dependent services to miss the normal XDG global config layer. That issue reports the same behavior forAGENTS.mdloading; this issue reports it forreferencesand v2/core services.opencode-agent commentedon Jul 8, 2026
Audit update (2026-07-08): #32824 is the active fix, currently targeting
dev. It explicitly covers this V2 config regression; confirm merge/port sequencing before closing.minutechreview commentedon Sep 5, 2026
Same root cause as #28658, fixed in #47468.
fix(agents): Orca の overlay 下でも OpenCode の permission を読ませる
hotzen commentedon Sep 26, 2026
please fix this asap.
as this is now completely bypassing the global config, also mcp servers are missing
diachkow commentedon Sep 27, 2026
+1, I was also relying on that feature with my setup on v1
Stainless2k commentedon Sep 28, 2026
We also use this feature in our assistant to ship tools etc, while leaving the user the option to have their own tools etc via the global dir. Would be nice to have this reverted to old behavior or at least have a new env var with the old behavior, maybe
OPENCODE_EXTRA_CONF_DIRcbrachem commentedon Oct 13, 2026
I can still reproduce the issue on version 2.0.27. It's now broader than
referencesandAGENTS.md: withOPENCODE_CONFIG_DIRset,~/.config/opencodeis not loaded at all (config, agents, commands, skills, plugins, MCP servers).As far as I can tell, no single commit changed this on purpose:
OPENCODE_CONFIG_DIRas an additional directory. V1ConfigPaths.directories()loaded it next toGlobal.Path.config. The V1 docs still describe it that way.Global.configtoOPENCODE_CONFIG_DIR ?? Path.configin core. V1 still worked becausepackages/opencodeused its own additive loader. Only core consumers broke (the globalAGENTS.md,references). That is this issue.packages/opencode, and 9445b4d940 wires the variable throughGlobal.layerWith({ config })in the CLI and server. Since then,ConfigDiscoverytreats it as the only global directory.The V2 docs do not mention
OPENCODE_CONFIG_DIRanymore, so I don't know if that change was intentional.I would like to propose that
OPENCODE_CONFIG_DIRis added back as an additional config source. We use it to deploy company wide OpenCode components to engineers. Other commenters here and on #47468 describe the same setup.If
OPENCODE_CONFIG_DIRis indeed intended to mean "replaces the original config dir" now, could we perhaps introduce a new env variable likeOPENCODE_EXTRA_CONFIG_DIRwith the previous semantics?If a maintainer confirms the intended semantics, I am happy to bring a PR.
CodeWithAdarsh007 commentedon Oct 22, 2026
Reproduced on v2.1.3 and traced it to
packages/util/src/global.ts:79:With
OPENCODE_CONFIG_DIRset,Path.config(the real XDG global config dir) is discarded entirely -- the override replaces rather than adds to the global root.Reproduction
On a config with ~131 models from custom providers:
Result:
override.txtcontains only the built-inopenai/*defaults. Every custom provider defined in the global config is gone. Confirmed visually via/modelinopencode --standaloneas well -- the list collapses to built-ins only.Two related observations from the code
packages/cli/src/server-process.ts:38readsOPENCODE_CONFIG_DIRonce at server startup:So an already-running shared daemon won't pick up client-side env changes -- reproduction needs
--standaloneor a server restart to observe the drop.PR fix(core): keep OPENCODE_CONFIG_DIR additive for global AGENTS.md #47468 (closed as V1-only) addressed the same class of issue for AGENTS.md only -- it left the
??alone and added aGlobal.configDirs()helper so callers check both the override and the real global default (additive). It does not cover provider/model config, so I'm not proposing to port it as a fix for this bug. But the shape of the fix is probably the same: additive at theConfigDiscoverylayer, rather than replacing at thePath.configroot.Questions for maintainers
OPENCODE_CONFIG_DIRreplace the global config root (current behavior), or add to it? V1 docs describe additive; V2 docs don't mention the variable at all.util/global.ts:79(change the??), or via aconfigDirs()-style helper at the config-loading layer, like fix(core): keep OPENCODE_CONFIG_DIR additive for global AGENTS.md #47468 did for AGENTS.md?OPENCODE_EXTRA_CONFIG_DIRSas a separate additive var -- is that the preferred direction rather than changingOPENCODE_CONFIG_DIR's semantics?Happy to open a V2 PR against
devonce the intended semantics are clear.tslator commentedon Oct 26, 2026
Reproduced on 2.0.32 and 2.1.4 (Linux x64,
opencode serve --servicebehind basic auth). In current v2 the replacement is partial per resource type:agents/andskills/behave additively, butcommands/from the config dir replace the user-level set.Setup: fake
HOME(copy of the real~/.config/opencode, containingcommands/{deeper,explain,summarize}.md),OPENCODE_CONFIG_DIR=/tmp/rk3/configwithcommands/rk3-cmd.md+agent/rk3-agent.md. QueryGET /api/command?location[directory]=<HOME>(twice — see caveat below):/tmp/rk3/config/tmp/rk3/configSo with the env set, config-dir commands do load, but every
~/.config/opencode/commands/entry silently disappears — whileGET /api/agenton the same instance still lists both config-dir and built-in agents. Matches thePath.configoverride inglobal.tsflagged above, but the resource-type split (agents additive, commands exclusive) is new data.Probe caveats that can misdiagnose this:
GET /api/commandfor a givenlocation[directory]returns{"data":[]}(lazy load); repeated a few seconds later it returns the full set.opencode serve(no--service) returns[]for every location even after warm-up, including built-ins — only background-service mode is a valid probe.Impact: setups using
OPENCODE_CONFIG_DIRas an overlay to ship a shared agent/skill/command set silently lose the user's own commands. Related headless angle in #49903.tslator commentedon Oct 26, 2026
Follow-up on the same setup: the replacement also covers the config file, not just resource directories — and the provider list under it follows a rule worth writing down. Reproduced on 2.1.4 (Linux x64, fake
HOME,serve --service+ basic auth,opencode debug configfor the source list; all credentials fake).1. The user-level config file is not a source at all.
OPENCODE_CONFIG_DIRopencode debug config~/.config/opencode/opencode.json,~/.config/opencode/<dir><dir>/opencode.jsonc,<dir><dir>+OPENCODE_CONFIG=<file><dir>/opencode.jsonc,<dir>,<file>So
providers,permissions,default_agentandinstructionsin the user file stop applying silently. Confirmed by enforcement, not just the source list: a{"action":"read","resource":"*secrets*","effect":"deny"}rule that exists only in the user file does not stop the read; the same rule in the config-dir file producesPermission denied: read.2. Providers that still get listed anyway — and it is not the credential story. The guess "a provider with a stored credential survives the override" is false:
auth.jsonentryauth.jsonentry, no definitionbaseURLand model aliases are goneOPENCODE_CONFIGbaseURL, and no credential neededOPENCODE_CONFIG)opencode,huggingface)i.e. listed = always-on built-ins ∪ (models.dev catalog id ∩ credential) ∪ ids defined in the active config file. On a real host this predicts the post-cutover list exactly: two catalog ids with stored credentials survive, two custom ids with stored credentials vanish.
The dangerous row is #6: the provider keeps answering requests while the operator's own definition of it is inert, because the catalog's
apihappens to match thebaseURLthe definition used. Nothing errors, so the loss of the custombaseURL/alias is invisible.3.
OPENCODE_CONFIGmerges whereOPENCODE_CONFIG_DIRreplaces. As a third source it is additive:providersfrom it list alongside the config dir's, itspermissionsarray concatenates with the config dir's (with a*secrets*deny in the config dir and a*probe-token*deny in the overlay, both fire — an overlay cannot weaken the shared policy), and itsdefault_agent/instructionsapply. So the layering workaround exists; the complaint is that the default direction is silent replacement in a variable whose name (CONFIG_DIR) reads as "add this directory".Probe caveats that can misdiagnose this:
opencode debug configstarts a managed service; with a live service already holding the default port it exits withManaged service port 49374 … is already in use. Runopencode service set port <p>per fakeHOMEfirst./api/providerreturns{"data":[]}until the location warms — a cold read looks exactly like suppression (consistent with caveat 1 above)./api/provideralone; thedebug configsource list is what makes the mechanism visible.Impact: anyone using
OPENCODE_CONFIG_DIRto ship a shared config silently loses their ownproviders/permissions/default_agent/instructions, and providers whose id matches models.dev keep working under different settings than the ones they wrote. Minimal ask: log one warning line naming the user-level file that was skipped (and ideally the ids whose definition was shadowed by a catalog entry).niebloomj commentedon Oct 28, 2026
+1, still reproducible on 2.2.0 (macOS arm64, desktop app + Homebrew CLI).
My setup: a managed setup script exports
OPENCODE_CONFIG_DIRpointing to a separate folder for company-managed settings (providers, policies). A config sync tool writes my personal MCP servers to~/.config/opencode/opencode.json. WithOPENCODE_CONFIG_DIRset, none of the servers in~/.config/opencodeload, and nothing reports it. The only sign is that the servers' tools are missing in a session.What made it hard to diagnose: an older 1.x binary earlier on my
PATHstill showed the servers as connected inopencode mcp list.opencode debug configfrom the 2.x binary was the only thing that showed~/.config/opencodewasn't in the source list.+1 to an additive option (like the
OPENCODE_EXTRA_CONF_DIRsuggested above) so managed and personal configs can coexist.opencodejr commentedon Oct 29, 2026
Thanks for the report. I reproduced this on v2.2.0, and it still happens on the latest
dev(f43563b).Setup: a global
~/.config/opencode/opencode.jsonand a separate directory with its ownopencode.json, in a throwawayHOME.opencode debug configlists the global file and directory as sources. WithOPENCODE_CONFIG_DIRpointing at the other directory, it lists only that directory: the global layer, and every setting in it, is no longer loaded.The docs say the custom directory is loaded after the global config and can override it, so it should add to the global config rather than replace it. The cause seems to be that the global config directory itself is set to
OPENCODE_CONFIG_DIR ?? <XDG config dir>(packages/util/src/global.ts), so config discovery never looks at~/.config/opencodeonce the variable is set.The script below reproduces it against an installed
opencode. Related: #28658 (same root cause, forAGENTS.md); #58141 and #58300 (background service and per-config-dir isolation).Reproduction script (repro.sh)
Run it:
bash repro.sh