Skip to content

fix(config): clone global cache to prevent cross-workspace mutation leak - #41950

Open
weiconghe wants to merge 1 commit into
anomalyco:devfrom
weiconghe:fix/config-global-cache-leak
Open

fix(config): clone global cache to prevent cross-workspace mutation leak#41950
weiconghe wants to merge 1 commit into
anomalyco:devfrom
weiconghe:fix/config-global-cache-leak

Conversation

@weiconghe

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes#41916

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Config.loadInstanceState merges the process-global config cache into the per-workspace result via remeda mergeDeep. When the workspace has no project-level value for a nested key (e.g. mcp), mergeDeep copies the same object reference from the global cache into the result — verified against remeda 2.26.0:

mergeDeep({}, { mcp: { server1: {...} } }).mcp === global.mcp // true

Plugin config hooks receive the workspace's live config-state object (packages/opencode/src/plugin/index.ts:245) and commonly mutate it in place (config.mcp[name] ??= {...}). When the workspace config shares a nested reference with the global cache, those writes leak back into cachedGlobal — which has Duration.infinity TTL. Every other workspace attached to the same opencode serve process then sees the leaked MCP servers (or any other nested config) in its /mcp status.

Fix:structuredClone the cached global config before merging it into the per-workspace result. The loadGlobal(authEnv) path already returns freshly parsed objects (no caching), so only the getGlobal() path needs cloning.

How did you verify your code works?

Added a regression test in test/config/config.test.ts that:

  1. Sets up a global-only config with mcp (no project-level override)
  2. Loads the instance config, simulates a plugin config hook mutation (cfg.mcp.injected = {...})
  3. Asserts Config.use.getGlobal() does not see the mutation

The test fails without the fix (globalCfg.mcp.injected is the injected object) and passes with it. Full config test suite: 93 pass, 4 pre-existing Windows-specific failures (unrelated — ENOENT on .jsonc auto-creation, Git Bash/Cygwin path timeout, permission key-order on Windows).

Note: the test clears OPENCODE_CONFIG_DIR because a developer's local config may contain mcp: {}, which incidentally breaks the shared reference chain via mergeDeep and masks the bug.

Screenshots / recordings

N/A

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

renekris added a commit to renekris/opencode-lowmem that referenced this pull request Aug 22, 2026
renekris added a commit to renekris/opencode-lowmem that referenced this pull request Aug 26, 2026
- pin note, seam-lookup range, and deferred-port re-verification now
reference v1.18.23 (post-split packages/ai|util tree still absent,
anomalyco#43769 stays blocked)
- watch-list gains the 2026-08-26 sweep candidates (anomalyco#39930, anomalyco#38939,
anomalyco#41950, anomalyco#33713, anomalyco#44631)
- fork-build.sh BASE lookup now excludes *-lowmem.* tags: after a fresh
upstream merge the previous fork tag ties the new base tag on commit
distance with a newer date and git describe stamps the OLD base
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@weiconghe