Pty file-descriptor leak in Claude.app exhausts macOS `kern.tty.ptmx_max` after ~3 days, blocks all forkpty system-wide
Summary
/Applications/Claude.app (the desktop wrapper that hosts the Claude Code CLI) leaks pty master file descriptors over its lifetime. After ~3.5 days of uptime with active skill/subagent usage, a single Claude.app process held the entire macOS pty pool (kern.tty.ptmx_max, default 511 on macOS Tahoe), causing every system-wide call to forkpty(3) to fail with ENXIO. Terminal.app, iTerm, Warp, VS Code integrated terminal, etc. all fail to spawn their login shell until either Claude.app is restarted or the kernel cap is raised.
Environment
| Field | Value |
|---|---|
| macOS | 26.3 (Tahoe), build 25D125 |
| Kernel | Darwin 25.3.0 / xnu-12377.81.4 / arm64 / Apple M-series |
| Claude.app | 1.7196.0 (/Applications/Claude.app/Contents/MacOS/Claude) |
| Claude Code CLI (embedded) | 2.1.138 (~/Library/Application Support/Claude/claude-code/2.1.138/claude.app/Contents/MacOS/claude) |
| kern.tty.ptmx_max (default) | 511 |
| Claude.app process uptime at failure | 3d 12h |
| Mac uptime at failure | 9d 13h |
Reproduction
- Launch Claude.app on macOS Tahoe.
- Use it actively for 2–3 days with frequent Skill / subagent / Bash-tool invocations (each spawns a child shell via pty).
- Eventually,
lsof /dev/ptmxshows the Claude PID holding fds equal to or approachingsysctl kern.tty.ptmx_max. - Open Terminal.app (or any other terminal emulator).
- Observe failure:
[forkpty: Device not configured]
[Konnte keinen neuen Prozess erstellen und ein Pseudo-TTY öffnen.]
Same error from iTerm, Warp, VS Code's integrated terminal, etc. — anything calling forkpty(3) / openpty(3) / posix_openpt(3) fails with ENXIO because the kernel cannot allocate a new pty unit.
Expected behavior
Claude.app should release its pty master fds when the child shell process exits, so that the kernel can free the pty unit number for reuse.
Actual behavior
Pty master fds accumulate monotonically across Claude.app's lifetime. lsof evidence below shows the leak signature clearly.
Evidence (from the failure on my machine)
Kernel state at failure
$ sysctl kern.tty.ptmx_max
kern.tty.ptmx_max: 511
$ ls /dev/ttys* | wc -l
527 # 511 dynamic + 16 legacy
$ lsof /dev/ptmx 2>/dev/null | tail -n +2 | wc -l
511 # all slots taken
Who is holding them
$ lsof /dev/ptmx 2>/dev/null | awk '{print $1, $2}' | sort | uniq -c | sort -rn
511 Claude 98698
A single PID holds the entire pool.
fd breakdown for PID 98698
$ lsof -p 98698 | awk '{print $5}' | sort | uniq -c | sort -rn | head
524 CHR
521 KQUEUE
511 (revoked)
251 REG
88 unix
72 PIPE
6 DIR
The 511 (revoked) entries are the smoking gun: these are fds whose underlying device was torn down (child shell exited, slave tty went away) but whose master fd was never closed. They are the leaked pty masters from previous child processes. Each one keeps its pty unit number allocated, so the kernel cannot hand out new ones.
Total open fds on Claude.app PID 98698: 1963 (well below the kern.maxfilesperproc ceiling of 61440, so the leak hits the pty-specific cap before the general fd cap).
Process tree
PID 98698 → /Applications/Claude.app/Contents/MacOS/Claude (root parent, launched by launchd)
└─ Claude Helper subprocesses (Electron utility processes)
└─ Claude Code CLI sessions (claude --output-format stream-json ...)
└─ Bash subshells per skill / tool / subagent invocation
Skill/subagent invocations spawn shells through this chain. Each shell needs a pty. The leaked fds appear on the root Claude PID, which suggests fds are being inherited from a fork chain and not closed when the deepest child exits.
Workaround (non-destructive, no file edit, no reboot, no Claude.app restart)
Bumping the kernel cap above the leaked count immediately restores the ability to spawn new terminals without touching the running Claude.app:
do shell script "/usr/sbin/sysctl -w kern.tty.ptmx_max=999" with administrator privileges
Notes:
kern.tty.ptmx_max=2048is rejected withEINVALon macOS 26.3 — the kernel-enforced ceiling appears to be 999.- Effect is runtime-only; the value resets on reboot.
- Does not fix the leak — only buys time. Roughly 3 more days of headroom at the observed leak rate before the new cap is reached.
- Restarting Claude.app cleanly releases all leaked fds.
Suspected cause
Classic node-pty / Electron child_process leak pattern: master fd is open()'d, child is forked + execed, child eventually exits, kernel revokes the slave-side device node — but the user-space master fd is never close()'d. Until that fd is closed, the kernel keeps the pty unit number allocated.
The (revoked) count == leaked-fd count is the canonical fingerprint of this bug.
Likely suspects in the codebase (without source access I can only guess):
- A
spawn/pty.spawncall site in the skill / subagent / Bash-tool execution path that does not register a close handler on theexitevent of the pty wrapper. - An Electron utility process that forks pty children for the embedded Claude Code CLI but does not propagate the close.
Impact
- High severity for long-uptime users: the failure mode is silent until it triggers, then it bricks every terminal emulator on the system. A user who does not know the cause will reasonably assume their terminal app is broken (I did — I uninstalled iTerm before realizing iTerm was fine and Terminal.app failed identically).
- Cross-app blast radius: this is not contained to Claude.app or Claude Code. It blocks every other macOS process that needs a new pty.
- Recovery requires either kernel knowledge (
sysctl) or knowing to quit Claude.app — the error message gives no hint that Claude is the cause.
Asks
- Investigate the leak (
(revoked)fd accumulation on the main Claude.app PID). - Add a defensive
close()on pty master fds when the child exits / errors. - Consider a periodic self-check in Claude.app: if
lsof -p $$ | grep -c '/dev/ptmx'grows monotonically across sessions, log a warning or auto-recycle Helper processes. - Optionally, surface a more helpful diagnostic when forkpty fails during CLI startup ("pty pool exhausted, consider restarting Claude.app or running
sudo sysctl -w kern.tty.ptmx_max=999").
Happy to provide more lsof snapshots or sample/spindump traces of Claude.app if useful.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗