Windows: Claude Desktop Terminal panel leaks one interactive ConPTY pwsh.exe (~85 MB) per Code session; ending the session does not terminate it, and closing the tab leaks its conhost.exe
Environment
- Claude Desktop 2.110.1 (Microsoft Store package
Claude_2.110.1.0_x64__pzs8sxrjxfjjc) - Claude Code 2.1.271 (
%APPDATA%\Claude\claude-code\2.1.271\claude.exe, entrypointclaude-desktop) - Windows 11 Pro 10.0.26200
- PowerShell 7.6.6 via
terminalCliPowerShellPath = %LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe
Summary
Each Code session's Terminal panel spawns an interactive pwsh.exe under the Electron NodeService utility
process (claude.exe --type=utility --utility-sub-type=node.mojom.NodeService) with a matchingconhost.exe (ConPTY). Ending the Code session does not terminate that shell. It persists until the app
exits — ~85 MB working set plus ~9 MB conhost each. Closing the terminal tab does terminate the shell but
does not release its conhost, which persists until app exit.
Over one 24.8-hour app run on a normal working day: 16 shells / 1.5 GB accumulated, of which 6 belonged to
sessions that had already ended, plus 4 orphaned conhosts from tabs that had been closed.
This is not #81095 / #80510
Those describe a leak **per PowerShell tool call (~300 MB/h, conhost-only, retention 100%). That is
not reproducing on this build: conhosts are exactly 1:1 with shells (16/16 + the 4 tab-close orphans),
not with tool calls, and the day's session made far more than 16 tool calls. What remains is a distinct,
per-session** leak of the interactive terminal shell itself, plus a per-tab-close conhost leak.
The PowerShell tool's own shells (launched cmd.exe → pwsh.exe -NoProfile -NonInteractive … $env:CLAUDE_CODE_SHELL_LAUNCHER_SCRIPT,
carrying CLAUDE_CODE_SESSION_ID / CLAUDE_PID) are created and released normally. They are not involved.
Reproduction
- Open a Code session in Claude Desktop on Windows. Observe: one new
pwsh.exe(command line is the bare
terminalCliPowerShellPath, no arguments) and one conhost.exe, both children of the NodeService pid,
created within the same second. Alternatively, open a Terminal-panel tab in an existing session — same
result, one pair per tab.
- End the session (close the Code tab / let the
claude.exe --output-format streamprocess exit) without
closing the terminal tab. Observe: the pwsh.exe and its conhost.exe remain, indefinitely.
- In another session, close a terminal tab. Observe: its
pwsh.exeexits; itsconhost.exeremains. - Quit the app. Observe: all of them exit (they become parent-dead as the NodeService exits).
Distinguishing features of the leaked shells, read from their PEB: bare command line; 62 environment
variables, the only Claude-related one being CLAUDE_AGENT_SDK_VERSION; no CLAUDE_CODE_SESSION_ID;
cwd = the session's project root; CPU accrues at a constant ~1.2 s per hour of life (an idle tick).
Expected
- Ending a Code session should terminate the session's terminal shell(s) —
ClosePseudoConsole+ terminate
the child — the same way the PowerShell tool's shells are released.
- Closing a terminal tab should release the pseudoconsole so
conhost.exeexits with the shell.
Why this cannot be cleaned up from outside the app
We tried. Nothing outside the app maps a shell pid to a session: not the process environment, not the
working directory (shared across sessions and mutable), not ~/.claude/sessions/<pid>.json (an otherwise
excellent live-session registry that records pid, sessionId, cwd, procStart — but no terminal pid),
not named pipes, not the app's on-disk state, not list_terminal_tabs (session-scoped, and returns no pid).
System handle-table correlation is unavailable to non-elevated callers (object pointers are zeroed) and
would not discriminate anyway since the pty peer is always the NodeService. Time-of-birth heuristics fail
their negative controls on real data (offsets range from −34 s to +81 s; concurrent session starts are
indistinguishable). A third-party reaper therefore has no safe rule and must leave live-parented shells alone.
A cheap secondary fix that would make external cleanup safe
If the lifecycle fix is slow, please publish the terminal shell pid(s) — e.g. a terminalPids array in~/.claude/sessions/<pid>.json (already procStart-guarded against pid reuse) and/or a pid field onlist_terminal_tabs rows. With that, "a bare shell whose pid appears in no live session's terminal list"
is an exact rule, safe in both directions, with no window or tolerance.
Workarounds today
Close terminal tabs before ending a session (frees the shell, leaks 9 MB conhost). Restart the app
periodically (frees everything).
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗