Plugin lspServers entries declaring restartOnCrash or shutdownTimeout are silently dropped by the LSP registrar (schema-valid, no diagnostic)

Open 💬 1 comment Opened Jun 10, 2026 by manderse21

Summary

On Claude Code (isolated on 2.1.195, re-confirmed on current 2.1.201), a plugin's plugin.json lspServers server entry is silently dropped by
the runtime LSP registrar if it declares restartOnCrash or shutdownTimeout. Both keys are
accepted by the plugin-manifest JSON schema -- the manifest passes claude plugin validate -- but
the registrar rejects any entry carrying either one, with no diagnostic anywhere in the event
stream. The server never registers, and every LSP operation on the affected file type returns
No LSP server available for file type: .... Removing both fields restores registration.

This is a schema-permits / registrar-rejects mismatch. It is distinct from the LspServerManager
init-ordering race (#14803, fixed) and from the marketplace packaging gap
(anthropics/claude-plugins-official#379): control plugins register on the same build, and the entry
here is read from plugin.json, which the installer does copy.

Environment

  • Claude Code: isolated on 2.1.195 (2026-06-26) via a controlled single-field matrix;

re-confirmed on 2.1.201 (2026-07-06) via a two-plugin control differing only by the two fields.

server declared in plugin.json lspServers.

  • PSES v4.6.0, PSScriptAnalyzer 1.25.0; Windows 11.
  • Harness: one builtin-LSP goToDefinition, driven through a fresh non-interactive

claude -p --allowedTools LSP --strict-mcp-config --mcp-config '{"mcpServers":{}}'
--output-format stream-json --verbose
; tool_use / tool_result read from the event stream
(not a prompt echo). The plugin was installed via claude plugin marketplace add <local-dir> +
claude plugin install (the installer populates the cache), then probed from a fresh
claude -p with no --plugin-dir.

Reproduction

Start from a known-good plugin.json lspServers entry that registers and serves -- a clean block
declaring command, args, extensionToLanguage, transport, startupTimeout, maxRestarts,
env (each of these was confirmed registrar-clean when added alone). Then:

  1. Add restartOnCrash to the entry. goToDefinition on a mapped file returns

No LSP server available for file type: ... -- the extension-to-server mapping is not
registered.

  1. Revert, and add shutdownTimeout instead. Same result -- not registered.
  2. Remove both. The entry registers and Claude Code launches the server.

Both fields validate under the manifest schema; neither drop emits any registrar diagnostic.

Single-field isolation matrix (Claude Code 2.1.195)

Signal strings:

  • No LSP server available for file type: .ps1 -> the mapping was NOT registered.
  • No definition found / Defined in ... -> registered and served (a known-good command).
  • ... timed out after 30000ms during initialization -> registered (the server launched; init did

not complete -- a separate serve-layer matter, see "Scope" below).

| Variant | Block delta | .ps1 result |
|---------|-------------|---------------|
| Known-good block in plugin.json | clean block, no restart/shutdown fields | REGISTER (served) |
| Known-good + transport / maxRestarts / env | each added alone | REGISTER (served) |
| Known-good + restartOnCrash (alone) | breaker | FAIL (no server) |
| Known-good + shutdownTimeout (alone) | breaker | FAIL (no server) |
| Shipped block | both breakers present | FAIL (no server) |
| Shipped minus restartOnCrash | one breaker removed, shutdownTimeout remains | FAIL (no server) |
| Shipped minus both breakers | the fix | REGISTER (launch; init timeout) |

Both fields must be removed: removing only one still fails. An earlier suspect -- an
env / ${CLAUDE_PLUGIN_DATA} block -- was refuted (known-good + env registers).

Re-confirmation on 2.1.201

The drop reproduces on the current build. Two throwaway plugins were installed one at a time, each
mapping a unique test extension (.ps1x) and differing only in that one declared restartOnCrash +
shutdownTimeout. A single documentSymbol call against the same file gave:

  • clean entry: reached a launch attempt and returned the separate #73961 Windows launcher-guard

error (Command 'pwsh' not found or is in an unsafe location) -- i.e. it registered.

  • breaker entry: No LSP server available for file type: .ps1x -- it never registered.

On current Windows the clean entry surfaces the #73961 launcher guard at spawn rather than a served
result; that still evidences registration (see Scope). The 2.1.195 matrix above remains the primary
single-field isolation; this is an equivalent two-plugin re-confirmation on the current build.

Which manifest is read

When a plugin ships a plugin.json, Claude Code registers lspServers from plugin.json and
ignores the marketplace.json entry; the marketplace.json lspServers is consulted only for
plugins that ship no plugin.json (as the official LSP plugins do -- they ship only LICENSE +
README and register from the marketplace entry). A plugin that needs a plugin.json -- for hooks
or userConfig -- must therefore carry a registrar-clean lspServers there; putting it only in
marketplace.json is inert while a plugin.json exists. This is why the two-field drop is
load-bearing rather than a packaging artifact: it is independent of anthropics/claude-plugins-official#379.

Suggested resolution

Either honor restartOnCrash / shutdownTimeout at registration, or -- if they are intentionally
unsupported at the registrar -- reject them at claude plugin validate with a named error, so the
schema-permits / registrar-rejects gap surfaces at authoring time instead of failing silently at
runtime. Failing that, a single event-stream warning when the registrar drops a schema-valid field
would localize this whole class of silent failure.

Relationship to existing issues

  • #14803 (registration race, fixed) / #15168 / #15148: the init-ordering race is not this. Control

plugins (e.g. the official typescript LSP) register on the same build, so the registrar runs;
the miss here is the two-field drop, a distinct defect.

  • anthropics/claude-plugins-official#379 (marketplace packaging gap): real and open, but

independent -- registration succeeds once the plugin.json block is registrar-clean.

  • anthropics/claude-plugins-official#1359-class server-to-client init handshake: the remaining serve-layer gap after registration is

restored (the launched server's init times out because the client rejects standard
server-to-client requests). Separate from registration; not this report.

  • #73961 (Windows native LSP launcher refusing bare commands): a separate, later, spawn-layer

blocker on Windows; also not this report.

Scope

This report is about registration only. It does not claim native serve works -- serve is separately
gated (anthropics/claude-plugins-official#1359), and on Windows the launcher separately refuses bare commands (#73961). The plugin's
own diagnostics path does not use native LSP and is unaffected in either state.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗