[BUG][Windows] Leftover `claude.exe.old.<ts>` process after auto-update enters a `taskkill` spawn loop, wedging WMI (`WmiPrvSE` ~400% CPU) machine-wide
Not a duplicate of #73785 or #67888 — those share the downstream taskkill → WMI amplification mechanism but are driven by live concurrent sessions (statusline cancellation + cross-session liveness probes) during normal operation. This report is a different trigger: a single orphaned old-binary process left over by a completed auto-update that goes into a runaway cleanup loop on its own, with no session load driving it. Full differentiation in the appendix.
Summary
On a Windows npm-global install, the auto-updater renamed the running binary to claude.exe.old.<timestamp> (expected for an in-place swap) — but the old process kept running and entered a runaway loop spawning 1–2 taskkill.exe per second, sustained for hours. Each taskkill issues a WMI Win32_Process ExecQuery, so the loop turned into a WMI query flood that pinned the shared WmiPrvSE.exe (CIMWin32 provider host) at ~400% CPU. WmiPrvSE stayed wedged 90+ seconds after the runaway source was killed (no self-recovery), and while wedged, every WMI consumer on the machine (Task Manager, Explorer shell bits, vendor sensor daemons) queued/hung — whole-machine stutter. The leftover burned 2,439 CPU-seconds before it was found. Recovery required killing the .old process and manually killing the wedged WmiPrvSE.
The novel part vs. existing updater bugs is not the .old rename itself (that is expected / covered elsewhere) — it is that the **leftover old-binary process actively melts down WMI** via an unbounded taskkill retry loop.
Environment
- Claude Code: version not captured at incident time — the running binary had already been renamed to
claude.exe.old.<timestamp>, so--versionwas unavailable in-flight; the failure mode is the post-swap process behavior rather than a specific build. (Reporting so it is on record; happy to add the version if it can be recovered from the.oldfile.) - Install type: npm global — binary at
%APPDATA%\npm\node_modules\@anthropic-ai\claude-code\bin\claude.exe(this is the npm-global layout, distinct from the.local\binnative-installer layout seen in #9418 / #6021). - OS: Windows 11 Home, build 10.0.26200
- Host shell: PowerShell 7 (diagnostics run from PS7; Git Bash also present)
Timeline (local time)
- 18:05 — auto-update renames the running binary to
claude.exe.old.<timestamp>; the old process does not exit and enters thetaskkillspawn loop. - evening — machine-wide lag becomes noticeable as
WmiPrvSEsaturates and WMI consumers start queuing/hanging. - ~00:30 — diagnosis: identified the runaway
claude.exe.old.*process (~2,439 CPU-seconds accumulated) as the WMI flood source;Stop-Process -Forceon its PID stops the storm. - ~00:30 +90 s —
WmiPrvSEis still pinned (~390% CPU) with the source already gone → manually kill the wedgedWmiPrvSEPID (DCOM respawns it cleanly). - after — full recovery; WMI consumers responsive again.
Observed evidence — causal chain (each link tagged with its observation source)
- The
.oldprocess is alive and burning CPU. A leftoverclaude.exe.old.<timestamp>process was running with a large accumulated CPU total (~2,439 CPU-seconds). [Source: process census via PowerShell 7(Get-Process).Parentwalk (uses .NET, does not touch WMI); per-process CPU viatypeperf "\Process V2(*)\% Processor Time", whose instance names include the PID.Get-Processalone reports 0 CPU for elevated/SYSTEM-owned processes — a permissions artifact, not idle.]
- It spawns 1–2
taskkill.exeper second, continuously. Newly-createdtaskkill.exeprocesses appeared at ~1–2/sec for hours; their parent resolved to the leftover.oldprocess. [Source: polling new spawns in the process census + correlating the client PIDs in the WMI-Activity log to thetaskkillPIDs.]
- Each
taskkillperforms a fullWin32_ProcessWMIExecQuery. The WMI-Activity operational log filled with CIMWin32Win32_Processenumeration queries at the same rate as thetaskkillspawns. [Source:Microsoft-Windows-WMI-Activity/Operationalevent log — readable under a normal user account.] This matches the documentedtaskkill /Tbehavior (it builds the process tree via a fullWin32_Processscan), so each spawnedtaskkillamplifies one loop iteration into one WMI enumeration.
WmiPrvSEpegs ~400% and does not self-recover after the source is killed. The CIMWin32 provider hostWmiPrvSE.exesat at ~400% CPU during the storm and remained ~390% for 90+ seconds after the runaway.oldprocess was already terminated — no self-healing. [Source:typeperf "\Process V2(*)\% Processor Time"sampled ~90 s after the source kill.]
- Every WMI consumer on the machine hangs. While
WmiPrvSEwas wedged, Task Manager, Explorer shell components, and vendor sensor daemons (aida64 / iCUE / esrv) queued or hung — experienced as whole-machine stutter. Even local diagnostics hung for ~2 minutes during the wedge:Get-CimInstance(an actual WMI query), and — caught in the same machine-wide wedge rather than because they are WMI calls —Get-Counter '\Memory\*'(reads PDH performance counters, not WMI),schtasks /query(Task Scheduler API, not WMI), and baretaskkill. [Source: direct observation of those apps/daemons + hung diagnostic commands.]
- Killing the
.oldprocess AND the wedgedWmiPrvSErestores the machine.Stop-Process -Forceon the.oldPID stopped newtaskkillspawns; a secondStop-Process -Forceon the still-wedgedWmiPrvSEPID cleared the ~400% pin, and DCOM respawned a fresh provider host cleanly. [Source: post-kill observation — storm stopped, CPU normalized, consumers responsive.]
Root-cause hypothesis
A post-update cleanup / retry loop with no backoff, retry cap, or circuit breaker. The updater (or a cleanup routine running in the leftover old-binary process) appears to shell out to taskkill — most likely taskkill /PID <pid> /T /F — to tear down the old process/tree after the swap. Because the target never actually dies (the loop is running inside the very process it is trying to clean up, or the target PID never resolves), the kill "fails" and the routine retries immediately, forever, at 1–2/sec.
The amplification is the killer: taskkill /T walks the process tree via a full Win32_Process WMI enumeration, so an unbounded shell-out retry loop becomes an unbounded WMI query flood, which is what melts the single shared WmiPrvSE provider host and takes down every other WMI client on the box. (#73785 independently measured taskkill /T ≈ one full Win32_Process scan ≈ ~533 ms of provider CPU each; #67888 independently observed the same "retry loop when the previous kill never completes" shape.)
Suggested fixes
- Bound the cleanup loop: finite retry count + exponential backoff + a circuit breaker that gives up (and logs) instead of hammering
taskkillat 1–2/sec indefinitely. - Do not shell out to
taskkillfor process teardown. Use nativeTerminateProcessor spawn the old process into a Job Object so it can be killed directly — this avoids thetaskkill /TWMI tree-walk amplification entirely. (Same recommendation as #73785 / #67888 for a different call site.) - Detect the "old binary outlived the swap" case and stop retrying. If a cleanup routine is running inside the process it is trying to kill, or the target PID no longer exists, that is a terminal condition — break the loop, do not retry.
- Post-update cleanup should reap both the
.oldfile and the.oldprocess. Today the.oldfile is a known leftover (#65478, #9418); the process leftover is the gap this report adds. The updater should confirm the old process has actually exited (or terminate it via native API once), then remove the.oldfile. - Prefer native process enumeration (
CreateToolhelp32Snapshot) over any WMI-backed path for cleanup/liveness checks, so cleanup work can never saturate the shared WMI provider host.
Workaround deployed locally
A user-side scheduled watchdog now detects a high-CPU claude.exe.old* leftover and taskkill storms (WMI-free detection); after an initial alert-only observation window it auto-terminates confirmed .old runaways, and can optionally recycle a wedged WmiPrvSE once per incident when explicitly armed (out of scope for the fix; mentioned for completeness).
Appendix: duplicate-check table
Checked read-only via gh issue view / gh search issues (searches: claude.exe.old, taskkill loop, WmiPrvSE, WMI, auto-update runaway, spawn loop). This report's distinguishing signature is a **LIVE leftover .old process CPU runaway + WMI storm**, not a file-swap/lock/no-op-version issue and not a live-session load feedback loop.
| Issue | State | Why it is NOT this bug |
|---|---|---|
| #73785 | open | Nearest symptom twin (WmiPrvSE pinned by taskkill-driven Win32_Process scans), but the trigger is live load: statusline cancellation taskkill /T + cross-session liveness probes across ~9–12 concurrent interactive sessions during normal use. Mine is a single orphaned auto-update .old process looping on its own, no statusline, no session load. Same downstream mechanism, different root cause and fix surface. |
| #67888 | open | Same machine class (Win11 Home 26200) and same taskkill-retry shape, but there the taskkill processes hang and accumulate by the hundreds (process/memory leak) with a live claude.exe as parent. Mine: the taskkill processes complete at 1–2/sec (a query storm, not a leak), parent is the .old leftover, and the harm is a WmiPrvSE CPU meltdown, not accumulated hung kills. |
| #42705 | closed | WMI "corrupted" by abrupt thread killing at rate-limit, persisting across a full reboot. Different trigger (rate-limit thread termination), and mine clears once the source + WmiPrvSE are killed (no persistent corruption, no reboot). |
| #65478 | closed | Same .old naming artifact, but the failure mode is the swap window with no claude.exe killing concurrent sessions + CommandNotFoundException. It treats .old as a passive file; mine is the .old process actively running a taskkill runaway. File-side vs. process-side of the same rename. |
| #9418 | closed | Update leaves a 0-byte claude.exe beside the .old file — passive file corruption of the swap. No running process, no taskkill, no WMI. |
| #6021 | closed | Updater reports success but never replaces the active exe, so the old version keeps running — a stale-version bug. No runaway loop, no CPU/WMI meltdown. |
| #42897 | closed (dup) | Cowork MSIX/Store app: cowork-svc.exe respawned by services.exe after update, needs reboot. Different product and process; no taskkill runaway, no WMI storm. |
| #51954 | closed (dup) | Desktop/MSIX file-lock during package swap (CoworkVMService holds an exclusive lock), "Open with…" dialog, reboot/service-restart to fix. No running-process runaway, no WMI storm. |
| #67909 | open | Auto-updater EBUSY: 245 MB SEA locks its own dir during swap. File-lock during swap; no leftover-process runaway, no WMI. |
| #70039 / #74733 | closed / open | "Successfully updated" but the running binary/launcher is never swapped (infinite no-op updates / stays on old version). Version/no-op family; no runaway, no WMI. |
| #22172, #36343, #34568, #27415 | mixed | Various high-CPU / runaway-spawn / DoS reports, but none involves an auto-update .old leftover process or the taskkill→Win32_Process→WmiPrvSE WMI-storm chain (different triggers: parallel-instances+hooks, k hotkey, Bun posix_spawn, generic memory DoS). Listed for completeness. |