Skip to content

fix(install): respect OPENCODE_CONFIG_DIR env var for global config path#573

Open
cavanaug wants to merge 4 commits intoEveryInc:mainfrom
cavanaug:fix/opencode-config-dir
Open

fix(install): respect OPENCODE_CONFIG_DIR env var for global config path#573
cavanaug wants to merge 4 commits intoEveryInc:mainfrom
cavanaug:fix/opencode-config-dir

Conversation

@cavanaug
Copy link
Copy Markdown

Note: I used an AI agent (upstream-skill) to help draft this, but I (cavanaug) reviewed the write-up and the code. If the tone/format isn't a good fit here, please let me know and I'll adjust the skill.

I noticed the install command and sync registry always resolve the OpenCode global config path to ~/.config/opencode, ignoring the OPENCODE_CONFIG_DIR environment variable that OpenCode itself honours.

This causes problems when users have a non-standard config location (e.g. on NixOS, in Docker, or with XDG_CONFIG_HOME set), because bun x compound-engineering install or bun x compound-engineering sync writes to the wrong place while OpenCode reads from wherever OPENCODE_CONFIG_DIR points.

The fix is small: both resolveOutputRoot in src/commands/install.ts and the opencode entry in src/sync/registry.ts now check OPENCODE_CONFIG_DIR first and fall back to ~/.config/opencode when it's unset. No behaviour change for users without the variable set.

Honour OPENCODE_CONFIG_DIR when resolving the default OpenCode install
target, both in the install command and in the sync registry. Falls back
to ~/.config/opencode when the variable is unset, preserving existing
behaviour.
Copilot AI review requested due to automatic review settings April 16, 2026 10:19
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the install + sync flows to respect OpenCode’s OPENCODE_CONFIG_DIR environment override when determining the global OpenCode config directory, falling back to ~/.config/opencode when unset.

Changes:

  • Update OpenCode sync target detection/output root to prefer OPENCODE_CONFIG_DIR.
  • Update install default output root resolution to prefer OPENCODE_CONFIG_DIR.
  • Adjust inline comments to document the new precedence behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/sync/registry.ts Prefer OPENCODE_CONFIG_DIR for OpenCode detect paths and sync output root.
src/commands/install.ts Prefer OPENCODE_CONFIG_DIR when defaulting the install output root for OpenCode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/sync/registry.ts Outdated
Comment on lines +46 to +54
process.env.OPENCODE_CONFIG_DIR
? path.resolve(process.env.OPENCODE_CONFIG_DIR)
: path.join(home, ".config", "opencode"),
path.join(cwd, ".opencode"),
],
resolveOutputRoot: (home) => path.join(home, ".config", "opencode"),
resolveOutputRoot: (home) =>
process.env.OPENCODE_CONFIG_DIR
? path.resolve(process.env.OPENCODE_CONFIG_DIR)
: path.join(home, ".config", "opencode"),
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

OPENCODE_CONFIG_DIR is used without trimming or ~ expansion. If a user sets it to whitespace (e.g. " /some/path ") or a tilde path (e.g. ~/opencode), path.resolve will either resolve to an unintended directory (relative to CWD) or treat ~ literally. Consider normalizing with trim() + expandHome() (or a shared helper) before resolving, and only honoring the env var when the trimmed value is non-empty.

Copilot uses AI. Check for mistakes.
Comment thread src/commands/install.ts Outdated
Comment on lines +270 to +271
if (process.env.OPENCODE_CONFIG_DIR) {
return path.resolve(process.env.OPENCODE_CONFIG_DIR)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

process.env.OPENCODE_CONFIG_DIR is checked for truthiness but not trimmed/expanded. A value like " ~/cfg " (or any leading/trailing whitespace) will be treated as set and path.resolve won’t expand ~, potentially writing to the wrong location. Suggest using the existing resolveTargetHome/expandHome normalization (and only honoring the env var when the trimmed value is non-empty).

Suggested change
if (process.env.OPENCODE_CONFIG_DIR) {
return path.resolve(process.env.OPENCODE_CONFIG_DIR)
const configDir = process.env.OPENCODE_CONFIG_DIR?.trim()
if (configDir) {
return resolveTargetHome(configDir)

Copilot uses AI. Check for mistakes.
Comment thread src/commands/install.ts Outdated
Comment on lines 267 to 273
// OpenCode global config: respect OPENCODE_CONFIG_DIR if set, otherwise
// fall back to ~/.config/opencode per XDG spec.
// See: https://opencode.ai/docs/config/
if (process.env.OPENCODE_CONFIG_DIR) {
return path.resolve(process.env.OPENCODE_CONFIG_DIR)
}
return path.join(os.homedir(), ".config", "opencode")
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

This change introduces new behavior (default install output root depends on OPENCODE_CONFIG_DIR) but there’s no test covering it. There are already CLI tests asserting the default ~/.config/opencode behavior; please add a companion test that sets OPENCODE_CONFIG_DIR and asserts output is written under that directory.

Copilot uses AI. Check for mistakes.
Comment thread src/sync/registry.ts
Comment on lines 43 to 55
{
name: "opencode",
detectPaths: (home, cwd) => [
path.join(home, ".config", "opencode"),
process.env.OPENCODE_CONFIG_DIR
? path.resolve(process.env.OPENCODE_CONFIG_DIR)
: path.join(home, ".config", "opencode"),
path.join(cwd, ".opencode"),
],
resolveOutputRoot: (home) => path.join(home, ".config", "opencode"),
resolveOutputRoot: (home) =>
process.env.OPENCODE_CONFIG_DIR
? path.resolve(process.env.OPENCODE_CONFIG_DIR)
: path.join(home, ".config", "opencode"),
sync: syncToOpenCode,
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

detectPaths / resolveOutputRoot now depend on OPENCODE_CONFIG_DIR, but the tool detection/sync path logic isn’t covered by a test for this env override. The repo already has detectInstalledTools tests; consider adding a case that sets OPENCODE_CONFIG_DIR to a temp dir, creates that dir, and asserts OpenCode is detected (and not detected when only ~/.config/opencode exists and the env var points elsewhere).

Copilot uses AI. Check for mistakes.
Apply trim() and expandHome() to OPENCODE_CONFIG_DIR before path.resolve
in both src/sync/registry.ts and src/commands/install.ts, preventing
tilde paths (~/opencode) and whitespace-padded values from resolving to
wrong locations.

Refactor registry.ts to use a shared resolveOpenCodeConfigDir() helper
so the normalization is defined once and used in both detectPaths and
resolveOutputRoot.

Add tests covering the env var override for both install (cli.test.ts)
and tool detection (detect-tools.test.ts).
@cavanaug
Copy link
Copy Markdown
Author

Pushed a follow-up commit (eab704e) addressing all four Copilot review comments:

Normalization fixes (registry.ts + install.ts)

Both files now apply .trim() and expandHome() before path.resolve() on OPENCODE_CONFIG_DIR, so tilde paths (~/opencode) and whitespace-padded values no longer resolve to the wrong location. In registry.ts I extracted a resolveOpenCodeConfigDir(home) helper so the normalization is defined once and reused by both detectPaths and resolveOutputRoot — avoids the ternary duplication that was there before.

Tests

  • tests/cli.test.ts: new test sets OPENCODE_CONFIG_DIR to a temp dir, runs install --to opencode, and asserts output lands under that dir and not under ~/.config/opencode.
  • tests/detect-tools.test.ts: two new tests — one confirms detection when OPENCODE_CONFIG_DIR points at an existing dir (with no ~/.config/opencode present), the other confirms non-detection when the env var points at a non-existent dir (even when ~/.config/opencode does exist).

All 662 existing tests still pass; the one pre-existing failure (resolve-base.sh > prefers the PR base remote from gh metadata over origin) is a gh-network timeout unrelated to these changes.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eab704eb7b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/commands/install.ts
Comment on lines +270 to +273
const envDir = process.env.OPENCODE_CONFIG_DIR?.trim()
if (envDir) {
return path.resolve(expandHome(envDir))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat OPENCODE_CONFIG_DIR as global OpenCode root

Returning OPENCODE_CONFIG_DIR directly as outputRoot causes a layout regression when the directory name is not opencode/.opencode: writeOpenCodeBundle() infers global-vs-workspace structure from path.basename(outputRoot) (src/targets/opencode.ts, resolveOpenCodePaths), so installs go to <envDir>/.opencode/... instead of <envDir>/agents|skills|commands. In that scenario, users who set a custom config dir (for example /tmp/custom-config) will get files written to a path OpenCode does not treat as its global config tree.

Useful? React with 👍 / 👎.

…regression

writeOpenCodeBundle infers global vs workspace layout from path.basename,
so a custom OPENCODE_CONFIG_DIR value like /tmp/my-config would fall into
the workspace branch and write agents to <dir>/.opencode/agents/ instead
of the flat <dir>/agents/ layout OpenCode expects for its config root.

Fix: thread isGlobalOpenCodeConfig from resolveOutputRoot through install
and pass scope='global' to writeOpenCodeBundle when the output root is the
global config dir (default ~/.config/opencode or OPENCODE_CONFIG_DIR).
Update resolveOpenCodePaths to treat scope='global' as global layout,
independent of basename.

Also update the OPENCODE_CONFIG_DIR install test to assert flat layout.
@cavanaug
Copy link
Copy Markdown
Author

The Codex bot's concern is valid — pushed a second follow-up (411c420) to fix it.

Root cause confirmed

resolveOpenCodePaths in src/targets/opencode.ts infers global-vs-workspace layout from path.basename(outputRoot). It only recognises "opencode" or ".opencode" as global — any other name (including a custom OPENCODE_CONFIG_DIR like /home/user/myconfig) falls into the workspace branch and writes agents/ under a nested .opencode/ subdirectory, which OpenCode does not treat as its global config tree.

Fix (411c420)

  • resolveOutputRoot in install.ts now returns { root, isGlobalOpenCodeConfig }true when the path came from OPENCODE_CONFIG_DIR or the default ~/.config/opencode, false when it came from an explicit --output flag.
  • The install command computes effectiveScope = "global" when targetName === "opencode" and isGlobalOpenCodeConfig is true, then passes it to target.write().
  • writeOpenCodeBundle forwards scope to resolveOpenCodePaths, which now treats scope === "global" as global layout regardless of basename. The existing basename check is kept as a fallback for callers that don't pass scope.
  • The OPENCODE_CONFIG_DIR CLI test is updated to assert the flat layout (<dir>/agents/...) instead of the nested layout.

Existing behaviour for --output <custom-path> (workspace/nested layout) is unchanged — all 16 CLI tests pass.

@cavanaug
Copy link
Copy Markdown
Author

Treat OPENCODE_CONFIG_DIR as global OpenCode root — chatgpt-codex-connector[bot]

This was a valuable insight that we missed — thank you. The basename heuristic in resolveOpenCodePaths was indeed silently producing the wrong layout for any OPENCODE_CONFIG_DIR value whose last path segment isn't literally opencode or .opencode.

The approach we took (411c420) was to thread an explicit isGlobalOpenCodeConfig signal from resolveOutputRoot through the install command and into writeOpenCodeBundle as scope="global", rather than trying to fix the basename inference itself. The reasoning: the install command is the only place that knows why a particular output root was chosen, so it's the right layer to make this determination — and scope was already wired through to write() but unused for opencode.

That said, we're open to alternative approaches if there's a cleaner way to handle this. A few options we considered but didn't pursue:

  • Always append /opencode to OPENCODE_CONFIG_DIR if the basename isn't already opencode — preserves the basename heuristic but feels fragile and may not match what OpenCode actually does with the env var.
  • Fix resolveOpenCodePaths to accept an explicit isGlobal: boolean instead of scope — more explicit but essentially the same as what we did with different naming.
  • Remove the basename heuristic entirely and always require an explicit scope signal — cleaner long-term but a broader change with more surface area to test.

If there's a preferred approach or an issue with the current one, happy to revise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 411c4204f8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/commands/install.ts
Comment on lines +166 to +170
// For OpenCode, if the output root is the global config dir (default or OPENCODE_CONFIG_DIR),
// use "global" scope so writeOpenCodeBundle writes the flat layout regardless of basename.
const effectiveScope =
targetName === "opencode" && isGlobalOpenCodeConfig && resolvedScope === undefined
? "global"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply OpenCode global scope in --to all path

Fresh evidence from this commit: isGlobalOpenCodeConfig and effectiveScope were added, but effectiveScope is only computed in the single-target branch after the targetName === "all" early return. If OPENCODE_CONFIG_DIR is set to a directory whose basename is not opencode/.opencode, install --to all still calls writeOpenCodeBundle without global scope, so OpenCode files are written under <envDir>/.opencode/... instead of the global <envDir>/{agents,skills,commands} layout.

Useful? React with 👍 / 👎.

The effectiveScope/isGlobalOpenCodeConfig logic added in 411c420 was only
applied in the single-target branch. The --to all path called
handler.write(root, bundle) without a scope, so OPENCODE_CONFIG_DIR with
a non-standard basename still produced the nested workspace layout.

Pass scope='global' for the opencode target in the --to all loop when
isGlobalOpenCodeConfig is true, consistent with the single-target path.
@cavanaug
Copy link
Copy Markdown
Author

Apply OpenCode global scope in --to all path — chatgpt-codex-connector[bot]

Correct — another genuine miss. The effectiveScope / isGlobalOpenCodeConfig logic from 411c420 was only wired into the single-target branch; the --to all loop called handler.write(root, bundle) with no scope, leaving the same basename-inference bug in place for that path.

Fixed in c1475a1: the --to all loop now passes scope="global" for the opencode target when isGlobalOpenCodeConfig is true, consistent with the single-target path. All existing tests still pass.

@tmchow
Copy link
Copy Markdown
Collaborator

tmchow commented Apr 16, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1475a1456

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/commands/install.ts
Comment on lines +278 to +280
const envDir = process.env.OPENCODE_CONFIG_DIR?.trim()
if (envDir) {
return { root: path.resolve(expandHome(envDir)), isGlobalOpenCodeConfig: true }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep global opencode.json writes when OPENCODE_CONFIG_DIR is set

resolveOutputRoot now treats OPENCODE_CONFIG_DIR as the primary global output root, so install --to opencode writes opencode.json there and skips ~/.config/opencode/opencode.json whenever the env var exists. In this repo’s OpenCode spec (docs/specs/opencode.md), custom directory sources are additive in precedence rather than a replacement for global config files, so MCP/permission updates can be silently missed for users who set OPENCODE_CONFIG_DIR for overlays. The config file target should remain the global config path (or explicit OPENCODE_CONFIG) instead of being redirected by OPENCODE_CONFIG_DIR.

Useful? React with 👍 / 👎.

Comment thread src/sync/registry.ts
Comment on lines 54 to 56
detectPaths: (home, cwd) => [
path.join(home, ".config", "opencode"),
resolveOpenCodeConfigDir(home),
path.join(cwd, ".opencode"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve global OpenCode detection when custom dir is set

With this change, OpenCode detection uses resolveOpenCodeConfigDir(home) first and no longer checks ~/.config/opencode when OPENCODE_CONFIG_DIR is defined. If that env var points to a missing or transient directory, detectInstalledTools reports OpenCode as not installed even when the global install exists, which can cause install --to all and sync all to skip OpenCode unexpectedly. Detection should consider both the custom directory and the standard global directory rather than treating the env path as exclusive.

Useful? React with 👍 / 👎.

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.

3 participants