Skip to content

plugin: git dep with a build/prepare script fails — compiled binary can't run its own bundled npm-cli.js #49704

Open

Description

@lnwu

Summary

opencode plugin add/plugins fails with NpmInstallFailedError: git dep preparation failed for any git dependency whose package.json has a
build, prepare, preinstall, install, postinstall, or prepack
script, or a workspaces field — regardless of git host. Root cause: the
compiled opencode binary re-invokes itself as the "npm" binary for pacote's
git-dependency prepare step, but its own CLI doesn't know how to run that,
so it just prints help and exits instead of running the prepare install.

Environment

  • opencode version: 2.0.7 (channel: latest)
  • OS: Darwin 25.6.0 (macOS, darwin arm64)
  • Shell: /bin/zsh
  • Install: global npm-style install (@opencode/cli), compiled binary

Reproduction

Fully public, no auth/network involved beyond a normal git clone:

opencode plugin add "git+ssh://[email protected]/lnwu/opencode-plugin-repro-withbuild.git"

That repo's package.json is just:

{
  "name": "opencode-plugin-repro-withbuild",
  "version": "1.0.0",
  "scripts": { "build": "echo building" }
}

Renaming that one script key to anything other than build / prepare /
preinstall / install / postinstall / prepack (and not using
workspaces) makes the exact same install succeed — nothing else about the
repo changes. This is host-independent: I originally suspected a self-hosted
git server was the trigger, but a scriptless package installs fine from that
same self-hosted host, and this scripted package fails from github.com too.
Only the presence of one of those script names (or workspaces) matters.

Root Cause

opencode plugin add installs git specifiers through npm's real
@npmcli/arborist (packages/core/src/npm.ts, Npm.reify →
arborist.reify()), not Bun's own installer. Arborist's git fetching goes
through pacote's GitFetcher. pacote/lib/git.js's #prepareDir runs an
extra step only when the cloned package.json declares one of those
scripts or workspaces — it shells out to a real npm install inside the
clone so any prepare-style script has its own deps available:

// pacote/lib/git.js
return npm(
  this.npmBin,
  [].concat(this.npmInstallCmd).concat(this.npmCliConfig),
  dir,
  { ...process.env, _PACOTE_NO_PREPARE_: noPrepare.join('\n') },
  { message: 'git dep preparation failed' }   // <- the error we see
)

pacote's npmBin defaults to the string 'npm' (resolved via PATH), but
opencode's server log shows it's configured to a path inside its own bundled
filesystem instead:

args=["/$bunfs/bin/npm-cli.js","install","--force","--cache=...", ...]

pacote/lib/util/npm.js spawns this as <process.execPath> <npmBin> <args...> (the same way you'd run node npm-cli.js install ...). That's
correct for a normal Node runtime, but process.execPath for opencode's
compiled binary is the opencode binary itself — so this ends up invoking
opencode with /$bunfs/bin/npm-cli.js install --force ... as CLI arguments.
opencode's own CLI parser doesn't recognize those as a valid subcommand, so
it just prints help and exits (confirmed in the server log, immediately
before the NpmInstallFailedError):

message="cli starting" args=["/$bunfs/bin/npm-cli.js","install","--force", ...]
message="cli process failed" cause="Cause([Fail(~effect/cli/CliError/ShowHelp: Help requested)])"
message="cli process failed" cause="Cause([Fail(NpmInstallFailedError (cause: Error: git dep preparation failed))])"

So the nested npm install never actually runs, pacote's prepare step fails,
and opencode's error wrapper discards the real cause down to the generic
NpmInstallFailedError.

Confirmed by calling the exact same Arborist.reify() opencode calls
directly (outside opencode, both as a plain script and as a bun build --compile'd binary) — it installs the same git dependency successfully in
both cases, because npmBin isn't overridden to a bunfs path there.

Expected Behavior

Per the plugins docs ("Git
repositories can use hosted shortcuts, HTTPS, or SSH..."), any git dependency
should install the same way npm/bun would handle it, including ones with
a build/prepare script.

Workaround

None needed if you control the package: avoid build/prepare/
preinstall/install/postinstall/prepack script names (rename them) and
avoid workspaces in the installed package's package.json, and ship any
build output pre-built/committed instead. Not viable for git dependencies you
don't control.

Activity

  1. vvo commented on Oct 7, 2026

    @vvo

    Still happens on 2.0.24 (macOS arm64). We ran into it with opencode plugin add github:vercel/vercel-plugin: that repo has a build script, so the install dies with git dep preparation failed. With the script renamed, the same package installs in about 3 seconds, which is the workaround we shipped in vercel/vercel-plugin#314.

    Npm.reify already passes ignoreScripts: true, so the prepare step can't do anything useful for these installs anyway. Skipping it for git deps would fix this without touching how npm gets spawned.

  2. UE-DND commented on Oct 21, 2026

    @UE-DND

    Same issue still confirmed on 2.1.2 (macOS arm64).

  3. BoysGameStudio commented on Oct 23, 2026

    @BoysGameStudio

    Same issue still confirmed on 2.1.3 (macOS arm64).

  4. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I can confirm this.

    I set up two local git repos with the same minimal plugin. One has "scripts": {"build": "echo building"} and the other uses the same script under a different key. I ran opencode plugin add git+file://<repo> for each:

    • 2.0.7 and 2.2.0 (latest release): the repo without build installs. The one with build fails with NpmInstallFailedError caused by git dep preparation failed. The log shows the binary starting itself again with /$bunfs/bin/npm-cli.js install --force ... and exiting with ShowHelp, as you describe.
    • Latest dev (950111ff2) run from source: the install fails with the same git dep preparation failed.

    The npm config opencode builds appears to point npm's binary at an npm-cli.js under its own package. That file doesn't exist when running from source. In the compiled binary, the path ends up being run by opencode itself, so pacote's prepare step can never run.

    The script below reproduces it without network access.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Reproduce #49704: git plugin with a "build" script fails to install.
    # Usage: OPENCODE="opencode" bash repro.sh   (default: opencode from PATH)
    set -u
    OPENCODE=${OPENCODE:-opencode}
    W=$(mktemp -d /tmp/opencode/r49704.XXXX)
    mk() { # $1 name, $2 script key
      d="$W/$1"; mkdir -p "$d"; cd "$d"; git init -q
      printf '{"name":"%s","version":"1.0.0","main":"index.js","scripts":{"%s":"echo building"}}\n' "$1" "$2" > package.json
      echo 'export default async () => ({})' > index.js
      git add -A; git -c user.email=a@b -c user.name=a commit -qm init; cd - >/dev/null
    }
    mk plugin-withbuild build
    mk plugin-nobuild notbuild
    mkdir -p "$W/proj"; cd "$W/proj"; git init -q
    export HOME="$W/home" XDG_CONFIG_HOME="$W/home/.config" XDG_DATA_HOME="$W/home/.local/share" XDG_CACHE_HOME="$W/home/.cache" XDG_STATE_HOME="$W/home/.local/state"
    for p in plugin-nobuild plugin-withbuild; do
      echo "=== $p ==="
      $OPENCODE plugin add "git+file://$W/$p" 2>&1 | tail -${TAILN:-5}
      echo "exit=$?"
    done
    grep -rh "npm-cli\|ShowHelp\|preparation" "$XDG_DATA_HOME" 2>/dev/null | head -5

    Run it: bash repro.sh

  5. ybbuc commented on Nov 6, 2026

    @ybbuc

    Reproduced on 2.2.5 (macOS arm64), which is newer than the versions above.

    opencode plugin add 'github:ybbuc/opencode-plugins#main::path:peak' fails with NpmInstallFailedError: git dep preparation failed. The repo is public. peak and quota both have "build": "vite build" in their package.json.

    In a local copy, renaming that key to build:dist and installing with git+file://…#main exits 0 on 2.2.5. The github: and git+https:// forms fail the same way without the rename.

    The debug log shows the same args=["/$bunfs/bin/npm-cli.js","install","--force",…] followed by ShowHelp that you describe.

  6. JoshRob297 commented on Nov 9, 2026

    @JoshRob297

    Independent reproduction, plus the detail that decides the fix.

    Environment: Debian 12 container that never had opencode, installed with the official installer (1.19.5). Also reproduced on 2.2.6.

    What the child process actually is

    strace -f -e trace=execve over opencode plugin github:...:

    • 23 git execs, zero npm execs
    • one re-exec of the opencode binary:
    execve("/root/.opencode/bin/opencode",
      ["/root/.opencode/bin/opencode", "/$bunfs/bin/npm-cli.js", "install", "--force", "--cache=/root/.npm", ...])
    

    opencode /$bunfs/bin/npm-cli.js --version prints 1.19.5 — it runs its own CLI, ignores the script, exits 0. No npm debug log is ever written, which fits npm never running.

    Not specific to one repo: github:numman-ali/opencode-openai-codex-auth (about 2.2k stars) fails identically, as does the repo in this report.

    Isolated from opencode, with the [email protected] this repo pins:

    • pacote.tarball(spec, { npmBin: "/$bunfs/bin/npm-cli.js" }) → git dep preparation failed
    • pacote.tarball(spec, { npmBin: "npm" }) → preparation passes

    So the trigger is npmBin, not the host nor the script itself.

    The consequence that decides the fix

    With @npmcli/arborist, same options as Npm.reify, on a Git dependency whose prepare builds the entry point:

    • ignoreScripts: true (any npmBin) → install succeeds, node_modules/<pkg>/dist absent
    • ignoreScripts: false, npmBin: "npm" → install succeeds, dist present

    Honouring ignoreScripts therefore removes the error but ships the plugin unbuilt: main: ./dist/index.js does not exist and OpenCode loads nothing — a silent failure replacing a loud one, for every plugin that compiles during install.

    Opened #62817 to hand pacote a real npm, falling back to the current behaviour when none is on PATH.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions