clangd-lsp plugin never registers an LSP tool (missing lspServers manifest + possible hard gate)
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(commit1ba9d2211ae1) - 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, version1.0.0, installed at both user and project scope - Project has a valid, fresh
compile_commands.jsonat the repo root (generated viaCMAKE_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
ToolSearchwith queries like"lsp","clangd","goToDefinition findReferences hover documentSymbol"returns no matching deferred tools, in every session tried.- A freshly spawned interactive
claudesession 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
- The installed plugin package ships no server configuration. The entire
clangd-lspplugin (both the cached copy under~/.claude/plugins/cache/claude-plugins-official/clangd-lsp/1.0.0/and the marketplace source) contains onlyLICENSE,README.md, and a bare.claude-plugin/plugin.jsonwith justname/description/version/author. All five other*-lspplugins in the same marketplace (pyright-lsp,typescript-lsp,gopls-lsp,rust-analyzer-lsp,jdtls-lsp) are equally bare — onlyLICENSE+README.md, no manifest content at all.
lspServersis a real, documented plugin-manifest field that none of these packages use. Strings extracted from theclaudebinary show a manifest schema (parallel tomcpServers) 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...")])
extensionToLanguage
i.e. a plugin should declare either an inline map of server configs (command, args, ) 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.
- A separate flag exists (
ENABLE_LSP_TOOL) but had no observable effect. The binary definesENABLE_LSP_TOOLas a boolean env-var flag in the same flat registry as other standard, documented flags (DISABLE_AUTOUPDATER,DISABLE_TELEMETRY, etc.), all parsed identically viaprocess.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 mechanismsettings.json'senvblock 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.
- 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") ... }
"lspServers"
This reads as an early-return kill-switch check on the literal key , 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
- Install
clangdand confirm it's onPATH. - Enable
clangd-lsp@claude-plugins-officialinsettings.json("enabledPlugins": {"clangd-lsp@claude-plugins-official": true}). - Start a
claudesession in a C/C++ project with a validcompile_commands.jsonat the project root. - Ask the model to enumerate its available tools, or call
ToolSearchwith queries like"lsp"/"clangd"/"goToDefinition". - Observe: no LSP-related tool is present, loaded or deferred.
Suggested fix
- Ship an actual
lspServersmanifest (or.lsp.json) in theclangd-lspplugin package declaring theclangdcommand, standard args, and the C/C++extensionToLanguagemapping — the same should apply to the other*-lspplugins, which appear to have the identical gap. - If there is an additional feature gate controlling
lspServersinitialization beyond the plugin manifest and theENABLE_LSP_TOOLenv 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.