anomalyco / anomalyco/opencode

Plugin config hooks can mutate process-shared config state via shallow-merged nested objects

Open
#41,916 0 comments 0 reactions 1 assignee View on GitHub

@rekram1-node is already working on this.

Since Aug 12, 2026.

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

Description

Summary

Workspace-level config state shares nested objects (e.g. mcp) with the process-global config cache when the workspace has no project-level value for that key (mergeDeep shallow reference). Plugin config hooks receive that mutable object and can write into it, leaking state into every other workspace on the same server.

Details

  • Config.loadInstanceState merges the global config into the per-workspace result via mergeConfig (remeda mergeDeep). For nested keys absent on the workspace side, the merged value is the same object reference as the global cache's object (Config.getGlobal / cachedGlobal).
  • Plugin config hooks are invoked with cfg = config.get() — the workspace's live config-state object — at packages/opencode/src/plugin/index.ts (hook.config?.(cfg)). Plugins commonly mutate it in place (config.mcp[name] ??= {...}).
  • When such a plugin runs on a workspace that has no project-level mcp config, the write lands on the shared global mcp object. Every other workspace attached to the same server (opencode serve with multiple attached TUIs) then sees those MCP servers in its config//mcp status — disabled when the other workspace's MCP state predates the write, connected when it was created after.

Repro

  1. opencode serve; attach workspace A (any directory, no project-level mcp config) running a plugin whose config hook does config.mcp[name] ??= {...}.
  2. Attach workspace B (any directory, no project-level mcp config).
  3. GET /mcp?directory=<B> shows A's plugin-registered servers.

Fresh opencode --local processes are unaffected (fresh global cache); existing cached per-workspace states are unaffected until re-created. The leak persists for the life of the server process.

Impact

Cross-workspace config isolation is broken on shared servers. A workspace that only adds a project-level mcp config happens to be immune (deep merge then produces a fresh object), which makes the behavior depend on unrelated config details.

Suggested directions

  1. Deep-copy (or otherwise de-alias) nested config objects during merge so workspace configs never reference the global cache.
  2. Hand plugin config hooks an immutable copy (or a draft) of the config instead of the live state object, with hook return values (or explicit mutation APIs) as the update path.
  3. Document that config hooks must not mutate their input.

Verified against v1.18.13; the relevant code paths appear unchanged on current dev at the time of writing.

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.