Desktop app leaks /dev/ptmx master fds → system-wide pty exhaustion ("forkpty: Device not configured" / "Failed to spawn shell")

Resolved 💬 0 comments Opened Jun 15, 2026 by HR-AR Closed Jun 22, 2026

Summary

The macOS Claude desktop app leaks pseudo-terminal (pty) master file descriptors. It opens /dev/ptmx once per terminal/Cowork/tool session and never closes the fd when that session ends, so open masters accumulate for the lifetime of the app process. Because ptys are a global, capped kernel resource (kern.tty.ptmx_max, default 511 on macOS), a single long-running app instance can drain the entire pool — after which every forkpty() on the machine fails, not just Claude's. The app's own embedded Claude Code CLI is a victim too.

Environment

  • Claude desktop app: 1.12603.1 (macOS)
  • Embedded Claude Code CLI: 2.1.170
  • OS: macOS 27.0 (build 26A5353q), Apple Silicon (T6050), Darwin 27.0.0
  • kern.tty.ptmx_max = 511 (default)

Steps to reproduce

  1. Run the Claude desktop app for an extended period (here: ~3 days) while using terminal / Cowork / tool-running features that allocate ptys.
  2. Periodically check the open /dev/ptmx master count for the app process:

``
lsof -p <Claude-pid> | grep -c ptmx
``

  1. Observe it climb monotonically and never drop, even as terminals/sessions are closed.
  2. When the count approaches kern.tty.ptmx_max, any new pty allocation — a new Terminal tab, VS Code's integrated terminal, the embedded claude CLI — fails.

Actual behavior

At ~3 days uptime, one app process held 498 of 511 ptys (~97%) and the system pool was fully exhausted (free = 0). New shells failed with:

  • [forkpty: Device not configured] (kernel ENXIO — no pty available)
  • [Could not create a new process and open a pseudo-tty.]
  • Claude Code TUI: Failed to spawn shell

Expected behavior

The pty master fd is closed when its session/terminal/tab/tool-run ends. Concurrent pty usage stays bounded and is reclaimed; it must not grow without bound as a function of app uptime.

Evidence (live capture, app PID 59387, uptime 3d 3h)

$ sysctl kern.tty.ptmx_max
kern.tty.ptmx_max: 511

$ lsof -n /dev/ptmx | awk 'NR>1{print $1, $2}' | sort | uniq -c | sort -rn   # system-wide holders
 498 Claude 59387        # /Applications/Claude.app  — the desktop app
  10 Terminal 783
   3 Code 60509          # VS Code

# cap=511  in_use=511  free=0   → pool fully exhausted

$ lsof -p 59387 | grep -c ptmx
498

$ lsof -p 59387 | wc -l       # total open fds; 498 of them are leaked ptmx masters
2083

$ lsof -p 59387 | grep ptmx | head     # each is a distinct /dev/ptmx minor, opened and never closed
Claude 59387 hr2 37u CHR 15,489 0t0 605 /dev/ptmx
Claude 59387 hr2 44u CHR  15,18 0t0 605 /dev/ptmx
Claude 59387 hr2 45u CHR   15,4 0t0 605 /dev/ptmx
...

Impact

Ptys are shared system-wide, so exhaustion takes down terminal functionality for the whole machine — the user's Terminal/iTerm, editor integrated terminals, and the claude CLI the app itself ships. It presents as a confusing, unrelated-looking failure ("Failed to spawn shell") with no indication the desktop app is the cause.

Workaround

  • Quit and relaunch the desktop app — releases all leaked masters instantly, no reboot needed.
  • Optional headroom (does not fix the leak): sudo sysctl -w kern.tty.ptmx_max=4096.

Suggested fix

  • Close the pty master fd on every session teardown path (terminal exit, tab/pane close, renderer reload, tool-run completion).
  • Add a bounded pool + reaper for pty sessions; assert non-growth under a soak/long-uptime test.
  • Optionally, warn (or self-cap) as the process approaches a large share of kern.tty.ptmx_max instead of failing hard and taking the machine's terminals down with it.

View original on GitHub ↗