Repository navigation
Description
Description
On Linux, plugin installation fails with NpmInstallFailedError when XDG_CACHE_HOME / XDG_CONFIG_HOME point to symlink paths, even though the symlinks resolve to normal writable directories.
The same plugin install succeeds immediately if the same physical directories are passed as their real paths instead of via symlink.
This looks different from the proxy-related Arborist regressions in #21472 / #22454. In this case there is no proxy involved, and the reproducer is only about symlinked XDG paths.
I noticed this regression recently without changing my symlink setup. Older OpenCode versions did not have this problem in my environment.
OpenCode Version
1.4.11
Environment
- OS: Linux
- Reproduced on: April 18, 2026
- No proxy variables involved
- Reproduces with a generic public plugin (
opencode-wakatime), so this is not specific tooh-my-openagent
Minimal Reproduction
set -euo pipefail
root=/tmp/opencode-symlink-repro
rm -rf "$root"
mkdir -p "$root/real-cache" "$root/real-config" "$root/data" "$root/state"
ln -s "$root/real-cache" "$root/link-cache"
ln -s "$root/real-config" "$root/link-config"
export XDG_CACHE_HOME="$root/link-cache"
export XDG_CONFIG_HOME="$root/link-config"
export XDG_DATA_HOME="$root/data"
export XDG_STATE_HOME="$root/state"
opencode plugin opencode-wakatimeActual Behavior
The install fails with:
Install failed
Could not install "opencode-wakatime"
NpmInstallFailedError
Control Case
Using the exact same directories via real paths succeeds:
set -euo pipefail
root=/tmp/opencode-realpath-control
rm -rf "$root"
mkdir -p "$root/cache" "$root/config" "$root/data" "$root/state"
export XDG_CACHE_HOME="$root/cache"
export XDG_CONFIG_HOME="$root/config"
export XDG_DATA_HOME="$root/data"
export XDG_STATE_HOME="$root/state"
opencode plugin opencode-wakatimeThis succeeds and installs the plugin normally.
Additional Notes
I also debugged this locally by bypassing my wrapper and invoking the OpenCode binary directly. The behavior was the same:
- symlink XDG path -> plugin install fails with
NpmInstallFailedError - realpath XDG path -> plugin install succeeds
A likely clue is that the failure started after the 1.4.x plugin-installer changes that switched plugin installs onto @npmcli/arborist inside the Bun runtime, but this report is specifically about symlinked XDG/cache paths.
If helpful, I can provide a smaller debug trace, but the shell repro above already reproduces it consistently on my side.
Activity
PhillipChaffee commentedon Sep 27, 2026
Still reproducing on 1.18.32 / macOS (arm64), and I can now isolate the trigger to the cache symlink itself (no config symlink needed) and show the lockfile evidence behind the
NpmInstallFailedError.Repro (cache symlink only; config/data on real paths)
What the failed install leaves behind
Arborist reify materializes the full tree, then throws. The tree's
node_modules/.package-lock.jsoncomes out with mangled keys —path.relativeoutput between the root as given (symlink form) and the realpath'd node paths:A clean install keys
"node_modules/acorn"/"node_modules/ultraopen".Isolation matrix (1.18.32, macOS, npm plugin
ultraopen)~/.cache, no redirect)/tmp(symlink →/private/tmp)NpmInstallFailedError(deterministic, 5/5 across sessions), mangled keys$HOME-relative symlinkRetry masks it
A second
opencode plugin ultraopen -gagainst the same cache exits 0 without refetching — it short-circuits on the materialized (rejected) tree, so the first-install failure is easy to miss.Harness note: e2e harnesses that XDG-isolate into
/tmpscratch homes will hit this on the first plugin install; pointingXDG_CACHE_HOMEat a symlink-free path works fine.