[Bug] Toggling plugin enable/disable has no effect when a project config contains a plugins key
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 22
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
[Bug] Toggling plugin enable/disable has no effect when a project config contains a plugins key
Environment
- OS: macOS 25.5 (darwin 25.5.0), Apple Silicon (arm64)
- ZCode version: 3.7.6 (build 3.7.6.4691)
- App: desktop app (
/Applications/ZCode.app), bundlezcode.cjs - Model channel: Z.ai / GLM (client-side issue; model channel is not a factor)
Summary
Plugins that are explicitly disabled in ~/.zcode/cli/config.json (plugins.enabledPlugins) still load their skills/hooks in the session. The enable/disable toggle in Settings → Plugin Management appears to "do nothing": the state is written to the user config file correctly, but the runtime plugin resolver ignores it and falls back to each plugin's default-enabled state.
Root cause: ZCode's config merger does not deep-merge the plugins key across config scopes. When a project-level .zcode/config.json contains any plugins key (e.g. plugins.dirs, used for inline/hook plugins), it overwrites the user-level plugins.enabledPlugins and plugins.options.
Steps to reproduce
- In
~/.zcode/cli/config.json, setplugins.enabledPluginswith several plugins explicitlyfalse(e.g."browser-use@zcode-plugins-official": false,"zcode-guide@zcode-plugins-official": false). - In the project's
.zcode/config.json, add apluginskey with justdirs(this is required to load project governance hooks):{ "plugins": { "dirs": ["/Users/.../my-project"] } } - Start a new session in that project.
- Observe that the disabled plugins are still loaded:
browser-use/zcode-guideskills appear in the session, and the project's inline hook plugin (...-hooks@inline: false) still executes its hooks.
Expected vs. actual
- Expected: Plugins explicitly set to
falseinenabledPluginsare disabled — their skills, commands, and hooks do not load. Toggling the switch in Settings takes effect. - Actual: Disabled plugins still load; the toggle writes to
~/.zcode/cli/config.jsonbut the resolver ignores it because the project config'spluginsobject replaces the user config'spluginsobject during merge.
Root cause (from zcode.cjs, v3.7.6)
- Config scopes are merged with the following priorities:
System=0, User=10, Project=20, Session=30, Env=40, Cli=50. - The merger
mergeConfigs(i2e) merges each source with a shallowObject.assign(r, o)at the top level, then only deep-merges a hardcoded allowlist of keys:model,modelCatalog,modelStream,permission,storage,network,features,memory,mcp,skills,skillOverrides,commandOverrides,logging,toolConcurrency,modelAnomalyGuard,hooks,ui. pluginsis not in that allowlist. So when the Project scope (priority 20, after User=10) has apluginskey,Object.assignreplacesr.pluginswholesale with{ dirs: [...] }, dropping the user'senabledPlugins/options.- Plugin enable decision then becomes
enabled = config.plugins.enabledPlugins[id] ?? defaultEnabled. WithenabledPluginsempty, every plugin falls back to its default: bundled official plugins (browser-use,zcode-guide, etc., which havedefaultEnabled: true) and inlineplugins.dirsplugins (defaultEnabled: true) all load regardless of the user's explicitfalse.
Note: mcp is deep-merged correctly (including a servers sub-merge), which is why MCP server config from both user and project survives — but plugins does not have the equivalent treatment. This asymmetry is the bug.
Suggested fix
Add plugins to the deep-merge allowlist in mergeConfigs, e.g.:
if (o.plugins) r.plugins = {
...r.plugins,
...o.plugins,
dirs: [...(r.plugins?.dirs ?? []), ...(o.plugins?.dirs ?? [])],
enabledPlugins: { ...r.plugins?.enabledPlugins, ...o.plugins?.enabledPlugins },
options: { ...r.plugins?.options, ...o.plugins?.options },
suppressedBuiltins: [...(r.plugins?.suppressedBuiltins ?? []), ...(o.plugins?.suppressedBuiltins ?? [])],
};
This would allow user-level enabledPlugins/options to coexist with project-level dirs, making the Settings toggle effective again.
Workaround (until fixed)
Place enabledPlugins/options into the project .zcode/config.json plugins object (since project plugins wins wholesale), and edit that file to enable/disable plugins. The Settings UI toggle will still not work because it writes to the user config, which remains overridden.
Logs
- Startup log events
bootstrap.app.startup.plugins.completedshowenabledPluginCountinconsistent with the user config's explicittrueentries (e.g. 5 enabled while only 3 were explicitlytrue). - All plugin manifests validate (no parse errors, no invalid component paths), so this is not a manifest issue.
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.
Research direction
Start in the bundled zcode.cjs at mergeConfigs (i2e) and trace how project and user config scopes are combined before plugin resolution. Reproduce the issue with user enabledPlugins and project plugins.dirs entries, then verify that explicit disablement and project directories both survive configuration merging and affect the session plugin state.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- desktop, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100