Skip to content

[BUG]: OPENCODE_CONFIG_DIR replaces global config in v2 services #32825

Description

@totalolage

Description

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

  1. Put a config value used by v2/core services, such as references, in the normal global config directory.
  2. Set OPENCODE_CONFIG_DIR to another config directory.
  3. Start opencode or inspect config-backed reference behavior.
  4. 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.

Screenshot and/or share link

N/A

Operating System

Linux

Terminal

N/A

Activity

  1. github-actions commented on Jun 18, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    bugSomething isn't working
    on Jul 5, 2026
  3. opencode-agent commented on Jul 8, 2026

    @opencode-agent
    Contributor

    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.

  4. minutechreview commented on Sep 5, 2026

    @minutechreview

    Same root cause as #28658, fixed in #47468.

  5. hotzen commented on Sep 26, 2026

    @hotzen

    please fix this asap.

    as this is now completely bypassing the global config, also mcp servers are missing

  6. diachkow commented on Sep 27, 2026

    @diachkow

    +1, I was also relying on that feature with my setup on v1

  7. Stainless2k commented on Sep 28, 2026

    @Stainless2k

    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

  8. cbrachem commented on Oct 13, 2026

    @cbrachem

    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:

    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.

  9. CodeWithAdarsh007 commented on Oct 22, 2026

    @CodeWithAdarsh007

    Reproduced on v2.1.3 and traced it to packages/util/src/global.ts:79:

    config: process.env.OPENCODE_CONFIG_DIR ?? Path.config

    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

    1. packages/cli/src/server-process.ts:38 reads OPENCODE_CONFIG_DIR once at server startup:

      Global.layerWith(process.env.OPENCODE_CONFIG_DIR ? { config: process.env.OPENCODE_CONFIG_DIR } : {})

      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.

    2. 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

    Happy to open a V2 PR against dev once the intended semantics are clear.

  10. tslator commented on Oct 26, 2026

    @tslator

    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:

    1. The first GET /api/command for a given location[directory] returns {"data":[]} (lazy load); repeated a few seconds later it returns the full set.
    2. 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.

  11. tslator commented on Oct 26, 2026

    @tslator

    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.

    OPENCODE_CONFIG_DIR sources listed by opencode debug config
    unset ~/.config/opencode/opencode.json, ~/.config/opencode/
    <dir> <dir>/opencode.jsonc, <dir>
    <dir> + OPENCODE_CONFIG=<file> <dir>/opencode.jsonc, <dir>, <file>

    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:

    1. 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.
    2. /api/provider returns {"data":[]} until the location warms — a cold read looks exactly like suppression (consistent with caveat 1 above).
    3. 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).

  12. niebloomj commented on Oct 28, 2026

    @niebloomj

    +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.

  13. opencodejr commented on Oct 29, 2026

    @opencodejr

    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

    Run it: bash repro.sh

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

Metadata

Metadata

Assignees

Labels

2.0bugSomething isn't workingcoreAnything pertaining to core functionality of the application (opencode server stuff)reproduced

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions