anomalyco / anomalyco/opencode
Plugin config hooks can mutate process-shared config state via shallow-merged nested objects
@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.loadInstanceStatemerges the global config into the per-workspace result viamergeConfig(remedamergeDeep). 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
confighooks are invoked withcfg = config.get()— the workspace's live config-state object — atpackages/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
mcpconfig, the write lands on the shared globalmcpobject. Every other workspace attached to the same server (opencode servewith multiple attached TUIs) then sees those MCP servers in its config//mcpstatus —disabledwhen the other workspace's MCP state predates the write,connectedwhen it was created after.
Repro
opencode serve; attach workspace A (any directory, no project-levelmcpconfig) running a plugin whoseconfighook doesconfig.mcp[name] ??= {...}.- Attach workspace B (any directory, no project-level
mcpconfig). 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
- Deep-copy (or otherwise de-alias) nested config objects during merge so workspace configs never reference the global cache.
- Hand plugin
confighooks 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. - Document that
confighooks 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.