anomalyco / anomalyco/opencode

[BUG]: checkPluginCompatibility silently skips when a plugin's package.json cannot be read

Open
#43,094 1 comment 0 reactions 1 assignee View on GitHub

@kitlangton is already working on this.

Since Aug 17, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Describe the bug

packages/opencode/src/plugin/shared.ts:194-205 validates engines.opencode against the running version and throws a precise error on mismatch:

const range = engines.opencode
if (typeof range !== "string") return
if (!semver.satisfies(opencodeVersion, range)) {
  throw new Error(`Plugin requires opencode ${range} but running ${opencodeVersion}`)
}

But the package read on the line above is readPluginPackage(target).catch(() => undefined). An unreadable or malformed package.json yields undefined, the compatibility check is skipped entirely, and the plugin loads with no version gate and no signal.

This is a compatibility gate whose own read failure disables the gate. The failure it exists to catch — a plugin running against an incompatible host — then proceeds silently, and the operator sees a plugin that loaded "fine" until something it depends on turns out to be missing. The cost is highest at a major-version boundary, which is exactly when the gate is load-bearing.

Also worth stating in the same issue: the check no-ops when semver.major(opencodeVersion) === 0 or the version is not valid semver, so dev builds are ungated. That may well be intentional, but it is currently implicit.

Expected behavior

An unreadable package.json for a plugin that is being loaded should surface — a warning naming the plugin at minimum, or a refusal to load. A gate that could not read its input should not report the same outcome as a gate that read its input and passed.

Adjacent-correct behavior in the same function family, three lines below: every malformed field throws precisely (invalid server export, must default export either server() or tui(), not both). The field validation fails closed; the read failure fails open.

Steps to reproduce
  1. Install a plugin whose package.json is unreadable or malformed (bad permissions, invalid JSON) while its entry module still resolves.
  2. Load opencode with that plugin configured.
  3. Observe the plugin loads with no compatibility check performed and no warning emitted — indistinguishable from a plugin that declared a satisfied engines.opencode range.

Note: these steps are derived from the mechanism rather than from a recorded run — the finding came from reading the code path.

Environment
  • Version: reproduces on current dev
  • Platform: Linux (not platform-specific)
Additional context

Found during an audit of fail-open paths — places where a catch inside a guard or cleanup path lets the guarded failure through silently — applied to this repo's plugin loader.

The heuristic that produced it may be reusable: the defect usually sits beside a path that already handles the same condition correctly, so diffing a path against its own siblings surfaces the asymmetry. Here the sibling is the field validation in the same function.

This matters more than usual right now because engines.opencode is the one mechanism that converts a silent plugin non-load into a thrown error naming the plugin — plugin authors are being advised to declare it precisely so an incompatible host announces itself rather than dropping the plugin's tools and guards silently. A gate that skips on an unreadable manifest undercuts that advice in the case where it is needed most.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.