zai-org / zai-org/feedback

[Bug] Toggling plugin enable/disable has no effect when a project config contains a plugins key

Open
#249 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

priority: P2
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), bundle zcode.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

  1. In ~/.zcode/cli/config.json, set plugins.enabledPlugins with several plugins explicitly false (e.g. "browser-use@zcode-plugins-official": false, "zcode-guide@zcode-plugins-official": false).
  2. In the project's .zcode/config.json, add a plugins key with just dirs (this is required to load project governance hooks):
    { "plugins": { "dirs": ["/Users/.../my-project"] } }
    
  3. Start a new session in that project.
  4. Observe that the disabled plugins are still loaded: browser-use / zcode-guide skills 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 false in enabledPlugins are 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.json but the resolver ignores it because the project config's plugins object replaces the user config's plugins object 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 shallow Object.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.
  • plugins is not in that allowlist. So when the Project scope (priority 20, after User=10) has a plugins key, Object.assign replaces r.plugins wholesale with { dirs: [...] }, dropping the user's enabledPlugins / options.
  • Plugin enable decision then becomes enabled = config.plugins.enabledPlugins[id] ?? defaultEnabled. With enabledPlugins empty, every plugin falls back to its default: bundled official plugins (browser-use, zcode-guide, etc., which have defaultEnabled: true) and inline plugins.dirs plugins (defaultEnabled: true) all load regardless of the user's explicit false.

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.completed show enabledPluginCount inconsistent with the user config's explicit true entries (e.g. 5 enabled while only 3 were explicitly true).
  • All plugin manifests validate (no parse errors, no invalid component paths), so this is not a manifest issue.

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.

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.