macOS: PTY file descriptors leaked across a long Bash-heavy session, exhausting kern.tty.ptmx_max (forkpty / posix_openpt fail with ENXIO system-wide)
Summary
After a long Claude Code session that issued many Bash tool calls, the Claude desktop process accumulated 511 open file descriptors against /dev/ptmx, exactly matching kern.tty.ptmx_max=511. From that point on, forkpty(3) and posix_openpt(3) fail with errno 6 (ENXIO, "Device not configured") for every process on the machine, not just inside Claude. That breaks Terminal.app (forkpty: Device not configured), VS Code's integrated terminal (posix_openpt failed: Device not configured), and any other tool that needs a fresh PTY (tmux, screen, ssh's local PTY allocation, etc.).
The workaround is to quit and relaunch Claude. The 511 PTYs release on process exit and Terminal works again immediately.
Environment
- macOS: 26.4.1 (build 25E253), aka Tahoe
- Kernel: Darwin 25.4.0, arm64 (Apple Silicon)
- Claude Code: Desktop app
- kern.tty.ptmx_max: 511 (system default on macOS 26)
Reproduction
- Open Claude Code Desktop.
- Have a single long session that issues many Bash tool calls (in my case, several hours of dependency triage:
npm installrepeated, dev server start/stop cycles,curlsmoke tests,gh prcalls, etc.). I'd estimate the session made ~300+ Bash tool invocations. - After several hours, open a new Terminal.app window. It refuses to spawn a shell with
forkpty: Device not configured.
Diagnostic
$ sysctl kern.tty.ptmx_max
kern.tty.ptmx_max: 511
$ ls /dev/ttys* | wc -l
527 # device nodes (cumulative across uptime, not the live constraint)
$ lsof -nP | grep '/dev/ptmx' | awk '{print $1, $2}' | sort | uniq -c | sort -rn
511 Claude 21371
$ lsof /dev/ptmx | head -5
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Claude 21371 jdl 38u CHR 15,36 0t0 605 /dev/ptmx
Claude 21371 jdl 40u CHR 15,38 0t0 605 /dev/ptmx
Claude 21371 jdl 46u CHR 15,14 0t0 605 /dev/ptmx
Claude 21371 jdl 84u CHR 15,25 0t0 605 /dev/ptmx
...
The single Claude process (PID 21371) held 511 open /dev/ptmx file descriptors, equal to the kernel's pseudo-terminal limit. No other process on the system held any. Quitting Claude immediately restored Terminal functionality.
Likely cause
The Bash tool spawns subprocesses through a PTY. When the subprocess exits, the PTY master file descriptor inside the Claude process appears to NOT be closed. Over hundreds of Bash calls, those leaked FDs accumulate until they hit kern.tty.ptmx_max. Because the FDs are still open from Claude's perspective, the kernel keeps the PTY pair allocated, denying new PTY allocations to any process system-wide.
Suggested fix
After the Bash tool's subprocess exits, explicitly close(2) the PTY master file descriptor (and any pipe/event-loop registrations holding it). Node's child_process doesn't auto-close PTY FDs in some configurations; the Bash tool harness needs to manage that lifecycle deterministically.
A defensive bound on concurrent open PTYs per Claude process (e.g., warn or recycle at 256) would also turn this from "machine becomes unusable after N hours" into a soft alert.
Severity
Medium-high. The session length needed to hit it is long but not extreme (one full workday of dep triage). When it does hit, it takes the user's entire terminal layer down, not just Claude. Cmd+Q on Claude is the only recovery without a reboot.
22 Comments
Found 1 possible duplicate issue:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Independent +1 from a separate machine — same root cause, fresh corroborating data:
Claude.appPID holding 506 / 511/dev/ptmxmaster fds after an 18h 22m session (kern.tty.ptmx_max: 511, system default)/dev/ptmx+ttysfd counts at the same moment:iTerm2 43, zsh 12, node 6, codex 6— onlyClaude.appis leakingforkpty: Device not configured, iTerm2 instant-closes on launch, every system tool that needs a fresh PTY failsWorkaround that does not require quitting Claude:
sudo sysctl -w kern.tty.ptmx_max=2048raises the kernel pty cap at runtime. The leaked fds stay held but are harmless once new pty allocations have headroom. Doesn't persist past reboot, but neither does the leak — useful for not losing in-flight Claude session state. (Note:sudowon't be runnable from a Terminal that's already broken; Script Editor'sdo shell script ... with administrator privilegesworks as a GUI fallback.)Re: the auto-flagged duplicate of #47909 — I'd push back on closing this as a strict dupe. The two reports likely share a root cause (Claude not closing PTY master fds on Bash subprocess exit, as OP suggests), but they describe distinct ceilings:
RLIMIT_NOFILE=256exhausted → Claude-internal errors only (ENOENT: posix_spawn 'rg', "directory no longer exists", "low max file descriptors" on session resume)kern.tty.ptmx_max=511exhausted → Terminal.app, iTerm, tmux, ssh local PTY allocation, etc. break for every app on the machineSame leak, but on machines where the per-process fd ceiling has been raised (or where the leak is dominantly PTY-master fds rather than miscellaneous fds), it skips past #47909's symptoms and hits the system-wide pty cap instead. Fixing the PTY-master
close(2)on subprocess exit plausibly resolves both, but the symptom surfaces here are much harder to diagnose from the user's perspective (the obvious suspect is the user's shell config, not Claude) and deserve their own thread.---
EDIT: human note 😄 the Script Editor thing did not work for me so I ended up just restarting.
Confirming on Claude.app v1.6608.2, same single-PID pattern, ~501 of 511 over ~27 h
Additional data point from another macOS machine. I am redacting local usernames, repository names, absolute paths, session IDs, and full process arguments.
I hit the same system-wide PTY exhaustion symptom while using Claude Code from the Desktop app. New PTY allocation started failing with
Device not configured/ errno 6 (ENXIO). A small Python probe usingos.openpty()could only allocate a few more PTYs before failing:At first I saw many
/dev/ttys*entries and suspected orphan shell/dev processes. There were some of those, but they did not explain the scale:The important part was the master side. Grouping open character devices with major
15(/dev/ptmx) by process showed one Claude Desktop process holding almost all of the PTY master FDs:lsoffor the Claude process showed hundreds of entries like:The minor numbers spanned a large range and reached the high 400s / 510 range. This matches the issue description: the live constraint is not the cumulative
/dev/ttys*nodes, but the PTY master FDs still held by the Claude process.There were also a few unrelated orphan PTY holders on the machine, for example detached
pnpm dev/go runprocesses andzsh+gitstatusdpairs holding slave-side/dev/ttysNNNdescriptors. Those accounted for only a small number of PTYs and were not sufficient to explain the/dev/ptmxexhaustion.I have not quit Claude yet because I wanted to preserve the evidence, but the ownership pattern strongly suggests the same leak path as reported here: Claude Desktop is retaining PTY master FDs across tool calls/sessions until the macOS PTY pool is nearly exhausted system-wide.
Can confirm this is happening with me too with Claude.app v1.7196.3 holding 493 out of 511 unclosed tty. Wrote a small bash script for process age distribution with
ps -eo etime, revealsTotal 33 processes
Note that the helpful workaround above,
sudo sysctl -w kern.tty.ptmx_max=2048, may not work becausesudoitself needs to allocate a pseudo-tty:The fix is to free up PTYs first by quitting Claude Code, rebooting, or closing terminal windows. Also, on my machine at least, I cannot raise
ptmx_maxbeyond 999.I am hitting this problem almost daily and I think it should be given high priority.
Same problem here, pls fix!
Hitting this right now — another fresh datapoint, in case it helps narrow down the leak path.
Environment
| | |
|---|---|
| macOS | 26.4.1 (build 25E253), arm64, Darwin 25.4.0 |
| Claude.app (desktop) | 1.9659.2 |
| claude-code | 2.1.156 |
|
kern.tty.ptmx_max| 511 (default) |Symptoms
Terminal.app and any other native terminal app fails to launch with:
This blocks every PTY-using app system-wide (Terminal, iTerm2, tmux, ssh,
script, etc.) until Claude.app is restarted.Diagnosis
/dev/ptmxpermissions are fine, the pool is just fully consumed:All 511 are held by a single PID — the Claude.app main process:
That's the Electron parent process (not a Helper / Renderer / Bash subprocess). It accumulated 511 master-side
/dev/ptmxfds and never released them. Slave devices (/dev/ttys*) keep showing up inlsoftoo — 527 of them on disk, lining up with the 511 leaked masters.Trigger pattern
This Mac has been running Claude.app continuously since
May 20 23:07(~11 days, matcheswho | grep console). During that span there were many parallel Claude Code sessions; right nowpgrep -lf claudestill shows ~10 liveclaudechild processes (pairs ofHelpers/disclaimer+claude-code/.../claude), plus an unknown number of long-gone ones whose master fds the parent never closed.So it's not strictly tied to a single Bash-heavy session — it's the parent Claude.app accumulating PTY masters across the lifetime of child
claudeprocesses, not just within one. The longer the app runs and the more Claude Code sessions are spawned/closed, the closer it creeps to 511.Workaround
Cmd-Q+ relaunch Claude.app frees all 511 fds instantly and Terminal works again. Killing individual childclaudePIDs doesn't help — the leaked masters are held by the parent.Suggestion for triage
Looking at the leaked fd numbers (
47u,75u,83u,84u,91u, ...) — non-contiguous and scattered across a wide range, which is consistent with "open new pty per Claude Code session, forget to close on session end" rather than a single tight loop. Whatever spawns the childclaudeprocess via forkpty seems to be missing theclose(masterfd)on child exit / disconnect.Happy to dump
lsof -p 78757in full or run anything else useful before I have to restart the app.Mid-session, every terminal on my Mac stopped working at once — Terminal, iTerm, even a fresh ssh all spat back forkpty: Device not configured. Nothing was obviously wrong, so I assumed I'd wrecked my own machine. lsof /dev/ptmx told a different story: a single long-running Claude Desktop process was quietly holding 510 of the system's 511 pseudo-terminal slots — one leaked master per shell it had ever spawned, none ever freed. The app had slowly strangled the entire OS's ability to open a terminal, then tripped over its own feet when its Bash tool couldn't open one either. Quit the app, all 510 slots came back. The 4-day uptime caused a slow drip to eventually build up.
Claude desktop app leaks pty master fds across Claude Code sessions → exhausts
kern.tty.ptmx_max(511) → breaks ALL new terminals system-wide withforkpty: Device not configuredTL;DR
The Claude desktop app's main (Electron) process opens a pseudo-terminal (pty master) for each Claude Code session and never closes the master file descriptor when the session ends. The leaked masters accumulate for the entire lifetime of the app process. On a machine where the app had been running ~9 days across ~76 Claude Code sessions, the app process alone held 511 open pty masters — exactly
kern.tty.ptmx_max(511), i.e. 100% of the system pty pool.Once the pool is exhausted, every
forkpty()on the machine fails — not just Claude. Opening any new Terminal.app / iTerm2 window, a VS Code integrated terminal, an SSH login shell,tmux/screen, or any other pty consumer dies with:The only way to release the fds is to quit and relaunch the Claude desktop app — killing the individual Claude Code session/child processes does not free them (proven below), because the fds are owned by the app's main process, not the children.
---
Relationship to existing reports
This failure mode is already tracked upstream — filed independently by multiple users since 2026-05-09. Open: #57580, #61391, #62378, #63131, #63169, #65090. Closed: #58263, #59544, #59839, #61124, #61358. It is acknowledged by Anthropic (labels:
bug,has repro,platform:macos,area:bash,area:desktop) but as of this writing has no fix, target version, or assignee, and affected versions span 2.1.138 through 2.1.156 (i.e. upgrading does not avoid it).What this write-up adds that the existing reports do not: a controlled experiment isolating fd ownership. Killing all 56 abandoned child CLI sessions (76 → 18 processes) left the open-master count pinned at 511/511, while
lsofsimultaneously showed 0 open slaves against 511 open masters. Together these prove the masters are orphaned and held by the long-lived Electron parent (PID 40685) — so only an app restart releases them, and any per-session teardown fix must close the master fd on the parent side. Offered as additional evidence for #57580.---
Severity / Impact
kern.tty.ptmx_maxdenies pseudo-terminals to every process on the machine. The user discovers it as "my terminal is completely broken," with no indication that Claude is the cause.Device not configured(ENXIO) reads like a corrupted device node or a permissions problem on/dev/ptmx, sending users down the wrong diagnostic path (SIP,/devperms, reinstalling the terminal, etc.).kern.tty.ptmx_maxviasudo sysctl -w— is unreachable, becausesudoneeds a password and there is no working terminal left to type it into. The user is locked out of the CLI entirely until they restart the GUI app.---
Environment (live machine where this was diagnosed)
| | |
|---|---|
| Hardware | MacBook Air (M1) —
MacBookAir10,1, 8 cores, 16 GB RAM || OS | macOS 26.0 (build 25A354), Darwin 25.0.0, arm64 |
| Claude Code | 2.1.138 (
~/Library/Application Support/Claude/claude-code/2.1.138/claude.app/Contents/MacOS/claude) || Claude desktop app |
/Applications/Claude.app/Contents/MacOS/Claude(Electron main, PID 40685), uptime ~9 days at time of diagnosis ||
kern.tty.ptmx_max| 511 (system default) |---
Symptom
Opening a terminal window anywhere (Terminal.app, iTerm2, VS Code, SSH session into the box, etc.) immediately prints:
No shell prompt is ever reached. The machine is otherwise responsive (GUI apps work).
---
How this was found (research path & the direction that cracked it)
This was diagnosed collaboratively. The user's framing is what pointed the investigation straight at the real class of problem instead of chasing red herrings:
forkpty: Device not configuredis commonly misread as a broken/permissions-damaged/dev/ptmx. The user instead treated it as a resource-exhaustion problem — "the system can't make a new pty," not "the pty device is corrupt."Heavy CPU usage/Runaway user processCode Helper (Plugin)instances at 615% CPU)The specific culprit there (VS Code) turned out to be a red herring for the live machine — but the shape of the problem the user pulled out of that report was exactly right: "one application is spawning/leaking a large number of subordinate resources and exhausting a system limit." That is precisely what was happening, just with Claude Code as the multiplier instead of VS Code.
kern.tty.ptmx_max) and to which process holds the open pty handles (lsof /dev/ptmx) — rather than wasting time on/devpermissions, SIP, or terminal-app reinstalls.That instinct — "this is the same family as the runaway-helper exhaustion I saw before" — is what turned a confusing, generic macOS error into a five-minute root-cause.
---
Investigation (commands + evidence)
All commands were run through a non-interactive shell (pipes, no new pty), which still works when interactive terminals cannot — that is itself a useful diagnostic:
forkptywas failing while plainfork+execover pipes still succeeded, which already localises the fault to the pty subsystem rather than to process creation in general.1. Limits
Process counts were healthy (648 system / 394 user) and well under
maxproc/maxprocperuid— so this was not a process-table exhaustion (fork: Resource temporarily unavailable), it was specifically a pty exhaustion.2. pty pool — the smoking gun
511 open masters, exactly at the 511 cap. Zero free ptys. That is why
forkptyfails.The 0 open slaves vs 511 open masters is the key forensic signature: every one of those 511 ptys has no slave side open — i.e., the shell/child that once used each pty is gone, but the master end is still being held open. That is the textbook fingerprint of a master-fd leak: whoever opened the master forgot to
close()it after the pty's child exited.3. Who holds them
All 511 masters are held by a single process: the Claude desktop app's Electron main process (PID 40685), up ~9 days.
4. The session multiplier
~76 Claude Code sessions had accumulated under the app over the 9-day uptime — many abandoned for days. The leaked master count (511) tracks with sessions × ptys-per-session over that window.
5. The band-aid was unreachable
Raising
kern.tty.ptmx_maxrequires root,sudorequires a password, and there is no working terminal to enter it (that's the bug). The GUI app restart is the only escape.---
Proof the leak is in the app process, not the children
We tested the hypothesis directly. We terminated all 56 abandoned (≥1-day-old) Claude Code CLI child processes, carefully protecting the active session and the app:
Killing the child sessions reclaimed CPU/RAM but freed no ptys, because the pty master file descriptors are owned by the app's main process (PID 40685), and the OS only reclaims a pty when the process holding its master fd closes that fd (or exits). The children holding the (already-closed) slave side are irrelevant.
Conclusion: the only release path is the app main process closing those fds — i.e., quitting/restarting
Claude.app.---
Root cause
In the Claude desktop app, each Claude Code session is backed by a pseudo-terminal whose master fd is opened and retained by the app's long-lived Electron main process (typical
node-pty/posix_openptownership model). When a session ends — normal exit, crash, window/tab close, disconnect, or silent abandonment — the master fd is not closed. The pty therefore stays allocated for the entire lifetime of the app process.Because the app is designed to run for days, leaked masters monotonically accumulate:
and converge on the system ceiling
kern.tty.ptmx_max(default 511). At that pointforkpty()returns ENXIO ("Device not configured") for every consumer on the machine.The
0 slaves / 511 mastersmeasurement confirms the masters are orphaned (no live child on the other end) — they are pure leaks, not in-use sessions.---
Reproduction
``
bash
``watch -n 5 'lsof -nP | grep -c /dev/ptmx'
sysctl kern.tty.ptmx_max(511), new terminals start failing withforkpty: Device not configured. At 511/511 it is total.Expected: closing/ending a session releases its pty (count goes down).
Actual: the count only ever goes up until the app is restarted.
---
Workaround (for users hitting this now)
``
bash
`sudo sysctl -w kern.tty.ptmx_max=4096 # this boot only
sysctl -w` at boot.To persist across reboots, install a LaunchDaemon that runs the same
Diagnostic one-liners (to confirm you have this exact issue)
If the holder is
Claude <pid>and the master count equalskern.tty.ptmx_max, this is the bug.---
Suggested fixes (for maintainers)
kill()s thenode-ptyinstance andclose()s the underlying master fd on every exit path: normal completion, error/crash, user-initiated close, tab/window close, IPC/renderer disconnect, and app-side reaping of dead sessions.0 slaves / N masterscondition is directly detectable.kern.tty.ptmx_max, with backpressure or oldest-idle eviction, so a leak (or just heavy use) can never consume the whole system pool.forkpty/pty allocation fails with ENXIO, detect pty exhaustion and surface an actionable message ("pseudo-terminal pool exhausted; the Claude app is holding N/511 — restart it") instead of the rawDevice not configured.---
Appendix: timeline of the diagnosis
| Step | Finding |
|---|---|
| Symptom |
forkpty: Device not configuredon every new terminal || User direction | Reframed as resource exhaustion via a prior EtreCheck "runaway helper" reference → pointed at pty/handle limits |
|
sysctl|kern.tty.ptmx_max=511; process tables healthy → pty-specific, not process exhaustion ||
lsof /dev/ptmx| 511 masters open = 100% of cap; 0 slaves → orphaned-master leak signature || holder | All 511 held by
Claude(desktop app main, PID 40685, up 9 days) || sessions | ~76 Claude Code CLI sessions accumulated (56 ≥1 day old) |
|
sudo -n| password required → limit-raise band-aid unreachable (no terminal) || kill test | Killed 56 children (76→18 procs); masters stayed 511 → fds owned by app, not children |
| fix | Quit + relaunch
Claude.appreleases all 511 ptys; only viable release path |Still reproducing on the latest release (v2.1.165) — confirming this is unfixed in the current build, not resolved by upgrading.
Environment
Live measurement (single session, in real time)
The Electron parent (
/Applications/Claude.app/Contents/MacOS/Claude) holds the leaked/dev/ptmxmasters. During one active session the count climbed monotonically as Bash tool calls ran:So it accumulates ≈1 master per Bash invocation and never releases them while the app stays open — a true monotonic leak, not a burst. With the default
kern.tty.ptmx_max=511this hits the cap within a long session, after which all pty allocation fails system-wide (forkpty: Device not configured), breaking Terminal.app, iTerm2, VS Code, etc.Severity amplifier (deadlock): once the pool is fully exhausted, the usual workarounds can't run — neither
sudo sysctl kern.tty.ptmx_max=…(the admin/osascriptpath itself needs a pty) nor relaunching from a terminal. Recovery requires force-quitting Claude from Activity Monitor. The mitigation therefore has a cliff: you must act before the cap.Likely root cause / where to look: the sibling project gemini-cli traced the same class of bug to node-pty opening the PTY master before
posix_spawnand failing to close that fd on thechild_processfallback path (google-gemini/gemini-cli#15945; Cursor and opencode#11016 show the same signature). Suggest auditing the Bash tool's pty host so the master fd is closed when the child exits, rather than retained by the Electron parent.Happy to provide
lsof/fs_usagetraces or test a patched build. Canonical tracking issue appears to be #47909.Another live datapoint plus a scope clarification and a detection/mitigation toolkit, in case it helps triage.
Live measurement (2026-06-08, still reproducing)
kern.tty.ptmx_max=511(default)/dev/ptmxmasters; one unrelatedtmuxheld the last one → pool fully exhausted,forkpty: Device not configuredsystem-wide.Scope: the holder is the Electron parent, not the embedded CLI
Walking the process chain of a Desktop-hosted session:
The masters are owned by the long-lived Electron main process, so killing the per-session CLI children does not free them (matches the kill-experiment already posted here) — only quitting Claude.app does. The practical corollary, which I haven't seen called out explicitly: running
claudeas a standalone CLI in a normal terminal (installed viaclaude.ai/install.shor npm, not launched by Claude.app) has no persistent Electron parent, so the masters are owned by a short-lived process and released at session end — no cross-session accumulation, no system-wide exhaustion. That's a usable workaround for anyone doing long or parallel sessions while this is open, distinct from just raising the ceiling.Detection + pre-cliff monitoring
Because the cliff is self-trapping (
sudoitself can't allocate a pty once exhausted), the fix has to happen before the cap. A simple headroom check that warns at ~80% and can be looped:Suggested fix direction (unchanged from prior reports)
Close the pty master fd on the Electron-parent side on every session/shell teardown path — normal exit, crash, tab/window close, IPC disconnect, and app-side reaping — rather than retaining it for the app's lifetime. Same signature traced in sibling tools (google-gemini/gemini-cli#15945, opencode#11016: node-pty opens the master before
posix_spawnand leaks it on thechild_processfallback).Happy to share the small headroom-monitor / preflight-gate scripts if useful.
The Claude app allocates 2 every time I switch between code sessions. Switch sessions ~250 times and I'm out of PTY fds and need to restart the app.
Still reproducing on CLI v2.1.170 / desktop shell 1.11847.5 — newer than the v2.1.165 confirmations above, so unfixed as of 2026-06-10.
Environment: macOS 26.3 (25D125), Apple Silicon,
kern.tty.ptmx_max=511(default)Measurement: after ~6h50m of app uptime running 4 concurrent desktop-hosted Code sessions (Bash-tool-heavy), the Electron parent held 426
/dev/ptmxmasters:Together with other apps' masters the pool was effectively at the 511 cap (
ls /dev/ttys* | wc -l→ 527 slave nodes). Existing terminals kept working; the next fresh PTY allocations would hit the ENXIO failures described in the OP.426 masters is far more than the session count (4), consistent with allocation per Bash invocation / per session switch (as @kixelated observed) rather than per session, and never closed.
Confirmed the only remedy is quitting the app — the masters are held by the live Electron parent (not the per-session
claudechildren), so nothing short of process exit releases them.Follow-up to my measurement above — a severity escalation observed at full exhaustion that triage should know about:
At the cap,
sudoitself stops working. macOS 26 sudo allocates a fresh PTY for every command (use_pty), so once the pool is exhausted:That means the natural shell-side mitigation (raising
kern.tty.ptmx_max) is unreachable from any terminal exactly when you need it — existing shells still run, but anything wanting a fresh PTY (new tabs, tmux, ssh -t, and sudo itself) fails with ENXIO.Working escape hatch (no PTY involved): escalate via Authorization Services instead of sudo:
GUI password dialog, takes effect immediately. Verified recovery with a real forkpty:
script -q /dev/null truesucceeds again afterward.Caveats: 999 is the hard ceiling (
ttysNNNnamespace), and the leak keeps growing (~426 masters in ~7h of 4-session use here), so the cap raise only buys roughly a workday — app restart remains the only way to actually drain the pool. The sysctl resets at reboot.Net for triage: at exhaustion this isn't just "terminals break" — the remediation paths break too, since privilege escalation from a shell needs the very resource that leaked away.
Independent confirmation on the same builds as the latest report above — desktop shell 1.11847.5, embedded Claude Code CLI 2.1.170, macOS 26.x (Darwin 25.4.0) — plus a CLI-vs-desktop isolation datapoint.
Measurement (2026-06-11)
After ~22.5 h of continuous desktop-app uptime with frequent create/teardown of embedded Code sessions, the Electron parent held 499
/dev/ptmxmasters and the system hit full exhaustion (kern.tty.ptmx_max=511, default):Symptoms at exhaustion: Terminal.app →
forkpty: Device not configured;python3 -c 'import pty; pty.openpty()'→OSError: out of pty devices.Averaged over the window that is ≈1 leaked master per ~3 minutes, under an automation-heavy pattern that frequently starts/ends embedded sessions — consistent with @kixelated's observation of ~2 PTYs leaked per session switch.
CLI vs desktop isolation
The same machine was concurrently running several external CLI sessions (claude-code 2.1.158–2.1.160) in ordinary terminals. None of those processes accumulated
/dev/ptmxmasters at anywhere near this magnitude — the leak concentrates in the desktop app's Electron parent. This supports the diagnosis that the desktop shell's PTY lifecycle (not the CLI's Bash tool in an external terminal) is what fails to close masters.Post-restart
After restarting the app, the parent process holds only 3
/dev/ptmxmasters and PTY allocation works system-wide again (pty.openpty()succeeds) — fully reclaimed on process exit, consistent with a per-process fd leak.Mitigation timing (+1 to the escalation above)
Corroborating @ArangoGutierrez: the
sudo sysctl -w kern.tty.ptmx_max=999mitigation is only reachable before the pool fills — at 511/511, sudo (use_ptyon macOS 26) cannot allocate a PTY for itself. Our prevention until this is fixed is a daily desktop-app restart.Independent confirmation on desktop shell 1.11847.5 / embedded Claude Code CLI 2.1.170 (latest as of today) — plus two symptom strings not yet in this thread (for searchability), and a measurement showing the leaked masters are orphaned, not live sessions.
Environment
kern.tty.ptmx_max=511(default)Measurement (2026-06-11, ~44 h app uptime)
The 499 masters are not backing live sessions — the process had only 20 children total, exactly 1 of them a shell:
Two symptom strings not yet in this thread
Also observed: Python
pty.fork()→OSError: out of pty devices.Workaround confirmed
Quit + relaunch Claude Desktop releases all 499 masters immediately; every terminal app works again.
Reproduced today with detailed measurements, adding as a data point since this hit the hard system limit and broke all terminals machine-wide.
Environment
Measurements (after ~30h app uptime)
kern.tty.ptmx_max= 511 (default);ls /dev/ttys* | wc -l→ 527lsof /dev/ptmxshowed the main Claude Electron process (single PID) holding 496 pty masters — every other app on the system held ≤6Symptoms at exhaustion
zpty: can't open pseudo terminal: device not configured(repeating), followed byis-at-least: maximum nested function level reachedError: forkpty(3) failed.from node-pty — no new terminal tabs could be opened anywhere on the systemRecovery
Quitting the Claude app released everything immediately: 527 → 36 allocated ptys, Claude's master count 496 → 4 after relaunch. No reboot needed.
Happy to provide more diagnostics next time it accumulates (it leaked ~500 in ~30 hours at my usage level).
Another confirmed occurrence, current build.
Environment
Symptoms
forkpty: Device not configured/Could not create a new process and open a pseudo-tty.kern.tty.ptmx_max=511,ls /dev/ttys[0-9a-f][0-9a-f][0-9a-f] | wc -l→ 511)Attribution
A single Claude.app process held all 511
/dev/ptmxmaster fds. No slave-side holders visible; node mtimes on/dev/ttys0xxspanned the prior week, consistent with gradual accumulation per Bash/session activity rather than a one-shot event.Quitting the app released all slots immediately; no reboot required.
Still broken.
v1.12603.1
Summary:
Title: Claude desktop app (macOS) leaks /dev/ptmx PTY master fds — exhausts
kern.tty.ptmx_max and breaks all PTY allocation system-wide
Environment:
Summary:
The desktop app does not close() the PTY master fd when a shell it spawned
exits. Open masters accumulate (one per spawned shell) until the kernel limit
kern.tty.ptmx_max (default 511) is exhausted, after which ALL new PTY
allocation fails machine-wide — not just inside the app.
Evidence (captured live):
sysctl kern.tty.ptmx_max-> 511lsof /dev/ptmx | awk 'NR>1{print $2,$1}' | sort | uniq -c | sort -rn-> a single process "Claude" (/Applications/Claude.app/Contents/MacOS/Claude)
holding 510 masters; total 511 of 511.
lsof /dev/ttys* | wc -l-> only 4 live slaves (a couple of real shells).So ~506 masters have NO slave attached = orphaned/leaked, not real usage.
Impact:
posix_openpt failed: Device not configured(ENXIO).sudoitself fails (unable to allocate pty: Device not configured) becausesudoers
use_ptyneeds a fresh PTY — so the natural fix(
sudo sysctl -w kern.tty.ptmx_max=...) is itself blocked. Effectivedeadlock; recovery needs a GUI-auth workaround or an app/system restart.
Reproduction:
lsof /dev/ptmx | wc -lclimb monotonically and never drop as shells exit.Workaround (not a fix):
osascript -e 'do shell script "/usr/sbin/sysctl -w kern.tty.ptmx_max=999" with administrator privileges'Regression / persistence:
NOT resolve it.
Expected behavior:
open-master count bounded by the number of live shells.
Another data point confirming this on macOS (Darwin 25.5.0, Apple Silicon):
kern.tty.ptmx_max= 511Claudeprocess (uptime ~26h) held 508 pty master fds; everything else combined ~3.python3 -c "import os; os.openpty()"reproduced the failure:OSError: [Errno 6] Device not configured(same ENXIO asforkpty: Device not configuredin new terminals).Quitting and relaunching the app dropped the count from 508 to 1 and restored allocation.
sudo sysctl -w kern.tty.ptmx_max=2048works as a temporary mitigation between restarts.Thanks @codebytere-ant