claude-code - 💡(How to fix) Fix PTY master FDs leak on long sessions — exhausts kern.tty.ptmx_max on macOS

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

The Claude Code host process leaks /dev/ptmx master file descriptors across tool invocations. On a long-running session, accumulated FDs exhaust macOS's kern.tty.ptmx_max (default 511), after which forkpty() returns ENXIO ("Device not configured") system-wide — new Terminal windows, sudo, SSH sessions, and Claude's own Bash/Agent spawns all fail until the process is killed or the machine is rebooted.

Error Message

In whatever wraps PTY-backed subprocess spawning, ensure the master FD is explicitly close()d on child.on('exit') (and on error), not just when the wrapping object is garbage-collected — V8 GC is not timely enough to prevent accumulation at typical tool-call rates.

Root Cause

The Claude Code host process leaks /dev/ptmx master file descriptors across tool invocations. On a long-running session, accumulated FDs exhaust macOS's kern.tty.ptmx_max (default 511), after which forkpty() returns ENXIO ("Device not configured") system-wide — new Terminal windows, sudo, SSH sessions, and Claude's own Bash/Agent spawns all fail until the process is killed or the machine is rebooted.

Fix Action

Fix / Workaround

Workaround for users on macOS

Add to /etc/sysctl.conf (requires reboot to apply):

kern.tty.ptmx_max=4096

This raises the ceiling ~8× but does not fix the underlying leak.

Code Example

Claude  54656  zacharypucci  1673u  CHR  15,10   0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1650u  CHR  15,510  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1647u  CHR  15,509  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1633u  CHR  15,503  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1631u  CHR  15,507  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1629u  CHR  15,3    0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1628u  CHR  15,505  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1618u  CHR  15,499  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1617u  CHR  15,508  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1613u  CHR  15,506  0t0  605  /dev/ptmx
... (continuing down through FDs 1588u, all to /dev/ptmx, each with a unique
device minor number — 15,490 / 15,493 / 15,495 / 15,496 / 15,497 / 15,498 /
15,499 / 15,501 / 15,503 / 15,504 / 15,505 / 15,506 / 15,507 / 15,508 / 15,509 /
15,510, ...)

---

$ sudo sysctl -w kern.tty.ptmx_max=4096
sudo: unable to allocate pty: Device not configured

# new Terminal.app window#   forkpty: Device not configured
#   Could not create a new process and open a pseudo-tty.

---

kern.tty.ptmx_max=4096
RAW_BUFFERClick to expand / collapse

Summary

The Claude Code host process leaks /dev/ptmx master file descriptors across tool invocations. On a long-running session, accumulated FDs exhaust macOS's kern.tty.ptmx_max (default 511), after which forkpty() returns ENXIO ("Device not configured") system-wide — new Terminal windows, sudo, SSH sessions, and Claude's own Bash/Agent spawns all fail until the process is killed or the machine is rebooted.

Environment

  • Claude Code: 2.1.136
  • macOS: 26.4.1 (build 25E253), arm64 (Apple silicon)
  • Kernel: Darwin 25.4.0
  • kern.tty.ptmx_max: 511 (macOS default)

Reproduction

  1. Start a Claude Code session.
  2. Use it heavily — many Bash tool calls, Agent spawns, MCP-backed tools.
  3. Periodically run lsof /dev/ptmx from another terminal.
  4. Observe Claude's open /dev/ptmx FDs grow monotonically and never decrease, even after the corresponding child processes have exited.

Evidence

lsof /dev/ptmx snapshot from one such session (single Claude PID 54656):

Claude  54656  zacharypucci  1673u  CHR  15,10   0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1650u  CHR  15,510  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1647u  CHR  15,509  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1633u  CHR  15,503  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1631u  CHR  15,507  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1629u  CHR  15,3    0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1628u  CHR  15,505  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1618u  CHR  15,499  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1617u  CHR  15,508  0t0  605  /dev/ptmx
Claude  54656  zacharypucci  1613u  CHR  15,506  0t0  605  /dev/ptmx
... (continuing down through FDs 1588u, all to /dev/ptmx, each with a unique
device minor number — 15,490 / 15,493 / 15,495 / 15,496 / 15,497 / 15,498 /
15,499 / 15,501 / 15,503 / 15,504 / 15,505 / 15,506 / 15,507 / 15,508 / 15,509 /
15,510, ...)
  • FD numbers in the 1500s+ range indicate sustained accumulation across the session.
  • Each line has a unique PTY device minor — i.e. these are distinct PTY allocations, not dup'd handles to one PTY.
  • ps -axo pid,user,tty,command | awk '$3 ~ /^ttys/' at the same moment showed only 1 actually-attached tty session (the user's interactive zsh), confirming the children have exited and only Claude's parent-side master FDs remain.

Symptoms when limit is hit

$ sudo sysctl -w kern.tty.ptmx_max=4096
sudo: unable to allocate pty: Device not configured

# new Terminal.app window →
#   forkpty: Device not configured
#   Could not create a new process and open a pseudo-tty.

kern.tty.ptmx_max is read-only after boot on macOS, so the only userland remediation is killing the leaking Claude process or rebooting.

Likely cause

A node-pty / forkpty wrapper somewhere in the Bash / Agent / MCP-stdio spawn path is not closing the master FD in the child's exit handler. Each tool call leaks 1 FD.

Suggested fix

In whatever wraps PTY-backed subprocess spawning, ensure the master FD is explicitly close()d on child.on('exit') (and on error), not just when the wrapping object is garbage-collected — V8 GC is not timely enough to prevent accumulation at typical tool-call rates.

Workaround for users on macOS

Add to /etc/sysctl.conf (requires reboot to apply):

kern.tty.ptmx_max=4096

This raises the ceiling ~8× but does not fix the underlying leak.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING