Skip to content

plugins: local plugin directory ignores package.json main and is silently skipped without a root entrypoint #52300

Closed as duplicate

Description

@rafaelsnv

Summary

When a plugins config entry points to a local plugin directory, OpenCode resolves
the entrypoint at the package root (e.g. index.ts) and ignores the package's
package.json "main" field. If no root-level entrypoint exists, the plugin is
silently skipped — no load attempt and no warning in the log.

Environment

  • opencode version: 2.0.20
  • OS: Windows 11 (Windows_NT 10.0.26200, win32 x64)
  • Terminal: VS Code terminal (TERM_PROGRAM=vscode)
  • Shell: Git Bash
  • Install/channel: latest
  • Active plugins: @tarquinen/opencode-dcp, @wakatime/opencode-wakatime, C:/repos/opencode-handoff, C:/repos/opencode-md-table-formatter

Reproduction

  1. Have a local plugin directory whose package.json is "main": "src/plugin.ts" (no root-level index.ts).
  2. Configure it: "plugins": ["C:/path/to/plugin-dir"].
  3. Restart the service.
  4. The plugin is never loaded and nothing is logged about it.
  5. Add a root index.ts re-exporting the plugin (export { default } from "./src/plugin") — the plugin then loads.

Related: pointing the config entry directly at the entrypoint file
(".../src/plugin.ts") is rejected with a warning:
WARN message="configured plugin path must be a directory" target=C:/repos/opencode-handoff/src/plugin.ts
even though the V2 plugin migration guide documents local file paths
({"package": "./plugin/local.ts", ...}) as supported.

Expected Behavior

The directory loader should honor package.json "main" (or at least log a clear
warning when no resolvable entrypoint is found) instead of silently dropping the
plugin. File-path entries should load per the plugin migration guide.

Actual Behavior

Plugin is silently not loaded; no error, no warning. Only symptom is the plugin
missing from the msg="loading plugin" log lines.

Additional Context

Workaround: add a root-level index.ts re-exporting the default export from the
real entrypoint. Also note the "must be a directory" restriction conflicts with
the documented ["./plugin/local.ts", {...}] local-plugin form in the V1→V2
migration guide.

Activity

  1. github-actions commented on Sep 30, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    • #46551: Reports local plugin file paths being rejected and directories without an index entrypoint being dropped, closely matching both behaviors here.
    • #41530: Reports local directory packages not being discovered through their package.json entrypoints, similar to the main resolution issue here.
  2. vvo commented on Oct 7, 2026

    @vvo

    Same on 2.0.24 (macOS). It ignores exports too, not only main: a local directory with "exports": { ".": "./x.mjs" } and no index.* is skipped without any log line.

    The same package installed with opencode plugin add git+file:///path/to/repo loads fine, because package installs do resolve exports. So a plugin can work when installed and still be invisible when you point plugins at its checkout to test it.

  3. TateWalker commented on Oct 21, 2026

    @TateWalker

    Hit the same bug class on 2.1.2 (Linux, release channel), but with a broader trigger condition: our directory does have a root-level entrypoint and a valid exports map, and it's still intermittently and silently skipped.

    Environment

    • opencode version: 2.1.2
    • OS: Linux 6.12.88+deb13-amd64 (linux x64)
    • Shell: /bin/bash
    • Install/channel: latest (release)
    • Active plugins: local directory plugin (./plugins/caveman, relative to ~/.config/opencode/opencode.json), superpowers@git+https://github.com/obra/superpowers.git

    Reproduction

    1. Local plugin directory ~/.config/opencode/plugins/caveman/ with:
      • plugin.js — valid V2 module: export default Plugin.define({ id: "caveman", setup(ctx) { ... } })
      • package.json with "type": "module", "main": "plugin.js", "exports": { ".": "./plugin.js" }
    2. Config: "plugins": ["./plugins/caveman", "superpowers@git+https://github.com/obra/superpowers.git"]
    3. opencode service restart, then opencode api get /api/plugin.

    Expected Behavior

    Plugin appears in GET /api/plugin with state.status: "active", same as the sibling superpowers package entry, which loads reliably on every restart from the same config.

    Actual Behavior

    • No msg="loading plugin" log line is ever emitted for this entry — not even an attempt, let alone a warning or state.status: "failed".
    • The entry is entirely absent from GET /api/plugin (only superpowers shows up).
    • Behavior is non-deterministic across otherwise-identical restarts with unmodified content: across ~10 clean opencode service restart cycles with the exact same files, it loaded successfully a small number of times and silently failed most of the time. No config, content, or path change correlated with success vs. failure in our testing.
    • We isolated variables extensively trying to find a deterministic trigger and could not:
      • Copying the identical plugin.js/package.json content to a brand-new directory under /tmp or directly under $HOME (no dot-prefixed ancestor) sometimes loaded and sometimes didn't, with no code or config difference between attempts.
      • Pointing the entry at the plugin's own git-tracked path inside ~/.config/opencode/plugins/ behaved the same as pointing it at copies elsewhere — ruling out any special-casing of that specific directory.
      • A trivial throwaway plugin (export default Plugin.define({ id: "x", setup() { console.log("loaded") } })) at a fresh path did load successfully on at least one occasion via hot-reload after an opencode.json edit, without a full service restart — suggesting the reconciliation/hot-reload path for local directory plugins is itself unreliable, separate from any package.json resolution issue.
    • By contrast, the superpowers package-form entry in the very same config loaded successfully on 100% of the restarts we tried (confirmed via matching msg="loading plugin" id=superpowers@... entrypoint=file://.../superpowers.js).

    Additional Context

    • This matches the symptom @vvo described in this thread (directory + exports, no log line at all) but in our case a root-level index-equivalent file is present and matches package.json "main"/"exports" exactly, so the "missing root entrypoint" explanation in the issue summary doesn't account for our failures.
    • No workaround found other than retrying opencode service restart repeatedly until GET /api/plugin happens to show the plugin as active, which is not viable for any workflow that depends on the plugin's hooks firing reliably (in our case, a plugin that tracks mode state via ctx.session.hook("prompt", ...) / ctx.session.hook("context", ...)).
    • Given the apparent non-determinism independent of path, content validity, and presence/absence of a root entrypoint, this may point to a race in plugin discovery/reconciliation rather than purely an exports resolution gap — happy to provide OPENCODE_LOG_LEVEL=DEBUG logs from a failing run if useful.
  4. heliowap commented on Oct 25, 2026

    @heliowap

    Still reproduces on 2.1.4 (macOS, Darwin 25.5.0 darwin arm64, release channel). Adding a data point that narrows the trigger, because this is easy to misdiagnose as a path-form problem — I did exactly that for an hour before reading the resolver.

    The path form is not a factor

    One config, five plugin directories, each with a distinct id. Four have a root index.ts; the fifth declares its entry only through package.json exports:

    {
      "plugins": [
        "./rel",
        "/abs/path/absolute",
        "file:///abs/path/urlplug",
        "./symlinked",   // symlink to a directory outside the project
        "./exportsonly"  // no root index; "exports": { ".": "./src/index.ts" }
      ]
    }

    Each plugin's setup logs its own id. Result:

    LOADED absplug
    LOADED linkplug
    LOADED relplug
    LOADED urlplug
    

    Relative paths, absolute paths, file:// URLs and symlinked directories all load. Only exportsonly is missing, and it is completely silent: with --print-logs --log-level all, grep -ci 'exportsonly|exportsplug' returns 0, while the eight unrelated plugin entries in the same run each emit their msg="loading plugin" line.

    Where it happens

    Line references are from a source checkout at 0413ce2a28; the behaviour above was verified against the released 2.1.4 binary.

    packages/plugin/src/host.ts:20-24 builds the specifier two different ways:

    const specifier = target.name
      ? [target.name, subpath].filter(Boolean).join("/")   // installed package: node resolution, exports honored
      : path.resolve(target.directory, subpath || "index") // local directory: a path, package.json never read

    A configured directory has no name, so resolution is <dir>/server and then <dir>/index as filesystem paths. The directory's own package.json is never opened, which is why main and exports are both ignored — as @vvo describes above — and why the same package loads when installed and vanishes when you point plugins at its checkout.

    The silence comes from packages/core/src/config/plugin/source.ts:167:

    if (!entrypoints.server) return []

    The entry is dropped from the operation list, with no log at any level. #46551's Additional Context mentions a configured plugin directory has no index entrypoint warning for this branch; that string does not exist anywhere in the tree at 0413ce2a28, so the branch appears to have lost its diagnostic since then.

    On the intermittent case

    @TateWalker's non-determinism looks like a different problem. Here the behaviour was deterministic in both directions: with the entry only in exports, it failed every time; after adding a root index.ts that re-exports src/index.ts, it loaded on every run — roughly fifteen runs over an hour, mixing opencode run --standalone and a private opencode serve, each in a fresh temporary project. That said, none of my runs went through opencode service restart on the shared service, so this does not rule out a race in the path you are hitting.

    Suggestion

    The fix for the resolution gap is the one already in the title of this issue. Independently of it, a single warning at source.ts:167 naming the target and the subpaths that were tried (<dir>/server, <dir>/index) would turn this class of bug from an hour of bisecting into one line of log output.

  5. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report and the workaround! I think this is a duplicate of #58647.

    Related issues, ordered from most to least likely to be the same problem:

    Some context on main: in v2, a local plugin directory is resolved through its package.json exports, which only applies when the package.json also has a name. Otherwise OpenCode looks for the usual files at the root (server/index); main isn't read. So instead of a root index.ts, you can also add "name" and "exports": { ".": "./src/plugin.ts" } to your package.json. The current plugin migration guide also says configured local paths must be directories, and that a configured file is skipped with a warning.

    The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.

  6. TateWalker commented on Oct 28, 2026

    @TateWalker

    Update: fixed for us as of 2.2.0.

    After upgrading to 2.2.0, the exact same config/directory that failed non-deterministically on every prior version (2.1.2, 2.1.3) has now loaded cleanly on 5 consecutive opencode service restart cycles, with zero failures — a complete reversal from the earlier ~10-restart test where it mostly failed.

    No changelog entry we could find mentions this specifically, so it's possible it was fixed incidentally alongside other plugin-reconciliation work between 2.1.3 and 2.2.0. Flagging in case it's useful signal for whoever eventually chases this down, but as far as our case goes this is resolved.

  7. opencodejr commented on Oct 29, 2026

    @opencodejr
    Contributor

    Closing this as a duplicate of #58647, since there's been no reply in the last day saying how this is different. That issue covers local plugin directories being skipped with nothing in the log. The rejection of file-path entries (configured plugin path must be a directory) is tracked in #46551.

    Until then, you can keep the root index.ts re-export, or add "name" and "exports": { ".": "./src/plugin.ts" } to the plugin's package.json instead of relying on main. If your problem is different from those issues, reply here and we can reopen.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions