You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Global plugins not loaded when ~/.config/opencode is a symlink (inotify_add_watch "Not a directory" aborts config load) #55119
On Linux, when the global config directory ~/.config/opencode is a symlink (e.g. into a dotfiles repo), plugins listed in the global opencode.jsonplugins array are never loaded. The config file itself is read (the TUI sees model, providers, etc.), but plugin reconciliation from the global config silently fails, so a provider registered by a global plugin never appears.
Root cause: the config watcher calls inotify_add_watch on the symlink path, and the kernel rejects a symlink with ENOTDIR. The failure aborts loading of the global config's plugins. The service log shows:
level=ERROR message="failed to subscribe" directory=/home/<user>/.config/opencode
cause="Error: inotify_add_watch on '/home/<user>/.config/opencode' failed: Not a directory
at Config.reconcileWatches (...)
at ServerProcess.start (...)"
The standalone / local .opencode/plugin/ discovery path works, so the plugin code itself is fine — only the global plugins config path is affected.
Workaround: set XDG_CONFIG_HOME to the resolved real directory (the parent of realpath ~/.config/opencode) so the watch target is a real directory, not the symlink.
Expected: a symlinked config directory should be watched/followed (resolve to the real path before inotify_add_watch), and global plugins should load.
Plugins
Local re-export plugin registering a custom provider via ctx.provider.transform + ctx.aisdk.hook, loaded through the global plugins array.
OpenCode version
2.0.24
Steps to reproduce
Make ~/.config/opencode a symlink to a real directory elsewhere (e.g. ~/.dotfiles/.../opencode).
Add a plugins entry to the global opencode.json that registers a provider.
Start opencode and query opencode api get /api/provider/<id> → ProviderNotFoundError; the log shows the inotify_add_watch ... Not a directory error.
Thanks for the detailed report. I tried this on 2.0.24 on Linux and couldn't reproduce plugins failing to load.
What I tried: temporary XDG directories with $XDG_CONFIG_HOME/opencode symlinked to a real directory. The global opencode.json there had "plugins": ["./myplugin"], pointing at a local plugin directory that uses @opencode/plugin. Then I ran opencode serve --print-logs.
That error only stops config edits from being picked up live. Right after it, the log shows loading plugin, the plugin's setup ran, and its transform showed up in the API. A non-symlinked directory gave the same result.
In the code, a failed watch is only logged, and watches run separately from config and plugin loading. So the watch error by itself shouldn't stop global plugins from loading.
So the ProviderNotFoundError may have a different cause. Some things to check:
Search your log for failed to load plugin or configured plugin path must be a directory. In 2.0.24, a plugins entry must point to the plugin directory, not a file. The plugin also needs @opencode/plugin to resolve from that directory, or it fails with Cannot find package '@opencode/plugin' (see plugins: local plugins cannot resolve "@opencode/plugin" import (2.0.12) #50434).
Please share the exact plugins entry from your global opencode.json, your plugin's setup (in particular the ctx.provider.transform part), and any log lines that mention your plugin or provider.
Does the same plugin register the provider when the config directory is a real directory? You mentioned the XDG_CONFIG_HOME workaround fixes it. Did that workaround change anything else, such as how the plugin path resolves?
Thanks — you were right to push back. My original "symlinked config dir / inotify" root cause was wrong. I applied the XDG_CONFIG_HOME workaround so the config dir resolves to a real directory (no more inotify_add_watch … Not a directory), restarted the service, and the provider was stillProviderNotFoundError. So the watch error is cosmetic here, exactly as you said. I kept digging and found the real cause.
Real root cause: absolute-directory plugins entry mis-resolves under Bun
A global plugins entry that is an absolute path to a plugin directory is dropped silently. resolveEntryPoint (core npm.ts) resolves the directory's entrypoint with:
entrypoint=typeofBun!=="undefined"
? import.meta.resolve(name,dir)// Bun path (what opencode runs)
: pathToFileURL(createRequire(path.join(dir,"package.json")).resolve(name)).href
For an absolute directory, the two branches disagree:
# Bun (the branch opencode actually uses)
import.meta.resolve("/abs/opencode-kiro") -> file:///abs/opencode-kiro # the directory itself
# Node branch
createRequire(".../package.json").resolve(dir) -> file:///abs/opencode-kiro/src/plugin.ts # correct manifest entry
So under Bun the entrypoint comes back as the directory, not the package.jsonmain/exports target. In the config-plugin scan the entry is then discarded (the resolved server equals the directory, so the containment check / subsequent handling drops it), and the whole branch is wrapped so the failure is swallowed — there is no loading plugin line and no error for the entry. That matches what I see: the config is read with plugins: ["/abs/opencode-kiro"], but nothing is ever attempted for it.
What I verified
Plugin setup is correct: driven with a mock ctx it runs ctx.provider.transform and editor.adds kiro-acp + model kiro, no throw.
The directory is a valid package: package.json present, @opencode/plugin resolvable from it, main/exports → ./src/plugin.ts.
The service reads the entry (/api/config shows plugins: ["/abs/opencode-kiro"]) but logs no load attempt for it; /api/plugin reports 0 non-builtin plugins; /api/provider/kiro-acp → ProviderNotFoundError.
import.meta.resolve(absDir) under Bun returns the dir; require.resolve(absDir) returns src/plugin.ts. Only the Bun branch is broken.
This looks distinct from the known cases (#50434Cannot find package, the "must be a directory" rule for file entries, the symlink hot-reload #54933). Here the entry is a valid directory and there is no error at all — it's the Bun import.meta.resolve directory-resolution that silently yields the wrong entrypoint.
Repro
A plugin package dir at an absolute path, with package.json"main"/"exports" → a child entry (e.g. ./src/plugin.ts), @opencode/plugin installed in it.
Global opencode.json: "plugins": ["/abs/path/to/plugin-dir"].
Start opencode (Bun). The plugin never loads: no loading plugin log, no error, provider absent.
Workaround that works
Dropping the absolute-dir entry and using config-dir file discovery instead — <configdir>/plugin/<name>.ts re-exporting the plugin's entry — loads correctly, because that path resolves the file target directly and never goes through the directory import.meta.resolve.
Thanks for digging further. I can confirm this on 2.0.24, and it still happens on the latest dev (c1615b1).
What I ran: a global opencode.json with "plugins": ["<path to plugin dir>"], where the plugin directory has @opencode/plugin installed and a package.json whose main/exports point to ./src/plugin.ts. The plugin's setup never runs, and the log has no loading plugin line, warning or error. Moving the same file to <plugin dir>/index.ts makes it load. The script below reproduces it.
One correction to the cause: it doesn't depend on the path being absolute. A relative ./myplugin entry behaves the same. When a configured plugin is a directory, OpenCode only looks for <dir>/server or <dir>/index. It doesn't read package.jsonmain/exports, so it finds no server entrypoint and skips the plugin silently.
Until this is fixed, there are two workarounds:
the re-export file in <configdir>/plugin/ that you already use
an index.ts at the root of the plugin directory that re-exports ./src/plugin.ts
The symlink and inotify_add_watch error is a separate issue, tracked in #54933.
I’m working on the directory entrypoint issue clarified above. The current resolver still only checks server and index for local directories. I’ll make local packages honor their main/exports metadata while preserving the existing server entrypoint precedence and import conditions.
Activity
opencode-agent commentedon Oct 13, 2026
Thanks for the detailed report. I tried this on 2.0.24 on Linux and couldn't reproduce plugins failing to load.
What I tried: temporary XDG directories with
$XDG_CONFIG_HOME/opencodesymlinked to a real directory. The globalopencode.jsonthere had"plugins": ["./myplugin"], pointing at a local plugin directory that uses@opencode/plugin. Then I ranopencode serve --print-logs.inotify_add_watch … Not a directoryerror does appear. It is already tracked in config: global config directory watch fails when ~/.config/opencode is a symlink (inotify ENOTDIR), so edits are never hot-reloaded #54933, and open PR fix(core): rebase directory watcher events across symlinked roots #54093 looks like it addresses it.loading plugin, the plugin'ssetupran, and its transform showed up in the API. A non-symlinked directory gave the same result.pluginsfrom loading.So the
ProviderNotFoundErrormay have a different cause. Some things to check:failed to load pluginorconfigured plugin path must be a directory. In 2.0.24, apluginsentry must point to the plugin directory, not a file. The plugin also needs@opencode/pluginto resolve from that directory, or it fails withCannot find package '@opencode/plugin'(see plugins: local plugins cannot resolve "@opencode/plugin" import (2.0.12) #50434).pluginsentry from your globalopencode.json, your plugin'ssetup(in particular thectx.provider.transformpart), and any log lines that mention your plugin or provider.XDG_CONFIG_HOMEworkaround fixes it. Did that workaround change anything else, such as how the plugin path resolves?Related issues:
Armadillidiid commentedon Oct 13, 2026
Thanks — you were right to push back. My original "symlinked config dir / inotify" root cause was wrong. I applied the
XDG_CONFIG_HOMEworkaround so the config dir resolves to a real directory (no moreinotify_add_watch … Not a directory), restarted the service, and the provider was stillProviderNotFoundError. So the watch error is cosmetic here, exactly as you said. I kept digging and found the real cause.Real root cause: absolute-directory
pluginsentry mis-resolves under BunA global
pluginsentry that is an absolute path to a plugin directory is dropped silently.resolveEntryPoint(corenpm.ts) resolves the directory's entrypoint with:For an absolute directory, the two branches disagree:
So under Bun the entrypoint comes back as the directory, not the
package.jsonmain/exportstarget. In the config-plugin scan the entry is then discarded (the resolved server equals the directory, so the containment check / subsequent handling drops it), and the whole branch is wrapped so the failure is swallowed — there is noloading pluginline and no error for the entry. That matches what I see: the config is read withplugins: ["/abs/opencode-kiro"], but nothing is ever attempted for it.What I verified
setupis correct: driven with a mock ctx it runsctx.provider.transformandeditor.addskiro-acp+ modelkiro, no throw.package.jsonpresent,@opencode/pluginresolvable from it,main/exports→./src/plugin.ts./api/configshowsplugins: ["/abs/opencode-kiro"]) but logs no load attempt for it;/api/pluginreports 0 non-builtin plugins;/api/provider/kiro-acp→ProviderNotFoundError.import.meta.resolve(absDir)under Bun returns the dir;require.resolve(absDir)returnssrc/plugin.ts. Only the Bun branch is broken.This looks distinct from the known cases (#50434
Cannot find package, the "must be a directory" rule for file entries, the symlink hot-reload #54933). Here the entry is a valid directory and there is no error at all — it's the Bunimport.meta.resolvedirectory-resolution that silently yields the wrong entrypoint.Repro
package.json"main"/"exports"→ a child entry (e.g../src/plugin.ts),@opencode/plugininstalled in it.opencode.json:"plugins": ["/abs/path/to/plugin-dir"].loading pluginlog, no error, provider absent.Workaround that works
Dropping the absolute-dir entry and using config-dir file discovery instead —
<configdir>/plugin/<name>.tsre-exporting the plugin's entry — loads correctly, because that path resolves the file target directly and never goes through the directoryimport.meta.resolve.Env: opencode 2.0.24, Linux, Bun runtime.
opencode-agent commentedon Oct 13, 2026
Thanks for digging further. I can confirm this on 2.0.24, and it still happens on the latest
dev(c1615b1).What I ran: a global
opencode.jsonwith"plugins": ["<path to plugin dir>"], where the plugin directory has@opencode/plugininstalled and apackage.jsonwhosemain/exportspoint to./src/plugin.ts. The plugin'ssetupnever runs, and the log has noloading pluginline, warning or error. Moving the same file to<plugin dir>/index.tsmakes it load. The script below reproduces it.One correction to the cause: it doesn't depend on the path being absolute. A relative
./mypluginentry behaves the same. When a configured plugin is a directory, OpenCode only looks for<dir>/serveror<dir>/index. It doesn't readpackage.jsonmain/exports, so it finds no server entrypoint and skips the plugin silently.Until this is fixed, there are two workarounds:
<configdir>/plugin/that you already useindex.tsat the root of the plugin directory that re-exports./src/plugin.tsThe symlink and
inotify_add_watcherror is a separate issue, tracked in #54933.Reproduction script (repro2.sh)
Run it:
bash repro2.shDante-dan commentedon Oct 14, 2026
I’m working on the directory entrypoint issue clarified above. The current resolver still only checks
serverandindexfor local directories. I’ll make local packages honor theirmain/exportsmetadata while preserving the existing server entrypoint precedence and import conditions.