Repository navigation
Windows crash on Node.js v22 – "Cannot resize a pty that has already exited" #827
Description
Activity
That error comes from here:
node-pty/src/windowsPtyAgent.ts
Lines 126 to 128 in 74ee292
if (this._exitCode !== undefined) { throw new Error('Cannot resize a pty that has already exited'); } It's just a thrown node exception, not a process crash? Your stack trace is also a node error trace?
SkybuckFlying commented
on Dec 20, 2025 AuthorMore actionsThank you for your feedback, just to be thorough I shall post a possible solution for this problem:
Here is the refined technical report, formatted for a GitHub Issue or Pull Request. It includes the stack trace as evidence and clearly distinguishes between a library logic error and a native crash.
Issue: Process crash (Exit Code 1) due to unhandled 'already exited' error in
resize()Summary
The application experiences a fatal crash when a resize event occurs simultaneously with or immediately after the PTY process terminates. This is not a native memory crash (segfault), but a synchronous JavaScript exception thrown by
node-ptythat goes uncaught, causing the Node.js runtime to perform an emergency shutdown.Evidence: Stack Trace
The following trace confirms that the crash is triggered by the
WindowsPtyAgentguard clause during a socket-driven event:G:\Tools\npm-global\node_modules\@google\gemini-cli\node_modules\@lydell\node-pty\windowsPtyAgent.js:93 throw new Error('Cannot resize a pty that has already exited'); ^ Error: Cannot resize a pty that has already exited at WindowsPtyAgent.resize (G:\Tools\npm-global\node_modules\@google\gemini-cli\node_modules\@lydell\node-pty\windowsPtyAgent.js:93:19) at WindowsTerminal.<anonymous> (G:\Tools\npm-global\node_modules\@google\gemini-cli\node_modules\@lydell\node-pty\windowsTerminal.js:131:26) at Object.run (G:\Tools\npm-global\node_modules\@google\gemini-cli\node_modules\@lydell\node-pty\windowsTerminal.js:167:50) at G:\Tools\npm-global\node_modules\@google\gemini-cli\node_modules\@lydell\node-pty\windowsTerminal.js:73:32 at Array.forEach (<anonymous>) at Socket.<anonymous> (G:\Tools\npm-global\node_modules\@google\gemini-cli\node_modules\@lydell\node-pty\windowsTerminal.js:68:42) at Socket.emit (node:events:536:35) at addChunk (node:internal/streams/readable:561:12) at readableAddChunkPushByteMode (node:internal/streams/readable:512:3) at Readable.push (node:internal/streams/readable:392:5) Node.js v22.12.0Technical Root Cause
The crash is located in
node-pty/src/windowsPtyAgent.ts(or the equivalent.jsin the@lydell/node-ptyfork):// Lines 126-128 in node-pty source if (this._exitCode !== undefined) { throw new Error('Cannot resize a pty that has already exited'); }
- Race Condition: The PTY sets
_exitCodewhen the shell process terminates. - Delayed Event: A pending resize event (triggered by the terminal UI or a socket buffer flush) calls
.resize()on the PTY instance. - Fatal Exception:
node-ptythrows an explicitError. Because the calling logic inwindowsTerminal.jsdoes not wrap this in atry/catch, the exception bubbles up to the Node.js global scope. - Runtime Shutdown: Node.js v22.x treats uncaught synchronous exceptions as fatal, terminating the process to prevent an inconsistent state.
Proposed Solution
The PTY manager should defensively handle the possibility that a resize request may arrive after the process has died.
Recommended Fix:
Wrap the agent resize call inwindowsTerminal.jswith atry/catchblock to safely ignore this specific lifecycle error:// Suggested patch for windowsTerminal.js try { this._agent.resize(cols, rows); } catch (e) { if (e instanceof Error && e.message === 'Cannot resize a pty that has already exited') { // Log as warning or ignore; the process is already dead so resize is moot. return; } throw e; // Re-throw any other unexpected errors }
Would you like me to help you draft the specific "Steps to Reproduce" section for the bug report?
Background information:
To understand what a PTY (Pseudo-Terminal) does, you have to look at how software communicates with a computer.
Normally, a program (like
lsorgrep) just takes input and spits out output. But some programs—like interactive shells (bash, powershell), text editors (vim, nano), or REPLs (Node, Python)—expect to be talking to a real human sitting at a physical terminal. They need to handle things like window resizing, keyboard shortcuts (Ctrl+C), and cursor movement.A PTY is the "middleman" that tricks these programs into thinking they are attached to a real terminal.
The Two Sides of a PTY
A PTY is actually a pair of devices that act like a bidirectional pipe:
- The Master (The "Controller"):
This is the side held by the Terminal Emulator (like VS Code's terminal, CMD, or your Gemini CLI). It sends user input (keystrokes) and receives the program’s output to display on the screen. - The Slave (The "Fake Terminal"):
This is the side held by the Shell or Process (likebash.exe). To the shell, the Slave side looks and behaves exactly like a hardware serial port or a physical teletype machine.
Key Responsibilities of a PTY
1. Line Discipline (The "Cooked" Mode)
When you type
helloand hit backspace to fix a typo, the PTY handles that. It doesn't send the "h-e-l-l-o-backspace" characters to the program immediately. It "cooks" the input, allowing you to edit the line, and only sends the final string to the program once you hit Enter.2. Control Signal Handling
When you press Ctrl+C, you aren't sending a "C" character. You are sending a signal. The PTY recognizes this special key combination and sends a
SIGINT(Interrupt) to the running process to stop it.3. Terminal Emulation & Layout
The PTY keeps track of the column and row count.
- When you resize your window, the "Master" tells the PTY the new dimensions.
- The PTY then sends a signal (
SIGWINCH) to the "Slave" process. - The process (like
vim) receives that signal and redraws its UI to fit the new size.
Note: This is exactly where your crash was happening. The "Master" (Gemini CLI) tried to tell the PTY the window dimensions changed, but the "Slave" (the shell) had already died and left the building.
4. Echoing
When you type a character, it doesn't automatically show up on your screen. The PTY "echoes" the character back to the Master so the terminal emulator knows to draw a "A" when you press the A key.
Why do we need
node-pty?Node.js normally uses
child_process.spawn, which uses standard "Pipes" (stdin/stdout). Pipes are "dumb"—they don't understand window resizing or Ctrl+C.If you tried to run
vimor an interactivepythonshell through a standard Node.js pipe, it would likely fail or look broken because those programs would realize they aren't attached to a "real" terminal.node-ptyprovides the native bindings to create a "real" PTY handle, allowing Node.js apps to host fully functional interactive terminals.- Race Condition: The PTY sets
We would be throwing regardless which I think is good behavior. You should try/catch on your end so you can handle the case when resizing something that cannot be resized.
SkybuckFlying commented
on Dec 29, 2025 AuthorMore actionsWe would be throwing regardless which I think is good behavior. You should try/catch on your end so you can handle the case when resizing something that cannot be resized.
The try/catch approach was implemented but the program still crashes, the AI seems to understand why, see below for full explanation, there is more going on then it seems at first glance, it's quite hard/complex to understand, it's some kind of interaction between the c++ world, the operating systems exceptions, the binding, and the fact that java script engine/node.js does not like handling os exceptions and simply crashes because they cannot garantue proper function.
Final bug fix by me and the AI going to do a pull request soon, this bug fix should be a bit more reliable if error message changes and also makes the race condition window very small for the isPtyActive and such, the try catch blocks are finally the end cure: /** * Resizes the pseudo-terminal (PTY) of a running process. * * @param pid The process ID of the target PTY. * @param cols The new number of columns. * @param rows The new number of rows. */ static resizePty(pid: number, cols: number, rows: number): void { // IMPORTANT CHANGE: Fetch the PTY reference once and guard on it immediately. // This eliminates a subtle race condition present in the original code. // Original: Checked isPtyActive(pid) first, then later fetched activePtys.get(pid). // Between those two calls, the entry could be removed from the Map (e.g., by an async exit handler), // leading to activePty being undefined even after the PID check passed. // New: Single lookup + combined guard ensures atomicity and consistency. const activePty = this.activePtys.get(pid); if (!activePty || !this.isPtyActive(pid)) { return; } try { activePty.ptyProcess.resize(cols, rows); activePty.headlessTerminal.resize(cols, rows); } catch (e) { // Safely ignore the known race-condition error // Ignore errors if the pty has already exited, which can happen // due to a race condition between the exit event and this call. if ( e instanceof Error && ((e.message.toLowerCase().includes('pty') && e.message.toLowerCase().includes('exited')) || // Windows (robust against wording changes) ('code' in e && (e as { code?: string }).code === 'ESRCH')) // Unix ) { return; // Safe to ignore - process already exited - resize is irrelevant } // For other unexpected errors, re-throw throw e; } } /** * Scrolls the pseudo-terminal (PTY) of a running process. * * @param pid The process ID of the target PTY. * @param lines The number of lines to scroll. */ static scrollPty(pid: number, lines: number): void { const activePty = this.activePtys.get(pid); if (!activePty || !this.isPtyActive(pid)) { return; } try { activePty.headlessTerminal.scrollLines(lines); if (activePty.headlessTerminal.buffer.active.viewportY < 0) { activePty.headlessTerminal.scrollToTop(); } } catch (e) { // Ignore errors if the pty has already exited, which can happen // due to a race condition between the exit event and this call. // Ignore known exit-related errors (similar to resize) if ( e instanceof Error && ((e.message.toLowerCase().includes('pty') && e.message.toLowerCase().includes('exited')) || // Windows (robust against wording changes) ('code' in e && (e as { code?: string }).code === 'ESRCH')) // Unix ) { return; } throw e; } }$ shell ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ⊶ Shell Command shell │ │ │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ A new version of Gemini CLI is available! 0.21.0-nightly.20251220.41a1a3eed → 0.22.2 │ │ Running from a local git clone. Please update with "git pull". │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ⠹ Channeling the Force... (esc to cancel, 0s) Using: 2 GEMINI.md files shell mode enabled (esc to disable) ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ! Type your shell command │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ M:\GeminiCLI\Branch\Develop\Typescript (fix/windows-pty-resize-crash-14557*) no sandbox (see /docs) Auto (Gemini 3) /model M:\GeminiCLI\Branch\Develop\Typescript\node_modules\@lydell\node-pty\windowsPtyAgent.js:93 throw new Error('Cannot resize a pty that has already exited'); ^ Error: Cannot resize a pty that has already exited at WindowsPtyAgent.resize (M:\GeminiCLI\Branch\Develop\Typescript\node_modules\@lydell\node-pty\windowsPtyAgent.js:93:19) at WindowsTerminal.<anonymous> (M:\GeminiCLI\Branch\Develop\Typescript\node_modules\@lydell\node-pty\windowsTerminal.js:131:26) at Object.run (M:\GeminiCLI\Branch\Develop\Typescript\node_modules\@lydell\node-pty\windowsTerminal.js:167:50) at M:\GeminiCLI\Branch\Develop\Typescript\node_modules\@lydell\node-pty\windowsTerminal.js:73:32 at Array.forEach (<anonymous>) at Socket.<anonymous> (M:\GeminiCLI\Branch\Develop\Typescript\node_modules\@lydell\node-pty\windowsTerminal.js:68:42) at Socket.emit (node:events:536:35) at addChunk (node:internal/streams/readable:561:12) at readableAddChunkPushByteMode (node:internal/streams/readable:512:3) at Readable.push (node:internal/streams/readable:392:5) Node.js v22.12.0 PS M:\GeminiCLI\Branch\Develop\Typescript>Unfortunately a deeper bug has been found:
This is a different crash, coming from node‑pty itself, before your code even gets a chance to intercept it.
Let me walk you through what just happened with absolute clarity.
🧩 1. This crash is NOT coming from your patched code
Your fix lives in:
packages/core/src/services/shellExecutionService.tsBut the crash stack trace is coming from:
node_modules/@lydell/node-pty/windowsPtyAgent.js node_modules/@lydell/node-pty/windowsTerminal.jsThis means:
❌ The resize event is reaching node‑pty before your wrapper code can catch it.
❌ node‑pty is throwing synchronously inside its internal resize handler.
❌ Your try/catch cannot intercept this because it’s happening inside the native binding callback.
This is a lower‑level crash than the one you fixed.
🧩 2. The exact error
Error: Cannot resize a pty that has already exitedThis is thrown by node‑pty when:
- the PTY process has already died
- but the terminal still receives a resize event
- and node‑pty tries to forward the resize to a dead handle
This is a known Windows‑specific race condition.
🧩 3. Why your fix didn’t catch this
Your fix guards the Gemini‑CLI layer, not the node‑pty native layer.
But this crash happens inside the native callback, before your code runs.
Your fix prevents your code from calling resize on a dead PTY.
But node‑pty itself is also listening for resize events and doing its own resize call.That’s the one that exploded.
🧩 4. What this means
You’ve uncovered a second bug, deeper than the one you fixed.
The sequence is:
- Gemini CLI opens a PTY
- You resize the terminal
- The PTY exits (maybe instantly, maybe due to a race)
- Windows Terminal sends a resize event
- node‑pty receives the resize
- node‑pty tries to resize a dead PTY
- node‑pty throws
- The whole process crashes
Your fix only prevents Gemini‑CLI from calling resize after exit.
It does not prevent node‑pty from doing the same.
🧩 5. The real root cause
node‑pty’s Windows backend does not guard against:
- resize events arriving after exit
- resize events arriving during teardown
- resize events arriving before initialization
This is a long‑standing issue in node‑pty.
🧩 6. What we need to do next
To fully fix this, we need to:
✔ Patch node‑pty’s Windows backend
Specifically in:
windowsPtyAgent.js windowsTerminal.jsAdd guards like:
if (this._exited) return;
before calling:
this._agent.resize(cols, rows);
✔ OR intercept resize events earlier
Inside Gemini‑CLI, before they reach node‑pty.
Your fix worked — but you just uncovered a deeper, upstream bug in node‑pty itself.
SkybuckFlying commented
on Dec 29, 2025 AuthorMore actionsExplanation about (a)synchronous JavaScript exception in the context of c/c++ addons for node.js:
In JavaScript, a synchronous exception is an error that occurs during the immediate execution of a block of code.1 Because JavaScript is single-threaded, when a synchronous exception is "thrown," the engine stops what it is doing and looks for an error handler (a
try...catchblock).If no handler is found, the script crashes or stops executing that specific task.
How It Works: The Call Stack
When you run synchronous code, each function call is added to the Call Stack.2 If an error happens, JavaScript starts "bubbling" that error up the stack. It checks the current function for a
catchblock; if it doesn't find one, it pops that function off and checks the caller, and so on.Common Examples
Synchronous exceptions usually stem from syntax errors, logic flaws, or trying to access something that doesn't exist.
ReferenceError: Using a variable that hasn't been declared.
TypeError: Performing an operation on the wrong data type (e.g., calling a method on
null).3RangeError: Passing a number to a function that is outside the allowed range.
JavaScriptfunction checkInventory() { // This will throw a synchronous TypeError because 'item' is undefined let item; console.log(item.name); }try {
checkInventory();
} catch (error) {
console.error("Caught a sync error:", error.message);
}
Synchronous vs. Asynchronous Exceptions
The distinction is vital because standard
try...catchblocks cannot catch asynchronous exceptions.Feature Synchronous Exception Asynchronous Exception Detection Happens immediately during execution. Happens later (e.g., in a setTimeout or Promise). Handling Caught easily with try...catch. Requires .catch() or await within a try block. Stack Trace Provides a clear path of how you got there. Can sometimes be "lost" or disconnected from the original caller. The Catch-22 of Async:
If you wrap a setTimeout in a try...catch, the try block finishes executing before the error inside the timeout even happens. This is why synchronous error handling logic is insufficient for modern, API-heavy web apps.
Key Takeaway
A synchronous exception is a "stop-the-world" event. It follows the linear path of your code. To handle it effectively, you should wrap risky logic (like parsing JSON or accessing deep object properties) in
try...catchblocks to ensure your application can recover gracefully.4Would you like to see how to properly handle exceptions within Asynchronous code using
async/await?SkybuckFlying commented
on Dec 29, 2025 AuthorMore actionsFor me as a none-javascript, none-typescript, none node.js programmer this is hard to understand, but I can understand some of it.
To me it seems like both are crashing at the same time. Both are trying to re-size. One is handled, the other one is not and still produces a second exception crashing the main program.
Perhaps you can figure it out... this is some very complex interaction going on between the addon, api, binding, etc, plus operating system events, etc, very difficult.
SkybuckFlying commented
on Dec 29, 2025 AuthorMore actionsOne last note, according to the AI, all other PTY solutions do not throw on this kind of re-size event when the PTY is already exited... however I want to give you the benefit of the doubt, I kinda like that you are trying to inform the user that it might be doing something wrong, however this is creating a very serious problem, and according to the AI, the user cannot do anything about it.
As also demonstrated by this try/catch solution which didn't actually solve the deeper problem. So in this case, it's literally impossible for the typescript/javascript developers to solve this throw ? and thus might as well leave it out.
I believe looking at the c/c++ code that it probably also does a throw on a resize when it failed.
So perhaps it's just an example of unnecessary "throws" or at least a situation where throwing could be considered bad.
Ultimately this is a philosophical dilemma, should the java script engine itself/node.js itself handled it, they have already decided not to because they cannot garantue virtual machine correct function if an add-on misbehaves.
Maybe there is a different way of notifieing the user of this condition/error, or perhaps a compromise solution would be to include a little boolean which can be set, to throw or not throw for this particular situation so that this crash can be avoided.
However I must warn you the AI also detected other potential race conditions. For this reason I have started an alternative implementation: Delphi-pty, it's on github as well on my github account.
I will try and integrate this new AI driven solution into gemini-cli to see if it can solve this problem.
I could have maybe simply fixed it, but disabling throw lines, only to discover later that this node-pty has other potential crashes and I really want gemini-cli to be rock solid, so I am going the route of re-implementing it, I was under the impression that AI was going to use a new API for PTYs, now I am not so sure anymore, maybe it kinda lied to me, I don't know. Soon I will find out if this delphi-pty works or not for gemini-cli... it would still be nice if a this project gets fixed as a fall back option, especially because it might be more cross platform and maybe my Delphi-pty solution might not be accepted by gemini-cli developers...
According to the AI the Delphi-pty implementation might be faster. I am not sure if that is true. Meanwhile it also implemented a minimalistic c/c++ version for debugging purposes.
The AI also complained that not all error return values were being checked in this node-pty code. Not sure anymore if that is really true ?!
Here is the new implementation, mostly in Delphi and some C/C++ typescript and javascript bindings:
https://github.com/SkybuckFlying/Delphi-PtySince you have more experience with this I would be interesting if you could take a look at it, to see what your initial thoughts are ?
Would it be better or worse ? does it use the same APIs ? Or indeed newer ? How is the return value checking ?
Maybe this project can even learn from the AI.
The latest and greatest AI was used: Gemini 3.0 Pro, Gemini 3.0 Flash and Claude 4.5 Sonnet.
I found it impressive how the AI was capable of doing everything and even debugging it.
I am not yet sure if it's 100% correct/debugged.
One thing which is kinda odd: the cmd.exe window does not re-size on re-size command, but apperently that is normal, the multi-tab version of cmd.exe does not give that kind of control ? Again not sure. I am very new to this and just using the AI to try and make sense of it. This is definetly one of the most complex things I've look at during my life time. (Personal note, I've had a long day, so my mind is slightly gone, but I am doing my best to make some sense of it, forgive me if it's not 100% crystal clear (combination of fatigue+complexity :), I wish I could do better/understand better, cheers).
SkybuckFlying commented
on Dec 29, 2025 AuthorMore actionsWhile taking to the Gemini AI from Gemini-CLI this bug can be triggered by asking the AI to do: "echo hi".
This will cause the AI to open a pty and a cmd.exe, after cmd.exe exits a resize event happens from inside gemini cli I think and this causes an "exception" to bubble up through the stack, now I can remember... this is a c/c++ kind of exception it must be caught if not c/c++ will unwind the stack, javascript/nodejs kinda detects this bubble and ultimately crashes it.
Anyway here is the AI explanation:
You’re circling the right failure mode, but the mechanism is a bit different from “the OS sends a resize because the window disappears.” Let me give you the precise, technical version of what’s actually happening — because the real cause is subtler and more interesting.
🧩 What actually happens (and why it crashes)
✔ 1.
cmd.exe(orbash) is spawned inside a PTY — not a real windowThere is no visible terminal window, so nothing is “disappearing.”
A PTY is just a pair of file descriptors:
- master side (Node.js)
- slave side (shell process)
No GUI. No OS window.
So the OS does not send a resize event because of a disappearing window.
🧩 2. The resize event comes from Gemini‑CLI itself, not the OS
Gemini‑CLI does this:
- It spawns a PTY
- It attaches a resize handler
- It tries to keep the PTY width in sync with its own UI
- It may reflow output or adjust width after the command runs
This resize is internal, not OS‑driven.
🧩 3. The crash happens because the resize fires after the PTY process exits
This is the real race:
cmd.exerunsecho hi- It exits instantly
- Gemini‑CLI tries to resize the PTY (for formatting)
node-ptychecks if the PTY is alive- It isn’t
- Native C++ throws:
“Cannot resize a pty that has already exited” - JavaScript also throws because the native exception bubbles up
- Gemini‑CLI doesn’t catch it
- Crash
So you get what feels like a “dual throw,” but it’s really:
- One throw from C++
- Propagated into JS
- Uncaught
- Process terminates
🧩 4. Why it feels like two throws
Because:
- The C++ layer throws synchronously
- Node.js converts that into a JS exception
- The JS exception is also thrown synchronously
- The CLI prints both stack traces or error messages
So it looks like two errors, but it’s one error traveling through two layers.
🧠 So your hypothesis was close, but the real cause is:
❌ Not the OS
❌ Not a disappearing window
❌ Not a dual throw from two independent sources
✔ It’s a race condition between:
- a fast‑exiting PTY child process
- a late resize call from Gemini‑CLI
- a synchronous native exception in
node-pty - an uncaught JS exception in the CLI
That’s the whole story.
If you want, I can sketch the exact event timeline as an ASCII diagram so you can see the race visually.
!!!
Additionally:
!!!
Why try catch does not stop it from crashing:
!!!
Here’s the real reason your try/catch in JavaScript or TypeScript does nothing to stop the Gemini‑CLI crash — and it’s one of those deep Node.js truths that bites every engineer at least once.
I’ll give you the clean, technical explanation without fluff.
🧩 1. The crash happens before your JS try/catch ever runs
The exception is thrown inside native C++ code in
node-pty, using something like:Napi::Error::New(env, "Cannot resize a pty that has already exited").ThrowAsJavaScriptException();
When a native module throws like this:
- It does not unwind into your JS try/catch
- It unwinds into the Node.js event loop
- If nothing catches it at the native boundary, Node terminates the process
Your try/catch is too far up the call stack to intercept it.
🧩 2. The resize happens asynchronously — outside your try/catch block
Even if you wrap the shell call:
try { pty.resize(100, 40); } catch (e) { console.log("caught"); }
Gemini‑CLI doesn’t call
resize()inside your code.It calls it:
- in an event handler
- in a callback
- in a promise chain
- in a UI reflow step
- in a terminal update loop
Your try/catch only protects synchronous code inside its own block.
The resize happens later, in a different tick of the event loop.
So your try/catch never sees it.
🧩 3. Native exceptions thrown during async callbacks bypass JS try/catch entirely
This is the killer detail.
When C++ throws during an async callback:
- Node tries to convert it to a JS exception
- But there is no active JS frame to catch it
- So Node treats it as a fatal exception
- The process crashes
This is why you see:
- A native stack trace
- A JS stack trace
- Then the whole CLI dies
🧩 4. Even if you wrap the entire tool call, it still won’t help
Because the crash is happening in a different call stack.
Your code:
try { runShellTool(); } catch (e) { console.log("caught"); }
Gemini‑CLI’s internal code:
spawn pty → process exits → resize event fires → C++ throws → Node diesYour try/catch is not on the stack when the resize happens.
🧩 5. This is why the crash feels “uncatchable”
Because it is.
It’s a native exception thrown during an async callback, which is one of the few things that can still kill a Node.js process instantly.
🧠 The real fix (conceptually)
To prevent the crash, the CLI must:
✔ Check if the PTY is alive before resizing
✔ Wrap the resize call in a try/catch inside the callback
✔ Or patch the native module to ignore resize-after-exit
✔ Or disable resize events entirely for short-lived commands
This must be done inside Gemini‑CLI, not in your code.
🧩 In one sentence
Your try/catch doesn’t work because the crash happens in native C++ code during an asynchronous callback, outside the JavaScript call stack your try/catch controls.
If you want, I can show you the exact call chain inside
node-ptythat makes this exception uncatchable, or sketch a patch that would make Gemini‑CLI immune to this race condition.But the crash stack trace is coming from:
node_modules/@lydell/node-pty/windowsPtyAgent.js
node_modules/@lydell/node-pty/windowsTerminal.jsThat's an outdated fork of this project, have you tried using actual
node-ptyon npm?- addedbugIssue identified by VS Code Team member as probable bugIssue identified by VS Code Team member as probable bughelp wantedIssues identified as good community contribution opportunitiesIssues identified as good community contribution opportunitiesand removed
on Dec 30, 2025 If you can put together a repro case and a fix it'll probably get merged.
This is on hold and waiting for an external fix, if this impacts Gemini CLI in a bad way folks who use or maintain that would ideally contribute the fix as it's not impacting us AFAIK.
jackwotherspoon commented
on Jan 5, 2026 More actionsGemini CLI team member here. What kind of fix is needed in the node-pty lib in your mind for this?
We are indeed seeing this crash quite frequently with Gemini CLI users.
Let me know, happy to try and contribute a fix.
Jack Wotherspoon (@jackwotherspoon) the analysis above looks like it's on to something, it's a lot to read though 😅. Try set up gemini CLI to point at a local clone of node-pty and then try debug with a debugger or logging to repro the crash and then figure out how to mitigate it. I imagine it'll look something like
if (this.isDisposed) { return }in TS or something in pty.cc to double check a native call won't crash.- added a commit that references this issue
on Mar 11, 2026 Gemini CLI team member here. What kind of fix is needed in the node-pty lib in your mind for this?
We are indeed seeing this crash quite frequently with Gemini CLI users.
Let me know, happy to try and contribute a fix.
I fixed it in my fork (euxaristia/node-pty), but I accidentally forked a repackage of this repo. So I'll be contributing it here asap.
- added 2 commits that reference this issue
on Mar 13, 2026 - added a commit that references this issue
on Jun 7, 2026
Environment:
Description:
When running @google/gemini-cli on Windows 11 with Node.js v22, the process crashes with the following error:
Error: Cannot resize a pty that has already exited
at WindowsPtyAgent.resize (node_modules/@lydell/node-pty/windowsPtyAgent.js:93:19)
at WindowsTerminal. (node_modules/@lydell/node-pty/windowsTerminal.js:131:26)
at Object.run (node_modules/@lydell/node-pty/windowsTerminal.js:167:50)
...
Steps to reproduce:
npm install -g @google/gemini-cli).Expected behavior:
Resize calls after PTY exit should fail gracefully or be ignored, not crash the entire process.
Actual behavior:
Process crashes with uncaught error "Cannot resize a pty that has already exited".
Notes:
gyp_rebuild=true) does not resolve the runtime crash.Request:
Please add compatibility support for Node.js v22 on Windows, or handle PTY resize calls more gracefully after exit.