What happened?
gemini-cli (v0.22.5) appears to be leaking Pseudo-Terminal (PTY) master handles (/dev/ptmx). Over time, this exhausts the macOS system limit (kern.tty.ptmx_max), which is typically 511. When I started debugging the problem i had 1058.
I think when my count crossed 1023 PTYs, I got the following problems in creating new tabs/sessions:
- iTerm2 presented a dialog when trying to open a new session:
"A session ended very soon after starting. Check that the command in profile "Default" is correct."
- Terminal.app displayed:
[forkpty: Device not configured] and [Could not create a new process and open a pseudo-tty.]
Investigation via lsof revealed that multiple node processes running gemini were holding hundreds of open handles to /dev/ptmx. One specific session had leaked 155 PTY handles and was holding a total of 495 file descriptors. Some of these sessions had been running for over two weeks (since Dec 22). (Though I had run /clear a few times, based on what i'm seeing in the json trajectories)
What did you expect to happen?
I expected gemini-cli and its associated processes (including MCP servers and node-pty instances) to correctly close file descriptors and PTY handles when they are no longer in use, and to terminate cleanly instead of remaining alive as idle processes that accumulate leaked resources. ;)
Client information
Client Information
> /about
│ CLI Version 0.22.5 │
│ Git Commit 8daf2d34b │
│ Model gemini-3-flash-preview │
│ Sandbox no sandbox │
│ OS darwin │
│ Auth Method gemini-api-key
- Platform: macOS 15.7.3 (arm64)
- Binary Path:
~/.homebrew/bin/gemini - installed via npm install -g
- Node version: v24.11.1
- Dependency: @lydell/node-pty-darwin-arm64 (version: 1.1.0)
Login information
Logged in via GEMINI_API_KEY environment variable.
Anything else we need to know?
The leak may be correlated with the usage of MCP servers. In one instance, a gemini process (PID 46302) was the parent of an MCP server process, yet the parent gemini process was the one accumulating the /dev/ptmx handles. (I only have 1 mcp server enabled currently and it rarely is used so... not sure about this)
Observations in shellExecutionService.ts
I noticed some logic in packages/core/src/services/shellExecutionService.ts that might be worth investigating:
- In
executeWithPty (around line 467), ptyInfo.module.spawn(...) is called.
- If
spawn fails (e.g., throwing "posix_spawnp failed"), the error is caught by the try...catch block (around line 733).
- The catch block issues a warning:
[GEMINI_CLI_WARNING] PTY execution failed, falling back to child_process.
- It's possible that if
node-pty opens the PTY master (/dev/ptmx) before the posix_spawn syscall actually fails, the resulting file descriptor might not be getting closed during the fallback to child_process.
Technical Evidence (lsof and Stack Traces)
PTY Handle Count per Process:
PID 46302: PTMX=155, FDs=495 | /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
PID 98406: PTMX=74, FDs=255 | /usr/local/bin/node ~/.homebrew/bin/gemini
PID 94695: PTMX=51, FDs=183 | /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
... [multiple other sessions with 10-30 PTMX handles each] ...
LSOF Detailed view for PID 46302:
node 46302 [USER] txt REG 1,18 85160 188016135 ~/homebrew/lib/node_modules/@google/gemini-cli/node_modules/@lydell/node-pty-darwin-arm64/pty.node
node 46302 [USER] 17u CHR 15,155 0t0 605 /dev/ptmx
node 46302 [USER] 20u CHR 15,151 0t0 605 /dev/ptmx
node 46302 [USER] 23u CHR 15,152 0t0 605 /dev/ptmx
... [truncated 150+ similar entries] ...
Stack Trace (Main Thread) of Leaking Process:
The process appears idle in the event loop while holding the leaked FDs:
883 Thread_28021267 DispatchQueue_1: com.apple.main-thread (serial)
+ 883 start (in dyld) + 6076 [0x18a23ab98]
+ 883 node::Start(int, char**) (in node) + 604 [0x1048298a8]
+ 883 node::NodeMainInstance::Run() (in node) + 276 [0x1048c3744]
+ 883 node::SpinEventLoopInternal(node::Environment*) (in node) + 256 [0x104771b7c]
+ 883 uv_run (in node) + 408 [0x1056c5d1c]
+ 883 uv__io_poll (in node) + 784 [0x1056d9874]
+ 883 kevent (in libsystem_kernel.dylib) + 8 [0x18a59fd04]
System Limits:
sysctl kern.tty.ptmx_max = 511
Diagnostic Script
I generated this to help investigate PTY usage
pty_report.sh
#!/bin/bash
# PTY Usage Detailed Report
echo "PTY Usage Report"
echo "================="
# Get system PTY limits
PTY_MAX=$(sysctl -n kern.tty.ptmx_max 2>/dev/null || echo "Unknown")
PTY_TOTAL=$(lsof -n /dev/ptmx 2>/dev/null | awk 'NR>1' | wc -l | xargs)
echo "System Max PTYs (kern.tty.ptmx_max): $PTY_MAX"
echo "Current Total Open PTY Handles: $PTY_TOTAL"
echo ""
printf "%-8s %-6s %-25s %-35s %s\n" "PID" "PTYs" "Binary" "CWD" "Full Command"
echo "------------------------------------------------------------------------------------------------------------------------"
# Get PIDs and their ptmx counts from lsof
lsof -n /dev/ptmx 2>/dev/null | awk 'NR>1 {print $2}' | sort | uniq -c | sort -nr | while read count pid; do
if [ -z "$pid" ]; then continue; fi
# Get command name
cmd=$(ps -p $pid -o comm= 2>/dev/null)
# Get full command line
args=$(ps -p $pid -o args= 2>/dev/null)
# Get CWD
cwd=$(lsof -a -p $pid -d cwd -Fn 2>/dev/null | sed -n 's/^n//p')
# Format and print the row
printf "%-8s %-6s %-25.25s %-35.35s %.100s\n" "$pid" "$count" "$cmd" "$cwd" "$args"
done
My results:
PTY Usage Report
=================
PID PTYs Binary CWD Full Command
------------------------------------------------------------------------------------------------------------------------
1572 351 /Applications/iTerm.app/C / /Applications/iTerm.app/Contents/MacOS/iTerm2
46302 155 /usr/local/bin/node ~/src/numderscore /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
98406 74 /usr/local/bin/node ~/src/chrome-devtoo /usr/local/bin/node ~/.homebrew/bin/gemini
94695 51 /usr/local/bin/node ~/src/nemo /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
34569 48 /Applications/Visual Stud / /Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper
1724 39 ~/Library/ / ~/Library/Application Support/iTerm2/iTermServer-3.6.4 ~/Library/Appli
30521 34 /usr/local/bin/node ~/src/proj2 /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
38568 30 /usr/local/bin/node ~/src/proj3 /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
22155 30 /usr/local/bin/node ~/src/proj4 /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
88722 23 /usr/local/bin/node ~/src/proj5 /usr/local/bin/node ~/src/gemini-cli/bundle/gemini.js
86950 19 /usr/local/bin/node ~/devtools/ /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
17628 8 /usr/local/bin/node ~/src/proj5 /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
What happened?
gemini-cli(v0.22.5) appears to be leaking Pseudo-Terminal (PTY) master handles (/dev/ptmx). Over time, this exhausts the macOS system limit (kern.tty.ptmx_max), which is typically 511. When I started debugging the problem i had 1058.I think when my count crossed 1023 PTYs, I got the following problems in creating new tabs/sessions:
"A session ended very soon after starting. Check that the command in profile "Default" is correct."[forkpty: Device not configured]and[Could not create a new process and open a pseudo-tty.]Investigation via
lsofrevealed that multiplenodeprocesses runninggeminiwere holding hundreds of open handles to/dev/ptmx. One specific session had leaked 155 PTY handles and was holding a total of 495 file descriptors. Some of these sessions had been running for over two weeks (since Dec 22). (Though I had run/cleara few times, based on what i'm seeing in the json trajectories)What did you expect to happen?
I expected
gemini-cliand its associated processes (including MCP servers and node-pty instances) to correctly close file descriptors and PTY handles when they are no longer in use, and to terminate cleanly instead of remaining alive as idle processes that accumulate leaked resources. ;)Client information
Client Information
~/.homebrew/bin/gemini- installed via npm install -gLogin information
Logged in via
GEMINI_API_KEYenvironment variable.Anything else we need to know?
The leak may be correlated with the usage of MCP servers. In one instance, a
geminiprocess (PID 46302) was the parent of an MCP server process, yet the parentgeminiprocess was the one accumulating the/dev/ptmxhandles. (I only have 1 mcp server enabled currently and it rarely is used so... not sure about this)Observations in
shellExecutionService.tsI noticed some logic in
packages/core/src/services/shellExecutionService.tsthat might be worth investigating:executeWithPty(around line 467),ptyInfo.module.spawn(...)is called.spawnfails (e.g., throwing "posix_spawnp failed"), the error is caught by thetry...catchblock (around line 733).[GEMINI_CLI_WARNING] PTY execution failed, falling back to child_process.node-ptyopens the PTY master (/dev/ptmx) before theposix_spawnsyscall actually fails, the resulting file descriptor might not be getting closed during the fallback tochild_process.Technical Evidence (lsof and Stack Traces)
PTY Handle Count per Process:
LSOF Detailed view for PID 46302:
Stack Trace (Main Thread) of Leaking Process:
The process appears idle in the event loop while holding the leaked FDs:
System Limits:
sysctl kern.tty.ptmx_max= 511Diagnostic Script
I generated this to help investigate PTY usage
pty_report.shMy results: