Repository navigation
Description
Environment
- OS: Windows (ConPTY path)
- node-pty version: 1.2.0-beta.10
- Node.js: v22.22.0
Description
After calling kill() on a Windows PTY and awaiting its exit event, Node.js cannot exit because active handles remain on the event loop. This forces consumers to call process.exit() as a workaround.
The root cause is that the ConoutConnection worker thread, its internal sockets, and several cleanup timeouts are never unref()'d. There is no .unref() call anywhere in node-pty's source.
Reproduction
const pty = require('node-pty');
const term = pty.spawn('cmd.exe', [], { cols: 80, rows: 24 });
term.onExit(() => {
console.log('exited');
// Node.js should exit here, but it hangs indefinitely
});
setTimeout(() => term.kill(), 500);Expected: process exits after "exited" is logged.
Actual: process hangs indefinitely.
Root cause
There are three categories of handles keeping the event loop alive after kill():
1. Worker thread (primary)
src/windowsConoutConnection.ts:47 — the Worker is created but never unref()'d:
this._worker = new Worker(join(scriptPath, 'worker/conoutSocketWorker.js'), { workerData });The worker runs a net.Socket + net.createServer (src/worker/conoutSocketWorker.ts:11-17) that are also never unref()'d. Both the worker thread and its internal sockets keep the event loop alive.
2. Drain timeout
When dispose() is called, it schedules a 1-second timeout before terminating the worker (src/windowsConoutConnection.ts:76):
this._drainTimeout = setTimeout(() => this._destroySocket(), FLUSH_DATA_INTERVAL);This timeout is not unref()'d, keeping the event loop alive for the duration.
A similar un-unref()'d timeout exists in WindowsPtyAgent._flushDataAndCleanUp() (src/windowsPtyAgent.ts:179):
this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL);3. I/O sockets
_outSocket and _inSocket in WindowsPtyAgent (src/windowsPtyAgent.ts:78-94) are never unref()'d.
Suggested fix
Call unref() on handles that shouldn't prevent process exit:
// windowsConoutConnection.ts — constructor
this._worker = new Worker(join(scriptPath, 'worker/conoutSocketWorker.js'), { workerData });
this._worker.unref();
// windowsConoutConnection.ts — _drainDataAndClose
this._drainTimeout = setTimeout(() => this._destroySocket(), FLUSH_DATA_INTERVAL);
this._drainTimeout.unref();
// windowsPtyAgent.ts — constructor (sockets)
this._outSocket.unref();
this._inSocket.unref();
// windowsPtyAgent.ts — _flushDataAndCleanUp
this._closeTimeout = setTimeout(() => this._cleanUpProcess(), FLUSH_DATA_INTERVAL);
this._closeTimeout.unref();This ensures cleanup completes if other work keeps the event loop alive, but doesn't prevent the process from exiting when everything else is done.
Activity
feat: isolate node-pty in child process on Windows
DrDexter6000 commentedon Apr 6, 2026
This issue's Windows ConOutConnection worker thread preventing Node.js from exiting after kill() may be related to a Node.js Windows runtime bug:
**nodejs/node#62125 ??Node.js v24.13.x on Windows silently terminates child processes spawned with
child_process.spawn({ detached: true })viaTerminateProcess()with no JS exit handlers, no diagnostic report, and no Event Log entry. The process simply vanishes after ~4-6 seconds.We independently verified this on Windows ( Chrome Native Messaging host chain):
setInterval(() => {}, 1000)in detached spawn dies in ~4-6s on v24.13.1TerminateProcess()bypasses all JS cleanupThe Note:
node-ptyalready has a Windows process lifecycle issues. If your ConoutConnection worker thread bug (#887) is related to Node.js v24+'s Windows process management regressions. The upstream bug in nodejs/node#62125 may help explain the silent termination behavior on Windows:setInterval(() => {}, 1000)in detached spawn dies in ~4-6s on v24.13.1 (zero I/O, zero business logic)nvm install 22 && nvm use 22DrDexter6000 commentedon Apr 6, 2026
This issue documents a Windows process lifecycle behavior that may be related to a Node.js v24/v25 Windows regression:
nodejs/node#62125 ??Node.js v24.13.x on Windows silently terminates child processes spawned with
child_process.spawn({ detached: true })viaTerminateProcess()with no JS exit handlers, no diagnostic report, and no Event Log entry. The process simply vanishes after ~4-6 seconds.We independently verified this on Windows (Chrome Native Messaging host chain):
setInterval(() => {}, 1000)in detached spawn dies in ~4-6s on v24.13.1 (zero I/O, zero business logic)TerminateProcess()bypasses all JS cleanupThe
node-ptyConoutConnection worker thread issue may be another symptom of Node.js v24+'s Windows process management regressions. Worth cross-referencing.Workaround:
nvm install 22 && nvm use 22DrDexter6000 commentedon Apr 6, 2026
Important Update: Windows Security Behavioral Detection Confirmed
After further investigation, we now have direct evidence that Windows Security (Defender) behavioral detection is killing detached child processes on Windows. This may be a contributing factor alongside any Node.js v24 runtime behavior.
Evidence
On 2026-04-06 at 08:23 local time, while running minimal detached spawn reproduction tests with Node.js v24.13.1 on Windows 11, Windows Security flagged and terminated the test script:
Trojan:Win32/SuspExec.SE(Suspicious Execution)The Windows Security Protection History shows the exact command line that was flagged. The test script was a minimal reproduction: spawn a detached
node -e 'setInterval(()=>{},1000)'child, then poll it withprocess.kill(pid, 0).Why This Pattern Triggers Detection
Windows Security behavioral analysis flags this pattern because it resembles C2 beacon/persistence behavior:
This is indistinguishable from malware persistence techniques at the behavioral level.
Key Observation: Other Tools Work Fine
On the same machine, other browser automation tools work without issues:
This suggests the issue is not purely a Node.js v24 runtime bug. If it were, all Node.js-based tools would be equally affected.
Revised Hypothesis
The root cause is likely a combination of factors:
detached: true + stdio: 'ignore' + child.unref()+ polling is exactly how many persistence implants work.Why Node v22 LTS Works
Node v22 LTS has been around longer. Windows Security has likely built behavioral whitelisting for common Node.js v22 spawn patterns. Node v24 is newer and its behavioral profile may not be fully recognized yet.
Updated Workaround
In addition to using Node v22 LTS, also consider:
nvm install 22 && nvm use 22This finding also explains the "invisible hand" effect we observed: processes dying after ~4-6 seconds with zero diagnostic output, zero JS exit handlers firing, and zero Event Log entries. Windows Security
TerminateProcess()produces exactly this symptom profile.DrDexter6000 commentedon Apr 6, 2026
TL;DR: Our original diagnosis was wrong. The actual root cause was Windows Defender behavior detection (
Trojan:Win32/SuspExec.SE), not Node.js v24 silently terminating detached processes.What Changed
After adding Windows Defender exclusions for
OpenCode.exe,opencode-cli.exe,node.exe, and their install directories:The same Node v24.13.1 binary, same code, same machine — the only change was Defender exclusions.
Corrected Diagnosis
TerminateProcess()Trojan:Win32/SuspExec.SEand kills themWhy the Misdiagnosis
TerminateProcess()was called — but by Windows Defender, not Node.js internalsTerminateProcess()is a forced kill, identical behavior to what we attributed to NodeEvidence Log
Trojan:Win32/SuspExec.SEdetected during debugging sessionsetIntervaldetached spawn on v24.13.1Apologies for the initial misattribution. The nodejs/node#62125 issue may still be valid for other reporters — we can only correct our own reproduction case.
Root cause analysis documented at: [Gist link TBD]
fix: guard PTY operations against errors from exited processes (elira…
fix: force-kill worker pools and ptys on timeout/signal to prevent Wi…