lspServers ${user_config.*} interpolation ignores declared userConfig defaults, dropping the plugin's entire LSP definition

Status Open
Reported on v2.1.233
Maintainer reply None cached
Activity 2 comments · opened Aug 15, 2026

Version: 2.1.233 (latest at time of filing), Windows 11, npm global install

Summary

A plugin that references ${user_config.<key>} inside lspServers.<server>.env fails to load all of its LSP servers unless the user has explicitly set that key -- even when the plugin's userConfig schema declares a default for it:

Failed to load LSP servers for plugin <name>: Error: Plugin option "<key>" isn't set.

The same ${user_config.*} syntax honors declared defaults for MCP servers. The MCP path builds its option map by merging the schema defaults under the stored values; the lspServers path passes the stored values alone to the same interpolator, which throws on undefined. So a default in userConfig is meaningful for one server type and inert for the other -- and the failure discards every server the plugin declares, not just the entry that referenced the key.

Reproducer

  1. A plugin manifest declaring userConfig: { "knob": { "type": "string", "default": "x" } } and lspServers: { "s": { ..., "env": { "K": "${user_config.knob}" } } }.
  2. Install it from a marketplace. Do not set knob in pluginConfigs[<plugin>].options.
  3. Start Claude Code.

Expected: the server loads with K=x, the declared default.

Actual: Failed to load LSP servers for plugin <name>: Error: Plugin option "knob" isn't set., and the plugin contributes no LSP servers at all.

(An inline --plugin-dir plugin is not a usable harness here -- its lspServers are not registered on 2.1.233 regardless.)

Control: the MCP path, same syntax, correct behavior

The MCP/channels path merges schema defaults before interpolating; the lspServers path does not. The two differ by a single call:

// MCP -- merges defaults:
return $Yd({ ...i, ...s }, { ...r, ...o });

// lspServers -- does not:
let o = e.manifest.userConfig ? $Q(hBe(e)) : void 0;   // raw stored options

where $Yd(stored, schema) is the existing helper that applies default ?? "" under the stored values, and the interpolator throws on any key absent from the map it is handed. (Identifiers are minified and specific to the 2.1.233 bundle -- quoted to make the report checkable, not as stable API.)

The configuration panel does not provide a way out

The error advises "Open /plugin manage to configure it, or check that the plugin's userConfig schema declares <key>". The schema does declare it, and opening the panel does not help:

  • the panel is seeded from the same defaults-free option map, and renders an unset field as empty rather than showing its declared default; and
  • its submit reducer skips a blank, non-required, non-number key whose stored value is undefined, so pressing Save without typing writes nothing.

A user must therefore retype, by hand, a value their plugin's schema already declares -- for every referenced key -- before any LSP server from that plugin will load.

Suggested fix

One call, matching the sibling path:

let o = e.manifest.userConfig
  ? $Yd($Q(hBe(e)), e.manifest.userConfig)   // was: $Q(hBe(e))
  : void 0;

Separately, it may be worth considering whether one unresolved key should discard every server a plugin declares, or only the entry that referenced it.

Impact

Any plugin combining userConfig with ${user_config.*} inside lspServers is unusable on a zero-configuration install. This appears to be unexercised upstream -- none of the 13 LSP plugins in claude-plugins-official declares a userConfig block at all.

We hit it in powershell-lsp and have shipped a downstream mitigation (removing the references, and reading the affected options at their shipped defaults inside the server subprocess). That is a temporary compatibility workaround for this defect, not a fix: it costs us the only supported way to pass a user's configuration into an LSP server subprocess, so we would restore the references once declared defaults are honored here.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗