[BUG] LSP tool is pruned from all subagent tool sets in interactive sessions (present in the parent, and in subagents under -p)
Summary
In an interactive Claude Code session,
the LSP tool is absent from every dispatched subagent's active tool set
and from its deferred-tool registry,
so no prompt wording and no agent configuration can reach it.
The parent session in the very same process has LSP available
and returns real language-server data.
The same dispatch under claude -p (headless) does give the subagent LSP,
in all six --permission-mode values.
The subagent receives this error, which is factually wrong:
Error: No such tool available: LSP. LSP is disabled for this session, in subagents as well as here.
Both clauses are false.
LSP is not disabled for the session,
and it is not unavailable outside subagents —
the parent loads the schema and calls it successfully minutes later
in the same session, every time.
This reproduces in a clean room —
a fresh config directory, a freshly issued token,
a throwaway two-line Python project,
and pyright-lsp as the only installed plugin,
with no CLAUDE.md, hooks, skills, or custom agents anywhere.
See "Clean-room reproduction" below.
Local configuration is not involved.
I'm happy to run further diagnostics.
Environment
- Claude Code
2.1.221and2.1.222(both reproduce) - Linux (Ubuntu, kernel 7.0.0-1005-gcp), bash
- Model: reproduced on both Opus 5 and Sonnet 5 — model-independent
- Plugin:
pyright-lsp@claude-plugins-officialv1.0.0, enabled in~/.claude/settings.json pyright-langserveron PATH;pyright 1.1.410(npm global)- Python project, probing a
.pyfile
The plugin install is correct — please don't close this as a config problem.pyright-lsp declares its server in the marketplace ROOT manifest,~/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json:
"name": "pyright-lsp", "version": "1.0.0",
"source": "./plugins/pyright-lsp",
"lspServers": {
"pyright": {
"command": "pyright-langserver",
"args": ["--stdio"],
"extensionToLanguage": { ".py": "python", ".pyi": "python" }
}
}
There is no per-plugin plugin.json for LSP plugins,
and the marketplace source checkout for plugins/pyright-lsp/
legitimately contains only LICENSE and README.md.
Twelve plugins in that manifest declare lspServers the same way.
The language server itself demonstrably works —
the parent gets correct documentSymbol output from it.
Reproduction
- Install
pyright-lspandnpm install -g pyright. - Start an interactive session in a Python project:
cd $PROJECT_PATH && claude - As the very first action, dispatch a plain
general-purposesubagent and tell it to call
LSP with operation=documentSymbol, a real .py filePath, line=1, character=1,
without calling ToolSearch first.
- The subagent reports the error above.
Ask it to run ToolSearch with select:LSP — it gets No matching deferred tools found.
Ask it to list its deferred tools — LSP is not among them.
- Now, in the same session, the parent runs
ToolSearchwithselect:LSP:
the full schema loads (9 operations).
The parent then calls LSP documentSymbol on the same file: real symbol data.
- Contrast — the same dispatch headless works:
claude -p "Dispatch a subagent with subagent_type='general-purpose' and have it call LSP \
with operation=documentSymbol, filePath=$PROJECT_PATH/path/to/module.py, line=1, character=1, \
without calling ToolSearch first. Report the outcome." \
--allowedTools Agent LSP ToolSearch Read
The headless subagent gets LSP and returns real symbol data.
Clean-room reproduction — rules out all local configuration
To eliminate every piece of local setup,
the interactive reproduction was repeated against a virgin configuration:
mkdir -p /tmp/clean-config /tmp/clean-proj
printf 'class Demo:\n def method(self, x: int) -> str:\n return str(x)\n' > /tmp/clean-proj/demo.py
cd /tmp/clean-proj && CLAUDE_CONFIG_DIR=/tmp/clean-config claude
In that session: accepted the trust dialog,
authenticated with a newly issued token,
installed pyright-lsp@claude-plugins-official as the only plugin,
then restarted so the plugin registered at startup.
This configuration has no global or project CLAUDE.md,
no hooks, no skills, no custom agent definitions,
no MCP servers beyond the built-in claude_ai_* auth stubs,
and no other plugins.
The split reproduces exactly.
Parent, direct LSP documentSymbol call on demo.py, no ToolSearch first:
Document symbols:
Demo (Class) - Line 1
method (Method) - Line 2
x (Variable) - Line 2
Subagent (general-purpose), identical call:
Error: No such tool available: LSP. LSP is disabled for this session, in subagents as well as here.
LSP was in neither the subagent's active set nor its deferred list.
That deferred list was, verbatim:EnterWorktree, ExitWorktree, Monitor, NotebookEdit, SendMessage, TaskStop, plus the
WebFetch, WebSearchmcp__claude_ai_* auth stubs —
the same eight built-in entries seen in every other session,
confirming the withheld set is constant and independent of configuration.
One further detail from this run:
the parent's direct call succeeded without any ToolSearch,
even though LSP was listed as deferred with no schema loaded.
So for the parent the deferred contract is advisory,
while for the subagent the tool genuinely does not exist by either path.
Expected vs actual
Expected: a subagent dispatched from a session where an LSP server is configured
and working should be able to use the LSP tool,
either directly or after ToolSearch.
Actual: interactive subagents cannot reach it by any path,
and are told it is disabled session-wide, which is untrue.
Evidence
Interactive: 8 of 8 dispatches ABSENT, across four permission modes
Four fresh interactive sessions on 2.1.222, each dispatching a general-purpose subagent
at its startup mode and again after a Shift+Tab mode cycle
(--model claude-sonnet-5 throughout, to avoid a concurrent provider incident):
| Startup mode | Dispatch 1 (startup mode) | Cycled to | Dispatch 2 (new mode) | Parent, same session |
|---------------|---------------------------|---------------|-----------------------|------------------------------|
| auto | ABSENT | manual | ABSENT | LSP loads, returns real data |
| manual | ABSENT | acceptEdits | ABSENT | LSP loads, returns real data |
| acceptEdits | ABSENT | auto | ABSENT | LSP loads, returns real data |
| dontAsk | ABSENT | manual | ABSENT | LSP loads, returns real data |
All eight subagents reported the identical 8-tool active set,
an identical ~74-entry deferred list,
and a character-for-character identical error string.
Headless -p: LSP present in all six modes
| --permission-mode | Subagent LSP |
|-----------------------------------------------------------------|-------------------------------------------------------------------|
| manual, dontAsk, bypassPermissions, acceptEdits, plan | available, callable directly |
| auto | available as a deferred tool (ToolSearch loads it, then it works) |
Note the direct contradiction:
interactive auto → ABSENT, headless auto → available.
Same version, same machine, same plugin, same permission mode.
Exhibit A — the deferred registry is scoped per agent
Parent's deferred-tool list (verbatim, interactive session):
CronCreate, CronDelete, CronList, DesignSync, EndConversation, EnterPlanMode,
EnterWorktree, ExitPlanMode, ExitWorktree, LSP, ListMcpResourcesTool, Monitor,
NotebookEdit, PushNotification, ReadMcpResourceDirTool, ReadMcpResourceTool,
RemoteTrigger, SendMessage, TaskCreate, TaskGet, TaskList, TaskOutput, TaskStop,
WebFetch, WebSearch
…plus all MCP tools.
Subagent's deferred-tool list (verbatim, same session):
EnterWorktree, ExitWorktree, Monitor, NotebookEdit, SendMessage, TaskStop,
WebFetch, WebSearch
…plus all MCP tools.
The subagent's active set is exactly:
Agent, Artifact, Bash, Edit, Read, Skill, ToolSearch, Write
The 18 entries withheld from subagents are a coherent
orchestration / session-control family —Cron*, Task*, Enter/ExitPlanMode, DesignSync, RemoteTrigger,PushNotification, EndConversation, ReadMcpResource*, ListMcpResourcesTool —
and LSP has apparently been swept in with them.
A code-navigation tool grouped with cron jobs and plan mode
looks like a classification mistake rather than an intentional restriction.
This also explains why no prompt wording helps:
a subagent cannot ToolSearch its way to a tool the harness never granted it.
Exhibit B — tools: frontmatter is honored for everything except LSP
A custom agent at .claude/agents/lsp-probe-explicit.md declaring:
tools: LSP, Read, ToolSearch, Bash
The dispatched subagent could call Read, Bash, and ToolSearch.Grep and Glob, correctly not granted because they are absent from the list,
were withheld — so the allowlist mechanism works exactly as documented.
Only the LSP entry silently evaporated:
no warning at dispatch time,
no error until the tool is actually called.
Reproduced on both Opus and Sonnet.
Also worth fixing:
the agent-type roster advertises this agent as (Tools: LSP, Read, ToolSearch, Bash),
which is not what it receives.
A sibling agent with no tools: field at all
received the harness default subagent toolset —
name-for-name identical to plain general-purpose, still no LSP —
rather than the dispatching parent's tools.
So there is no frontmatter path to LSP:
neither a narrow allowlist nor omitting the field entirely.
Exhibit C — the error message is wrong and actively misleading
LSP is disabled for this session, in subagents as well as here.
was returned to 5+ independent subagent dispatches across two models.
It describes a session-wide kill switch that does not exist.
This is not a cosmetic complaint:
the first investigator to hit this
reasonably concluded LSP was disabled session-wide,
reported that the parent could not use it either,
and only a direct parent-side test disproved it.
The message sends debuggers in the wrong direction.
Causes ruled out
Each of these was tested and eliminated, so they don't need re-litigating:
- All local configuration — reproduces in a clean room with a fresh config dir,
a fresh token, one plugin, and no CLAUDE.md / hooks / skills / custom agents.
Note also that no permissions mechanism could produce this shape:
permissions are inherited downward by subagents, never inverted,
and the pruning is visible in the harness-generated deferred registry itself.
- Plugin install / marketplace config — verified correct (see Environment); server works for the parent.
- Missing binary —
pyright-langserverandpyright 1.1.410both present. - Settings — nothing matching
lsp|disabledTools|disableAllTools|enableLsp|languageServer
in user, project, or managed settings; no /etc/claude-code/managed-settings.json.
- Session staleness — a 25-minute-old session reproduces it.
- Agent type — plain built-in
general-purposeand two custom types all fail. tools:frontmatter — see Exhibit B.--allowedTools LSP— headless works with and without it.- Model — identical on Opus 5 and Sonnet 5.
- Permission mode — 8/8 interactive dispatches ABSENT across four modes.
- Startup mode vs currently-active mode — a mid-session
Shift+Tabcycle changes nothing.
The only variable that changes the outcome is interactive vs -p.
Impact
Any workflow that delegates code navigation to subagents loses LSP entirely.
Every dispatched agent silently falls back to grep/find via Bash,
which is slower, costs far more tokens, and is less accurate
than findReferences / goToDefinition / documentSymbol.
Project instructions that tell subagents to prefer LSP over text search
are currently unfollowable, and there is no way to detect this from configuration —
only by dispatching an agent and watching it fail.
Related
- #27498 — "[BUG] LSP tool is no longer being registered as a callable tool"
(worked in 2.1.34, broke in 2.1.50, ToolSearch returned 0 matches; closed as not planned).
Same family. The main-session half appears fixed since;
the subagent half is what this report covers, with a much tighter reproduction:
parent and subagent differing inside one session.
- #31002 — built-in system tools moved behind
ToolSearch(undocumented).
The mechanism that makes tool availability an indexing problem.
- #32485 —
ToolSearchnot indexing things it should surface. - #60763 / #61993 / #46424 / #80036 — the
Agent/Tasktool being pruned from subagents
despite the docs saying it is inherited. Same shape of bug.
Possibly separate issue, noticed while investigating
Happy to split this out if preferred.
Grep and Glob are absent from every agent, parent included.
Parent active set:Agent, Artifact, AskUserQuestion, Bash, Edit, Read, ReportFindings, ScheduleWakeup, — no
ShareOnboardingGuide, Skill, ToolSearch, Workflow, WriteGrep, no Glob,
and neither in the deferred list.
Their error text ("Grep is not available in this session — search file contents with) is at least accurate, unlike LSP's.
grep via the Bash tool instead"
If this is intentional, it's undocumented, and it makes
the standard "use Grep/Glob for text search" guidance unexecutable.
3 Comments
Cross-reference:
ReportFindingsshows this same shape, which may make this issue the general mechanism rather than an LSP-specific one.Measured in an interactive session, same session seconds apart: the main thread had
ReportFindingswith a full schema and a live call succeeded, while a subagent lacked it from its tool list entirely and a call attempt returnedNo such tool available. Detail and the exact error string are in #84093 (which is otherwise reported as a/code-reviewbug). #85230 reports the same pattern forListMcpResourcesTool/ReadMcpResourceToolin background subagents.Three tools, same signature — present in the parent, absent in the subagent, interactive sessions. If the pruning you've identified here is the cause, those two are symptoms of it.
Still reproduces on 2.1.250 / Windows 11 with a different LSP plugin, so it is not pyright- or Linux-specific.
Environment: Claude Code 2.1.250 (native win32 binary),
ENABLE_LSP_TOOL=1in~/.claude/settings.jsonenv, pluginal-language-server-go-windows@claude-code-lsps1.11.3 (AL / Business Central), interactive session, AL project (~10k.alfiles).Parent session:
LSP documentSymbolon a table file returns full symbols; workspace diagnostics stream in — the server is connected and healthy.Subagents, same session, seconds later:
| Agent type |
tools:frontmatter | Probe | Result ||---|---|---|---|
| custom (
.claude/agents/*.md) | explicit list includingLSP|ToolSearch select:LSP|No matching deferred tools found|| custom, same | same | asked to call
LSPdirectly and dump its function list | function list = Read, Grep, Glob, Bash, ToolSearch; deferred = MCP tools only;LSPabsent from both ||
general-purpose|*|ToolSearch select:LSP| absent ||
Explore| built-in |ToolSearch select:LSP| absent |Two additional data points beyond the original report:
documentSymbol(i.e. after the server's "has ever connected" state was true) — absent both times. So it is not a lazy-connect race.From the bundled JS (2.1.250): the LSP tool is included in the base tool list unconditionally,
isEnabled()is the LSP manager'shasEverConnected, and the subagent pool filter's denylist contains only interactive tools (TaskOutput, ExitPlanMode, EnterPlanMode, AskUserQuestion, Poll, Workflow, memory_*, …) — nothing that names LSP. Whatever prunes it happens elsewhere in the subagent pool build.Workaround that's held up for me while this is open: don't route the subagent through the LSP tool at all, give it the CLI. For Python,
pyright --outputjson path/to/file.pyfrom Bash returns the same diagnostics the language server would, as JSON the agent can parse, and subagents keep Bash even when LSP is pruned. It's slower (fresh process each call, a couple of seconds on a mid-size project) and you lose documentSymbol/hover, but for the "find the type errors in the files you just touched" step it's been fine.The other thing I've done is move the LSP-dependent step into a nested
claude -pcall from the subagent's Bash, since you've confirmed headless dispatch does get the tool. Ugly, but it's a one-line change and it unblocked a nightly job.