Skip to content

Plugin install fails when XDG cache/config directories are symlinks #23312

Closed

Description

@yanhao98

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 to oh-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-wakatime

Actual 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-wakatime

This 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

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Apr 18, 2026
  2. PhillipChaffee commented on Sep 27, 2026

    @PhillipChaffee

    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)

    base="$HOME/oc-repro"
    rm -rf "$base"; mkdir -p "$base/cache-real" "$base/data" "$base/config"
    ln -s "$base/cache-real" "$base/cache-link"
    export XDG_CACHE_HOME="$base/cache-link"
    export XDG_DATA_HOME="$base/data" XDG_CONFIG_HOME="$base/config"
    opencode plugin ultraopen -g   # any npm plugin; exit 1, NpmInstallFailedError

    What the failed install leaves behind

    Arborist reify materializes the full tree, then throws. The tree's node_modules/.package-lock.json comes out with mangled keys — path.relative output between the root as given (symlink form) and the realpath'd node paths:

    [
      "../../../../cache-real/opencode/packages/ultraopen@latest/node_modules/acorn",
      "../../../../cache-real/opencode/packages/ultraopen@latest/node_modules/ultraopen"
    ]

    A clean install keys "node_modules/acorn" / "node_modules/ultraopen".

    Isolation matrix (1.18.32, macOS, npm plugin ultraopen)

    XDG_CACHE_HOME Result
    default (~/.cache, no redirect) installs (2/2)
    redirected, symlink-free path installs, canonical keys
    redirected, path under /tmp (symlink → /private/tmp) NpmInstallFailedError (deterministic, 5/5 across sessions), mangled keys
    redirected, $HOME-relative symlink same failure, keys above

    Retry masks it

    A second opencode plugin ultraopen -g against 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 /tmp scratch homes will hit this on the first plugin install; pointing XDG_CACHE_HOME at a symlink-free path works fine.

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

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions