clangd-lsp plugin never registers an LSP tool (missing lspServers manifest + possible hard gate)

Status Open
Reported on v2.1.246
Maintainer reply None cached
Activity 0 comments · opened Aug 27, 2026

clangd-lsp plugin never registers an LSP tool — missing manifest field + possible hard gate

Summary

The clangd-lsp@claude-plugins-official plugin is enabled and clangd is installed and on PATH, but no LSP tool (goToDefinition/findReferences/hover/documentSymbol, or similar) is ever exposed to a session — not loaded, not deferred, not discoverable via ToolSearch. This reproduces in both a plain interactive terminal session and a headless (-p) session, on macOS, Claude Code 2.1.246.

Environment

  • Claude Code version: 2.1.246 (commit 1ba9d2211ae1)
  • Platform: darwin-arm64 (macOS 25.6.0)
  • clangd --version: Apple clangd version 21.0.0 (clang-2100.1.1.101), at /usr/bin/clangd
  • Plugin: clangd-lsp@claude-plugins-official, version 1.0.0, installed at both user and project scope
  • Project has a valid, fresh compile_commands.json at the repo root (generated via CMAKE_EXPORT_COMPILE_COMMANDS)

Expected behavior

With the plugin enabled and clangd on PATH, some LSP tool should be available to the model for C/C++ symbol analysis (per the plugin's own README, which frames it as providing "code intelligence, diagnostics, and formatting").

Actual behavior

  • ToolSearch with queries like "lsp", "clangd", "goToDefinition findReferences hover documentSymbol" returns no matching deferred tools, in every session tried.
  • A freshly spawned interactive claude session in the project directory, asked directly to enumerate its own available tools (loaded + deferred), confirms no LSP-related tool exists in either list.
  • The plugin's runtime data directory (~/.claude/plugins/data/clangd-lsp-claude-plugins-official/) is completely empty — no logs, no index state — suggesting an LSP server has never actually been spawned by this plugin on this machine.

Root cause investigation

  1. The installed plugin package ships no server configuration. The entire clangd-lsp plugin (both the cached copy under ~/.claude/plugins/cache/claude-plugins-official/clangd-lsp/1.0.0/ and the marketplace source) contains only LICENSE, README.md, and a bare .claude-plugin/plugin.json with just name/description/version/author. All five other *-lsp plugins in the same marketplace (pyright-lsp, typescript-lsp, gopls-lsp, rust-analyzer-lsp, jdtls-lsp) are equally bare — only LICENSE + README.md, no manifest content at all.
  1. lspServers is a real, documented plugin-manifest field that none of these packages use. Strings extracted from the claude binary show a manifest schema (parallel to mcpServers) that plugins are expected to populate:

``
lspServers: T([ee().describe("Path to .lsp.json configuration file relative to plugin root"),
M(r(),Nt()).describe("LSP server configurations keyed by server name...")])
`
i.e. a plugin should declare either an inline map of server configs (command, args,
extensionToLanguage) or a .lsp.json file path relative to the plugin root. clangd-lsp's plugin.json has neither. There is also user-facing error handling in the binary for this exact scenario ("lsp-config-invalid", "Failed to load LSP server configuration", "declares two LSP servers for ... in its lspServers config"), confirming the feature is designed to work this way but the shipped clangd-lsp` package doesn't populate it.

  1. A separate flag exists (ENABLE_LSP_TOOL) but had no observable effect. The binary defines ENABLE_LSP_TOOL as a boolean env-var flag in the same flat registry as other standard, documented flags (DISABLE_AUTOUPDATER, DISABLE_TELEMETRY, etc.), all parsed identically via process.env. We tested setting it two ways:
  • Directly in the shell: ENABLE_LSP_TOOL=1 claude -p "..."
  • Via --settings '{"env":{"ENABLE_LSP_TOOL":"1"}}' (the same mechanism settings.json's env block uses)

Neither changed anything — no LSP tool appeared, loaded or deferred, in either case, and --debug plugins,mcp,tools,flags produced no log output referencing the flag at all.

  1. The LSP manager's init path appears to be gated by something else. Also visible in the binary, guarding initializeLspServerManager():

``js
function f(h,y,_){ if(_a("lspServers")) return; if(Wzo()){...} ... b("[LSP MANAGER] initializeLspServerManager() called") ... }
`
This reads as an early-return kill-switch check on the literal key
"lspServers", independent of the ENABLE_LSP_TOOL env var we tested. Given a sibling flag DISABLE_GROWTHBOOK exists in the same env-var registry, this is plausibly a remotely-controlled feature gate (e.g. GrowthBook) rather than something controllable via local settings — which would explain why the ENABLE_LSP_TOOL` override had zero effect.

Repro steps

  1. Install clangd and confirm it's on PATH.
  2. Enable clangd-lsp@claude-plugins-official in settings.json ("enabledPlugins": {"clangd-lsp@claude-plugins-official": true}).
  3. Start a claude session in a C/C++ project with a valid compile_commands.json at the project root.
  4. Ask the model to enumerate its available tools, or call ToolSearch with queries like "lsp" / "clangd" / "goToDefinition".
  5. Observe: no LSP-related tool is present, loaded or deferred.

Suggested fix

  • Ship an actual lspServers manifest (or .lsp.json) in the clangd-lsp plugin package declaring the clangd command, standard args, and the C/C++ extensionToLanguage mapping — the same should apply to the other *-lsp plugins, which appear to have the identical gap.
  • If there is an additional feature gate controlling lspServers initialization beyond the plugin manifest and the ENABLE_LSP_TOOL env var, please document how (if at all) it can be enabled for a given account/session — right now there is no way for a customer to self-diagnose or self-enable this from the client side.

Impact

Our team's project-level CLAUDE.md and a PreToolUse hook enforce "always use LSP for C++ symbol analysis, never grep" as a hard requirement, on the assumption that this plugin makes an LSP tool available once installed and enabled. Since it currently doesn't, every agent session correctly falls back to grep/Read per our own fallback policy, but the enforcement hook fires on every such fallback, which reads as agent misbehavior until traced back to this gap.

View original on GitHub ↗