LSP plugins fail on Windows: spawn() cannot find npm-installed .cmd binaries

Resolved 💬 3 comments Opened Mar 5, 2026 by piersrobcoleman Closed Mar 8, 2026

Summary

LSP plugins (pyright-lsp, typescript-lsp, and any npm-installed language server) fail to start on Windows because child_process.spawn() without shell: true cannot execute .cmd wrapper scripts.

Root Cause

When npm installs a global package on Windows, it creates .cmd batch wrappers (e.g., pyright-langserver.cmd) in %APPDATA%\npm\. These are not .exe files.

spawn('pyright-langserver', ['--stdio']) calls CreateProcessW under the hood, which only resolves .exe and .com extensions. The .cmd wrapper is invisible to it, so the process fails with ENOENT.

Reproduction

  1. Install pyright-lsp or typescript-lsp plugin on Windows
  2. Open a .py or .ts file
  3. LSP features (diagnostics, go-to-definition, etc.) do not work
  4. In debug logs, spawn fails with ENOENT for pyright-langserver

Environment

  • Windows 11
  • Node.js v22.x (installed via winget, not nvm)
  • npm globals in %APPDATA%\npm\
  • Claude Code latest

Suggested Fix

Any of these would work:

  1. Use shell: true in the spawn() options when launching LSP servers on Windows. This lets cmd.exe resolve .cmd wrappers naturally.
  1. Platform-aware command resolution: On Windows, check if command + '.cmd' exists in PATH before spawning, and if so, either use shell: true or resolve the .cmd to its underlying node + JS entry point.
  1. Allow local LSP config overrides: Let users override lspServers entries in a local config file that doesn't get overwritten by plugin updates. Currently, marketplace.json is the only config source and gets overwritten on every plugin update.

Current Workaround

Manually patch marketplace.json to use node as the command with the JS entry point as the first argument:

// Before (fails on Windows):
"command": "pyright-langserver",
"args": ["--stdio"]

// After (works):
"command": "node",
"args": ["C:/Users/<user>/AppData/Roaming/npm/node_modules/pyright/langserver.index.js", "--stdio"]

This must be re-applied after every plugin update since marketplace.json is overwritten.

Related

The csharp-ls plugin (via dotnet tool install) has a separate issue: Claude Code's LSP client is missing handlers for workspace/configuration, client/registerCapability, and window/workDoneProgress/create requests that csharp-ls requires during initialization. This causes the server to hang waiting for responses that never come.

View original on GitHub ↗

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