Desktop app leaks /dev/ptmx master fds → system-wide pty exhaustion ("forkpty: Device not configured" / "Failed to spawn shell")
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
- Run the Claude desktop app for an extended period (here: ~3 days) while using terminal / Cowork / tool-running features that allocate ptys.
- Periodically check the open
/dev/ptmxmaster count for the app process:
````
lsof -p <Claude-pid> | grep -c ptmx
- Observe it climb monotonically and never drop, even as terminals/sessions are closed.
- When the count approaches
kern.tty.ptmx_max, any new pty allocation — a new Terminal tab, VS Code's integrated terminal, the embeddedclaudeCLI — 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](kernelENXIO— 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_maxinstead of failing hard and taking the machine's terminals down with it.