[GH-ISSUE #2168] fix(setup): empty XDG_CONFIG_HOME writes OpenCode registration under the working directory #714

Closed
opened 2026-10-15 18:27:38 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 12, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2168

Summary

With XDG_CONFIG_HOME set to an empty string and no OpenCode path overrides, rea setup --client opencode --skill=false --yes --json writes the registration to opencode/opencode.json under the caller's working directory. It exits successfully and reports ready, although the default global OpenCode registration file under HOME/.config/opencode is absent. Both tested REA builds produce this result. The unset-variable control writes the expected global file.

Runtime or environment

Source checkout c22111eca2, package metadata 6.4.0, and the separately installed published rea-agents 6.4.0 package. Both ran on Node.js 24.21.0, macOS arm64. This Node version is in REA's documented supported runtime range; the exact pinned contributor runtime was not used. OpenCode 1.18.35 and its pinned xdg-basedir 5.1.0 dependency were inspected to establish the client lookup contract. A native OpenCode session was not run.

Area: Setup, packaging, or upgrade. Interface: CLI. Earliest failure stage: Setup or provider admission. Target context: an empty isolated client home and a separate empty working directory; no analysis artifact or provider session is required.

Steps to reproduce

Use the tested package's scripts/rea.mjs dispatcher with a supported Node executable on PATH. Save this fixture as reproduce.py, then run python3 reproduce.py /absolute/path/to/rea-agents/scripts/rea.mjs. It uses an environment whitelist, so every inherited client-config override is absent. The three cases use different isolated homes and workspaces.

import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile

node = shutil.which("node")
assert node, "Put a supported Node executable on PATH"
cli = str(Path(sys.argv[1]).resolve())
assert Path(cli).is_file(), "Pass the selected package's scripts/rea.mjs"

with tempfile.TemporaryDirectory(prefix="rea-xdg-check-") as temporary:
    root = Path(temporary).resolve()
    for case in ("empty", "unset", "explicit"):
        base = root / case
        home = base / "home"
        workspace = base / "workspace"
        temporary_files = base / "tmp"
        for directory in (home, workspace, temporary_files):
            directory.mkdir(parents=True)
        environment = {
            "PATH": os.environ["PATH"],
            "HOME": str(home),
            "USERPROFILE": str(home),
            "TMPDIR": str(temporary_files),
            "XDG_DATA_HOME": str(base / "data"),
            "XDG_CACHE_HOME": str(base / "cache"),
            "XDG_STATE_HOME": str(base / "state"),
            "npm_config_cache": str(base / "npm-cache"),
        }
        if case == "empty":
            environment["XDG_CONFIG_HOME"] = ""
        if case == "explicit":
            environment["XDG_CONFIG_HOME"] = str(base / "config")
        result = subprocess.run(
            [node, "--max-old-space-size=256", cli, "setup", "--client",
             "opencode", "--skill=false", "--yes", "--json"],
            cwd=workspace, env=environment, text=True,
            capture_output=True, timeout=45,
        )
        report = json.loads(result.stdout)
        targets = [
            str(Path(action["target"]).resolve().relative_to(base))
            for action in report["plannedActions"]
            if action["kind"] == "configure_client"
        ]
        print(json.dumps({
            "case": case,
            "exit": result.returncode,
            "status": report["status"],
            "targets": targets,
            "files": sorted(str(path.relative_to(base))
                            for path in base.rglob("opencode.json")),
        }))

The independent verification ran these same three setup cases against both prepared dispatchers with fresh homes, absent OpenCode overrides and isolated temporary/cache/data/state directories.

Expected behavior

The empty and unset cases should select the same global directory and write home/.config/opencode/opencode.json, leaving the working directory empty. The explicit non-empty case should write config/opencode/opencode.json. Setup's planned path and readiness check should agree with the client's applicable global configuration path.

The XDG specification treats an empty XDG_CONFIG_HOME as unset. The inspected OpenCode client implements that rule through its xdg-basedir dependency and loads its global JSON/JSONC files from the resulting directory.

Actual behavior

Both tested builds produced the following placement results, with exit 0 and status: "ready" in every case:

case      planned target                       file created
empty     workspace/opencode/opencode.json     workspace/opencode/opencode.json
unset     home/.config/opencode/opencode.json   home/.config/opencode/opencode.json
explicit  config/opencode/opencode.json         config/opencode/opencode.json

Each empty case returned appliedActions: ["configured_opencode"]; its embedded doctor reported the registration aligned at the workspace path. The expected global file did not exist. The inspected OpenCode lookup rules select the default global directory, project opencode.json/opencode.jsonc files and .opencode directories, so the fixture's child directory named opencode is outside those default lookup locations. That client consequence is source-derived; no native OpenCode loader or MCP handshake was executed.

Evidence

  • Expected source: docs/installation.md, "Other client settings", promises registration in applicable OpenCode user configuration. The XDG Base Directory specification defines the empty-value fallback. The inspected OpenCode dependency declaration pins the dependency whose config-root rule implements that fallback. OpenCode's global path definition, global file loading and project-directory rules establish the applicable lookup locations.
  • Failure source: Independently executed public CLI setup invocations and filesystem inspection produced the placement table above on both tested builds. src/application/SupportedClients.ts:246 and :254 preserve an empty XDG value in both the OpenCode write-directory and global read-directory resolution; :278 resolves the missing-file write target against the working directory. The published package's dist/application/SupportedClients.js:116 and :119 contain the same resolution and reproduced the same result.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The explicit opencode client selection reaches the supported client definition without requiring client installation. Setup's approval, path, configuration and readback guards all admitted this isolated case. No source/configuration fix or native client run formed part of verification.

Restoration check

Repeat the three-case fixture against each affected REA build. The empty case must plan and create only home/.config/opencode/opencode.json, and the setup readiness check must identify that global registration rather than a workspace copy. The unset case must keep the same default placement. The explicit non-empty case must keep its selected config directory. No case should create an opencode directory in the working directory. Successful setup alone is insufficient; inspect both the target and the filesystem result.

Additional context

The related merged client-root correction addressed empty OpenCode directory overrides and other client-root handling. It does not establish a fix for this empty-XDG case, which remains observable in both tested builds. The report covers OpenCode registration placement only. Other clients, platforms and empty environment variables were not verified here.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: An empty XDG variable makes setup create a registration outside OpenCode's default global lookup directory while reporting readiness. The condition is bounded, the misplaced file is recoverable, and a safe workaround is demonstrated. No provider analysis or native client execution is claimed.
  • Workaround status: available
  • Workaround basis: Unsetting the empty XDG_CONFIG_HOME before setup restored the expected global registration placement on both tested builds. A non-empty absolute config root also produced the expected placement in the controls.
Originally created by @coygeek on GitHub (Oct 12, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2168 ## Summary With `XDG_CONFIG_HOME` set to an empty string and no OpenCode path overrides, `rea setup --client opencode --skill=false --yes --json` writes the registration to `opencode/opencode.json` under the caller's working directory. It exits successfully and reports `ready`, although the default global OpenCode registration file under `HOME/.config/opencode` is absent. Both tested REA builds produce this result. The unset-variable control writes the expected global file. ## Runtime or environment Source checkout c22111eca2c4bc84a084f12711f405b3e7422a40, package metadata 6.4.0, and the separately installed published `rea-agents` 6.4.0 package. Both ran on Node.js 24.21.0, macOS arm64. This Node version is in REA's documented supported runtime range; the exact pinned contributor runtime was not used. OpenCode 1.18.35 and its pinned `xdg-basedir` 5.1.0 dependency were inspected to establish the client lookup contract. A native OpenCode session was not run. Area: Setup, packaging, or upgrade. Interface: CLI. Earliest failure stage: Setup or provider admission. Target context: an empty isolated client home and a separate empty working directory; no analysis artifact or provider session is required. ## Steps to reproduce Use the tested package's `scripts/rea.mjs` dispatcher with a supported Node executable on `PATH`. Save this fixture as `reproduce.py`, then run `python3 reproduce.py /absolute/path/to/rea-agents/scripts/rea.mjs`. It uses an environment whitelist, so every inherited client-config override is absent. The three cases use different isolated homes and workspaces. ```python import json import os from pathlib import Path import shutil import subprocess import sys import tempfile node = shutil.which("node") assert node, "Put a supported Node executable on PATH" cli = str(Path(sys.argv[1]).resolve()) assert Path(cli).is_file(), "Pass the selected package's scripts/rea.mjs" with tempfile.TemporaryDirectory(prefix="rea-xdg-check-") as temporary: root = Path(temporary).resolve() for case in ("empty", "unset", "explicit"): base = root / case home = base / "home" workspace = base / "workspace" temporary_files = base / "tmp" for directory in (home, workspace, temporary_files): directory.mkdir(parents=True) environment = { "PATH": os.environ["PATH"], "HOME": str(home), "USERPROFILE": str(home), "TMPDIR": str(temporary_files), "XDG_DATA_HOME": str(base / "data"), "XDG_CACHE_HOME": str(base / "cache"), "XDG_STATE_HOME": str(base / "state"), "npm_config_cache": str(base / "npm-cache"), } if case == "empty": environment["XDG_CONFIG_HOME"] = "" if case == "explicit": environment["XDG_CONFIG_HOME"] = str(base / "config") result = subprocess.run( [node, "--max-old-space-size=256", cli, "setup", "--client", "opencode", "--skill=false", "--yes", "--json"], cwd=workspace, env=environment, text=True, capture_output=True, timeout=45, ) report = json.loads(result.stdout) targets = [ str(Path(action["target"]).resolve().relative_to(base)) for action in report["plannedActions"] if action["kind"] == "configure_client" ] print(json.dumps({ "case": case, "exit": result.returncode, "status": report["status"], "targets": targets, "files": sorted(str(path.relative_to(base)) for path in base.rglob("opencode.json")), })) ``` The independent verification ran these same three setup cases against both prepared dispatchers with fresh homes, absent OpenCode overrides and isolated temporary/cache/data/state directories. ## Expected behavior The empty and unset cases should select the same global directory and write `home/.config/opencode/opencode.json`, leaving the working directory empty. The explicit non-empty case should write `config/opencode/opencode.json`. Setup's planned path and readiness check should agree with the client's applicable global configuration path. The XDG specification treats an empty `XDG_CONFIG_HOME` as unset. The inspected OpenCode client implements that rule through its `xdg-basedir` dependency and loads its global JSON/JSONC files from the resulting directory. ## Actual behavior Both tested builds produced the following placement results, with exit 0 and `status: "ready"` in every case: ```text case planned target file created empty workspace/opencode/opencode.json workspace/opencode/opencode.json unset home/.config/opencode/opencode.json home/.config/opencode/opencode.json explicit config/opencode/opencode.json config/opencode/opencode.json ``` Each empty case returned `appliedActions: ["configured_opencode"]`; its embedded doctor reported the registration `aligned` at the workspace path. The expected global file did not exist. The inspected OpenCode lookup rules select the default global directory, project `opencode.json`/`opencode.jsonc` files and `.opencode` directories, so the fixture's child directory named `opencode` is outside those default lookup locations. That client consequence is source-derived; no native OpenCode loader or MCP handshake was executed. ## Evidence - Expected source: `docs/installation.md`, "Other client settings", promises registration in applicable OpenCode user configuration. The [XDG Base Directory specification](https://specifications.freedesktop.org/basedir-spec/latest/) defines the empty-value fallback. The inspected OpenCode [dependency declaration](https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/package.json) pins the dependency whose [config-root rule](https://github.com/sindresorhus/xdg-basedir/blob/v5.1.0/index.js#L10) implements that fallback. OpenCode's [global path definition](https://github.com/anomalyco/opencode/blob/v1.18.35/packages/core/src/global.ts#L14), [global file loading](https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/config.ts#L272) and [project-directory rules](https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/paths.ts) establish the applicable lookup locations. - Failure source: Independently executed public CLI setup invocations and filesystem inspection produced the placement table above on both tested builds. `src/application/SupportedClients.ts:246` and `:254` preserve an empty XDG value in both the OpenCode write-directory and global read-directory resolution; `:278` resolves the missing-file write target against the working directory. The published package's `dist/application/SupportedClients.js:116` and `:119` contain the same resolution and reproduced the same result. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The explicit `opencode` client selection reaches the supported client definition without requiring client installation. Setup's approval, path, configuration and readback guards all admitted this isolated case. No source/configuration fix or native client run formed part of verification. ## Restoration check Repeat the three-case fixture against each affected REA build. The empty case must plan and create only `home/.config/opencode/opencode.json`, and the setup readiness check must identify that global registration rather than a workspace copy. The unset case must keep the same default placement. The explicit non-empty case must keep its selected config directory. No case should create an `opencode` directory in the working directory. Successful setup alone is insufficient; inspect both the target and the filesystem result. ## Additional context The related [merged client-root correction](https://github.com/morluto/rea/pull/1504) addressed empty OpenCode directory overrides and other client-root handling. It does not establish a fix for this empty-XDG case, which remains observable in both tested builds. The report covers OpenCode registration placement only. Other clients, platforms and empty environment variables were not verified here. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: An empty XDG variable makes setup create a registration outside OpenCode's default global lookup directory while reporting readiness. The condition is bounded, the misplaced file is recoverable, and a safe workaround is demonstrated. No provider analysis or native client execution is claimed. - Workaround status: available - Workaround basis: Unsetting the empty XDG_CONFIG_HOME before setup restored the expected global registration placement on both tested builds. A non-empty absolute config root also produced the expected placement in the controls.
bsakaguchi 2026-10-15 18:27:38 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#714
No description provided.