wmic is gone and your Node process tree is silently wrong on Windows 11 A developer discovered that the Windows 11 removal of WMIC silently breaks Node.js process tree utilities like pidtree, causing process trees to appear empty. The issue stems from pidtree 0.6.0 shelling out to wmic, which fails with ENOENT on modern Windows, and the failure is silently swallowed by Promise.allSettled, making it look like a process has no children. The developer recommends upgrading to pidtree 1.0.0 or using a single Get-CimInstance snapshot for efficient process tree queries. The last post I wrote here ended on a line I had to go and check: killing a shell does not kill what the shell started. This is what I found underneath it, and one of the things is a dependency a lot of people have without knowing. Numbers below come from Windows 11 Home, build 26200, in August 2026. Run the commands yourself, they take a minute. I work on NestMux, which is where I hit this, but nothing here is specific to it. A terminal pane running a coding agent reported 77 MB of memory. That is roughly the PowerShell host and nothing else. The agent inside it, and the dev server the agent had started, were not being counted. So the process tree was empty. Every pane looked like an idle shell. Get-Command wmic Get-Command : The term 'wmic' is not recognized as the name of a cmdlet... Microsoft deprecated WMIC in Windows 10 21H1 and started removing it from the image in Windows 11 22H2. On a current install it is simply not there. This is documented and it was announced, and it is still going to break things for years, because wmic is buried inside libraries rather than in the code people wrote. pidtree is the standard way to get a process tree from Node. It backs pkill -style behavior in a lot of tooling and it has millions of downloads a week. On Windows, version 0.6.0 shells out to wmic . js npm i pidtree@0.6.0 node -e "require 'pidtree' process.pid,{root:true} .then console.log .catch e= console.log 'FAIL:',e.message " FAIL: spawn wmic ENOENT Version 1.0.0 fixed it. It still tries wmic first, and falls back to PowerShell Get-CimInstance when that fails: js npm i pidtree@1.0.0 node -e "const m=require 'pidtree' ; m.default||m process.pid,{root:true} .then console.log " OK 30520, 36000, 29244 So check what you actually resolve, not what your package.json says. A caret range of ^0.6.0 will never reach 1.0.0. That was our case: the range said ^0.6.0 , the lockfile said 0.6.0 , and every call on Windows was throwing ENOENT into a Promise.allSettled that treated the rejection as "no children" and moved on. It was not an error anyone saw. It was a tree of size one. That is the part worth repeating. This failure does not look like a failure. It looks like a process with no children. If you cannot upgrade, the replacement is Get-CimInstance Win32 Process . The instinct is to query per PID with a filter. Do not: one snapshot of everything Get-CimInstance Win32 Process | Select-Object ProcessId,ParentProcessId 396 processes in 200 ms five filtered queries 1..5 | ForEach-Object { Get-CimInstance Win32 Process -Filter "ParentProcessId=$PID" } 517 ms One full snapshot of 396 processes costs less than five filtered queries. The cost is in starting the CIM call, not in the rows. Take one snapshot per polling cycle, build a parent - children map from it, and walk the map in memory for every PID you care about. If you are polling several panes every few seconds, this is the difference between a background cost you can ignore and one you cannot. Now the part from the previous post, with the actual output. Start a grandchild. Root cmd spawns a cmd , which spawns timeout : php $g = Start-Process cmd -PassThru -WindowStyle Hidden -ArgumentList '/c','cmd /c timeout /t 90 /nobreak' @ Get-CimInstance Win32 Process -Filter "Name='timeout.exe'" .Count 1 Kill the root and only the root: Stop-Process -Id $g.Id -Force @ Get-CimInstance Win32 Process -Filter "Name='timeout.exe'" .Count 1 Still alive. Windows has no process groups in the POSIX sense and no automatic reparenting cleanup. Stop-Process , and process.kill pid from Node, both terminate exactly one process. The descendants keep running, holding their file handles and their ports, with a parent PID that now points at a dead process. taskkill /F /T walks it properly: taskkill /F /T /PID $g.Id SUCCESS: the process with PID 29828 child process of PID 34112 has been terminated. SUCCESS: the process with PID 39928 child process of PID 37072 has been terminated. SUCCESS: the process with PID 34112 child process of PID 37072 has been terminated. SUCCESS: the process with PID 37072 child process of PID 39620 has been terminated. Deepest first, then up. That ordering is the whole point: it resolves the tree while the links are still intact. Which gives you the trap. If you kill the root first and then go looking for the descendants, the links you needed are gone. The orphans are still there, they are still holding whatever they were holding, and nothing connects them to the thing you were trying to clean up. Resolve the tree first, or hand the root to taskkill /F /T and let it do both. One hazard while you are in here: Windows reuses PIDs, and aggressively. A PID you cached thirty seconds ago and are about to force-kill may now be something else. If you keep a PID map across polling cycles, validate it against a fresh snapshot before you kill anything. The uncomfortable case. A coding agent starts a dev server in the background. On Windows that server frequently ends up with no usable ancestry back to the pane: the intermediate process has exited, the parent PID is stale or recycled, and the binary path is just node.exe . The command line does not carry the worktree it belongs to either. What is left is the process's current directory. If the server's cwd is inside a given worktree, it belongs to that worktree. There is no Node API for reading another process's cwd on Windows. On Linux it is /proc/