Description
OpenCode manages plugins through two paths that are poorly synchronized:
Path 1: opencode plugin install --global — Uses @npmcli/arborist reify with save: true to append the plugin to package.json. This is append-only: it never removes entries for plugins that were previously installed but are no longer in config.
Path 2: Direct config edit + restart — Users edit opencode.json/tui.json directly and restart. On startup, Loader.resolve() calls Npm.add() for each plugin individually, which calls reify() per-plugin (no batch). If node_modules is missing or stale, each plugin triggers a separate npm resolve+install.
Over time, package.json drifts from the actual plugin declarations in config:
- Stale deps remain — e.g. replacing
@tarquinen/opencode-dcp with open-mem in config leaves the old entry in package.json forever
- Missing deps — plugins added directly to config JSON are never added to
package.json; startup must resolve them from scratch each time
- Slow startup — each plugin calls
Npm.add() → individual reify(), N plugins = N sequential npm operations instead of one batch install
- No
plugin remove command — there is no way to cleanly uninstall a plugin from both config and package.json
Reproduction
Scenario A (CLI path — stale entries):
opencode plugin install --global some-plugin@1.0.0 → package.json gets {"some-plugin": "1.0.0"}
opencode plugin install --global replacement@2.0.0 → config is updated, but some-plugin stays in package.json
- Repeat →
package.json accumulates orphaned deps
Scenario B (config edit path — missing entries + slow startup):
- Edit
~/.config/opencode/opencode.json: add "new-plugin@^1.0.0" to plugin array
- Delete
node_modules (or start fresh)
- Restart opencode → startup takes seconds as each plugin is resolved individually via
Npm.add() → reify()
- After startup,
package.json may still not reflect all declared plugins (only the ones that needed install)
Root Cause (source analysis)
| File |
Behavior |
packages/core/src/npm.ts L78-108 |
reify() uses save: true, saveType: "prod" — append-only, no prune |
packages/core/src/npm.ts L138-186 |
install() checks declared vs locked but never removes stale entries |
packages/opencode/src/plugin/install.ts |
patchPluginList() only modifies config JSON (opencode.json/tui.json), never package.json |
packages/opencode/src/plugin/loader.ts L94 |
Each plugin calls resolvePluginTarget() → Npm.add() → individual reify() |
packages/opencode/src/cli/cmd/plug.ts |
No remove/uninstall subcommand for plugins |
Suggested Fix
A. Sync package.json from config on startup — Before the per-plugin resolve loop, rebuild package.json dependencies to match the union of all plugin specifiers in opencode.json + tui.json, then run a single batch reify(). This would:
- Remove stale entries
- Add missing entries
- Replace N individual reify calls with one batch install
B. Add opencode plugin remove <module> command — Removes plugin from config JSON and prunes package.json entry. Gives users explicit control.
Workaround
Users experiencing long "loading plugins" delays on startup can manually clean up:
# Regenerate package.json from config, then batch install (eliminates the slow per-plugin resolve)
cd ~/.config/opencode
rm -rf node_modules package.json package-lock.json
# Restart opencode — it will regenerate from config declarations
Environment
- OpenCode: v1.15.13
- OS: macOS
- Config: global (opencode.json + tui.json)
- 8 plugins across opencode.json + tui.json
Description
OpenCode manages plugins through two paths that are poorly synchronized:
Path 1:
opencode plugin install --global— Uses@npmcli/arboristreify withsave: trueto append the plugin topackage.json. This is append-only: it never removes entries for plugins that were previously installed but are no longer in config.Path 2: Direct config edit + restart — Users edit
opencode.json/tui.jsondirectly and restart. On startup,Loader.resolve()callsNpm.add()for each plugin individually, which callsreify()per-plugin (no batch). Ifnode_modulesis missing or stale, each plugin triggers a separate npm resolve+install.Over time,
package.jsondrifts from the actual plugin declarations in config:@tarquinen/opencode-dcpwithopen-memin config leaves the old entry inpackage.jsonforeverpackage.json; startup must resolve them from scratch each timeNpm.add()→ individualreify(), N plugins = N sequential npm operations instead of one batch installplugin removecommand — there is no way to cleanly uninstall a plugin from both config andpackage.jsonReproduction
Scenario A (CLI path — stale entries):
opencode plugin install --global some-plugin@1.0.0→package.jsongets{"some-plugin": "1.0.0"}opencode plugin install --global replacement@2.0.0→ config is updated, butsome-pluginstays inpackage.jsonpackage.jsonaccumulates orphaned depsScenario B (config edit path — missing entries + slow startup):
~/.config/opencode/opencode.json: add"new-plugin@^1.0.0"topluginarraynode_modules(or start fresh)Npm.add()→reify()package.jsonmay still not reflect all declared plugins (only the ones that needed install)Root Cause (source analysis)
packages/core/src/npm.tsL78-108reify()usessave: true, saveType: "prod"— append-only, no prunepackages/core/src/npm.tsL138-186install()checks declared vs locked but never removes stale entriespackages/opencode/src/plugin/install.tspatchPluginList()only modifies config JSON (opencode.json/tui.json), neverpackage.jsonpackages/opencode/src/plugin/loader.tsL94resolvePluginTarget()→Npm.add()→ individualreify()packages/opencode/src/cli/cmd/plug.tsremove/uninstallsubcommand for pluginsSuggested Fix
A. Sync
package.jsonfrom config on startup — Before the per-plugin resolve loop, rebuildpackage.jsondependencies to match the union of all plugin specifiers inopencode.json+tui.json, then run a single batchreify(). This would:B. Add
opencode plugin remove <module>command — Removes plugin from config JSON and prunespackage.jsonentry. Gives users explicit control.Workaround
Users experiencing long "loading plugins" delays on startup can manually clean up:
Environment