macOS: PTY file descriptors leaked across a long Bash-heavy session, exhausting kern.tty.ptmx_max (forkpty / posix_openpt fail with ENXIO system-wide)

Resolved 💬 22 comments Opened May 9, 2026 by JDL747 Closed Jun 22, 2026

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

  1. Open Claude Code Desktop.
  2. Have a single long session that issues many Bash tool calls (in my case, several hours of dependency triage: npm install repeated, dev server start/stop cycles, curl smoke tests, gh pr calls, etc.). I'd estimate the session made ~300+ Bash tool invocations.
  3. 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.

View original on GitHub ↗

22 Comments

github-actions[bot] · 2 months ago

Found 1 possible duplicate issue:

  1. https://github.com/anthropics/claude-code/issues/47909

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

kennethsinder · 2 months ago

Independent +1 from a separate machine — same root cause, fresh corroborating data:

  • macOS Darwin 25.4.0, Claude Code 2.1.128 (desktop)
  • Single Claude.app PID holding 506 / 511 /dev/ptmx master fds after an 18h 22m session (kern.tty.ptmx_max: 511, system default)
  • For context, other apps' /dev/ptmx+ttys fd counts at the same moment: iTerm2 43, zsh 12, node 6, codex 6 — only Claude.app is leaking
  • Symptoms match the OP exactly: Terminal.app shows forkpty: Device not configured, iTerm2 instant-closes on launch, every system tool that needs a fresh PTY fails

Workaround that does not require quitting Claude: sudo sysctl -w kern.tty.ptmx_max=2048 raises 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: sudo won't be runnable from a Terminal that's already broken; Script Editor's do shell script ... with administrator privileges works 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:

  • #47909: per-process RLIMIT_NOFILE=256 exhausted → Claude-internal errors only (ENOENT: posix_spawn 'rg', "directory no longer exists", "low max file descriptors" on session resume)
  • This issue: system-wide kern.tty.ptmx_max=511 exhausted → Terminal.app, iTerm, tmux, ssh local PTY allocation, etc. break for every app on the machine

Same 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.

error "sysctl: kern.tty.ptmx_max=2048: Invalid argument" number 1
thomasbachem · 2 months ago

Confirming on Claude.app v1.6608.2, same single-PID pattern, ~501 of 511 over ~27 h

0xJacky · 1 month ago

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 using os.openpty() could only allocate a few more PTYs before failing:

failed_after 5 OSError(6, 'Device not configured') last /dev/ttys363

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:

ls /dev/ttys* | wc -l
522

# live slave-side tty devices actually opened by processes: about 13 unique /dev/ttys* entries

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:

480 Claude <pid>
14 Cursor <pid>
7 go <pid>
7 go <pid>
5 Xcode <pid>
2 node <pid>
1 node <pid>
1 Terminal <pid>

lsof for the Claude process showed hundreds of entries like:

Claude <pid> <fd>u CHR 15,<minor> 0t0 /dev/ptmx

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 run processes and zsh + gitstatusd pairs holding slave-side /dev/ttysNNN descriptors. Those accounted for only a small number of PTYs and were not sufficient to explain the /dev/ptmx exhaustion.

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.

ydidwania · 1 month ago

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 , reveals

   4 0 hours elapsed
   1 01 hours elapsed
   5 20 hours elapsed
   2 46 hours elapsed
   1 64 hours elapsed
   2 68 hours elapsed
   2 73 hours elapsed
   2 76 hours elapsed
   4 94 hours elapsed
  10 100 hours elapsed

Total 33 processes

matthewlloyd · 1 month ago

Note that the helpful workaround above, sudo sysctl -w kern.tty.ptmx_max=2048, may not work because sudo itself needs to allocate a pseudo-tty:

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

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_max beyond 999.

I am hitting this problem almost daily and I think it should be given high priority.

tycooon · 1 month ago

Same problem here, pls fix!

mitiay7 · 1 month ago

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:

[forkpty: Device not configured]
[Could not create a new process and open a pseudo-tty.]

This blocks every PTY-using app system-wide (Terminal, iTerm2, tmux, ssh, script, etc.) until Claude.app is restarted.

Diagnosis

/dev/ptmx permissions are fine, the pool is just fully consumed:

$ ls -la /dev/ptmx
crw-rw-rw-  1 root  tty    0xf0001ff May 27 21:27 /dev/ptmx

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

$ lsof 2>/dev/null | grep -E "/dev/ttys|/dev/ptmx" | wc -l
     511

All 511 are held by a single PID — the Claude.app main process:

$ lsof 2>/dev/null | grep -E "/dev/ttys|/dev/ptmx" | awk '{print $1, $2}' | sort | uniq -c | sort -rn
 511 Claude 78757

$ ps -p 78757 -o pid,ppid,user,command
  PID  PPID USER   COMMAND
78757     1 mitiay /Applications/Claude.app/Contents/MacOS/Claude

$ lsof -p 78757 | grep ptmx | head -5
Claude  78757 mitiay   47u   CHR   15,20  0t0  605 /dev/ptmx
Claude  78757 mitiay   75u   CHR    15,6  0t0  605 /dev/ptmx
Claude  78757 mitiay   83u   CHR   15,96  0t0  605 /dev/ptmx
Claude  78757 mitiay   84u   CHR   15,95  0t0  605 /dev/ptmx
Claude  78757 mitiay   91u   CHR  15,172  0t0  605 /dev/ptmx

That's the Electron parent process (not a Helper / Renderer / Bash subprocess). It accumulated 511 master-side /dev/ptmx fds and never released them. Slave devices (/dev/ttys*) keep showing up in lsof too — 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, matches who | grep console). During that span there were many parallel Claude Code sessions; right now pgrep -lf claude still shows ~10 live claude child processes (pairs of Helpers/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 claude processes, 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 child claude PIDs 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 child claude process via forkpty seems to be missing the close(masterfd) on child exit / disconnect.

Happy to dump lsof -p 78757 in full or run anything else useful before I have to restart the app.

MikeHLee · 1 month ago

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.

ravisain231 · 1 month ago

Claude desktop app leaks pty master fds across Claude Code sessions → exhausts kern.tty.ptmx_max (511) → breaks ALL new terminals system-wide with forkpty: Device not configured

Reported & independently diagnosed by: Ravinder Sain — @ravisain231 This is a known issue (see "Relationship to existing reports"). The original contribution of this write-up is the controlled kill-experiment that empirically proves the leaked fds are owned by the long-lived Electron parent process, not the child sessions.

TL;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:

[forkpty: Device not configured]
[Could not create a new process and open a pseudo-tty.]

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 lsof simultaneously 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

  • System-wide, not app-local. Exhausting kern.tty.ptmx_max denies 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.
  • Silent until the wall. There is no warning as the app approaches the cap. The failure is sudden and total.
  • Misleading error. 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, /dev perms, reinstalling the terminal, etc.).
  • Self-trapping. The usual band-aid — raising kern.tty.ptmx_max via sudo sysctl -w — is unreachable, because sudo needs 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.
  • Time-bombed. It is a slow leak: it only bites after the app has been left running for days across many sessions, so it tends to strike during long uninterrupted work stretches.

---

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) |

A separate, older EtreCheckPro report (2023, a different MacBook Pro 16") was used only as a pattern reference — see "How this was found." The numbers above are from the live machine.

---

Symptom

Opening a terminal window anywhere (Terminal.app, iTerm2, VS Code, SSH session into the box, etc.) immediately prints:

[forkpty: Device not configured]
[Could not create a new process and open a pseudo-tty.]

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:

  1. Recognised the failure mode, not just the message. forkpty: Device not configured is 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."
  1. Brought a prior, similar incident as a reference. The user supplied an EtreCheckPro system report from an earlier, similar episode on another Mac. In that report the tell-tale signals were:
  • Heavy CPU usage / Runaway user process
  • A single app spawning a runaway swarm of helper processes (in that case VS Code19 Code Helper (Plugin) instances at 615% CPU)
  • General system-modification / resource-pressure flags

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.

  1. Pointed the diagnosis at process/handle exhaustion. With that direction, the investigation went straight to the pty subsystem limits (kern.tty.ptmx_max) and to which process holds the open pty handles (lsof /dev/ptmx) — rather than wasting time on /dev permissions, 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: forkpty was failing while plain fork+exec over pipes still succeeded, which already localises the fault to the pty subsystem rather than to process creation in general.

1. Limits

$ sysctl kern.tty.ptmx_max kern.maxproc kern.maxprocperuid
kern.tty.ptmx_max: 511      # hard cap on concurrently-open ptys
kern.maxproc: 4000
kern.maxprocperuid: 2666

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

$ lsof -nP | grep -c "/dev/ptmx"          # open pty MASTER handles
511
$ lsof -nP | grep "/dev/ttys" | wc -l     # open pty SLAVE handles
0

511 open masters, exactly at the 511 cap. Zero free ptys. That is why forkpty fails.

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

$ lsof -nP | grep "/dev/ptmx" | awk '{print $1, $2}' | sort | uniq -c | sort -rn | head
 511 Claude 40685

$ ps -o pid,ppid,etime,comm -p 40685
  PID  PPID     ELAPSED COMM
40685     1 09-00:17:04 /Applications/Claude.app/Contents/MacOS/Claude

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

$ pgrep -fl "claude.app/Contents/MacOS/claude" | wc -l
74-76                     # live Claude Code CLI sessions

# age distribution of those CLI processes:
#   20  under 1 day
#   56  >= 1 day old  (2-6 days)

~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

$ sudo -n true
sudo: a password is required        # exit 1

Raising kern.tty.ptmx_max requires root, sudo requires 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:

# killed 56 child sessions (TERM ignored by all → escalated to KILL)
survived TERM: 56
sending KILL to 56 survivors...
still alive after KILL: 0
# claude CLI processes:  76 -> 18

$ lsof -nP | grep -c "/dev/ptmx"
511         # <-- UNCHANGED. Still 511. Killing children freed ZERO ptys.

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_openpt ownership 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:

ptys_leaked ≈ Σ (sessions that have ended but whose master fd was never closed)

and converge on the system ceiling kern.tty.ptmx_max (default 511). At that point forkpty() returns ENXIO ("Device not configured") for every consumer on the machine.

The 0 slaves / 511 masters measurement confirms the masters are orphaned (no live child on the other end) — they are pure leaks, not in-use sessions.

---

Reproduction

  1. Launch the Claude desktop app and leave it running.
  2. Open and close many Claude Code sessions over time (or just let sessions accumulate across days of normal use).
  3. Watch the open pty masters climb and never decrease when sessions end:

``bash
watch -n 5 'lsof -nP | grep -c /dev/ptmx'
``

  1. As the count approaches sysctl kern.tty.ptmx_max (511), new terminals start failing with forkpty: 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)

  1. Quit the Claude desktop app (Cmd-Q, or right-click Dock icon → Quit) and relaunch it. This closes the app process, releasing all leaked pty masters at once. Terminals work again immediately.
  • Note: killing individual Claude Code session processes does not help — see proof above.
  1. Prevent recurrence: don't leave the app running for many days with dozens of accumulated sessions; restart it periodically.
  2. Optional cushion (does not fix the leak, only delays the wall — and only doable once you have a working terminal again):

``bash
sudo sysctl -w kern.tty.ptmx_max=4096 # this boot only
`
To persist across reboots, install a LaunchDaemon that runs the same
sysctl -w` at boot.

Diagnostic one-liners (to confirm you have this exact issue)

sysctl kern.tty.ptmx_max                                  # the cap (default 511)
lsof -nP | grep -c /dev/ptmx                              # open pty masters; == cap means exhausted
lsof -nP | grep /dev/ptmx | awk '{print $1,$2}' | sort | uniq -c | sort -rn | head   # who holds them
lsof -nP | grep /dev/ttys | wc -l                         # open slaves; ~0 alongside a full master count == leak

If the holder is Claude <pid> and the master count equals kern.tty.ptmx_max, this is the bug.

---

Suggested fixes (for maintainers)

  1. Close the pty master fd on session teardown — the core fix. Ensure the session lifecycle in the desktop app always kill()s the node-pty instance and close()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.
  2. Reap orphaned masters. Detect masters whose child has exited (slave closed) and close them proactively — defense-in-depth against any missed teardown path. The 0 slaves / N masters condition is directly detectable.
  3. Budget / cap concurrent ptys. Enforce a ceiling on concurrent session-ptys well below kern.tty.ptmx_max, with backpressure or oldest-idle eviction, so a leak (or just heavy use) can never consume the whole system pool.
  4. Fail loud, fail helpful. When 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 raw Device not configured.
  5. Telemetry guardrail. Log/emit the app's open-master count; warn as it approaches the system cap.

---

Appendix: timeline of the diagnosis

| Step | Finding |
|---|---|
| Symptom | forkpty: Device not configured on 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.app releases all 511 ptys; only viable release path |

odakin · 1 month ago

Still reproducing on the latest release (v2.1.165) — confirming this is unfixed in the current build, not resolved by upgrading.

Environment

  • Claude desktop app, Claude Code CLI v2.1.165 (newer than the 2.1.138–2.1.156 reported above)
  • macOS Sonoma 14.7.4 (build 23H420), Apple Silicon (arm64)
  • Independently observed earlier on macOS Ventura 13.7.x / Intel → cross-OS, cross-arch

Live measurement (single session, in real time)
The Electron parent (/Applications/Claude.app/Contents/MacOS/Claude) holds the leaked /dev/ptmx masters. During one active session the count climbed monotonically as Bash tool calls ran:

$ lsof /dev/ptmx | grep -c /dev/ptmx
511 -> 559 -> 590 -> 593     # ~+30 over a few minutes of tool use
$ lsof /dev/ptmx | awk '/ptmx/{print $1,$2}' | sort | uniq -c | sort -rn | head -1
593 Claude 60976

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=511 this 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/osascript path 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_spawn and failing to close that fd on the child_process fallback 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_usage traces or test a patched build. Canonical tracking issue appears to be #47909.

TomCruiseTorpedo · 1 month ago

Another live datapoint plus a scope clarification and a detection/mitigation toolkit, in case it helps triage.

Live measurement (2026-06-08, still reproducing)

  • Claude Code CLI 2.1.165, Claude Desktop, macOS (Apple Silicon), kern.tty.ptmx_max=511 (default)
  • A single process held 510/511 /dev/ptmx masters; one unrelated tmux held the last one → pool fully exhausted, forkpty: Device not configured system-wide.
$ lsof -n /dev/ptmx | awk 'NR>1{c[$2]++} END{for(p in c) print c[p],p}' | sort -rn
510 87210
1 17909
$ ps -p 87210 -o comm=
/Applications/Claude.app/Contents/MacOS/Claude
$ ps -p 17909 -o comm=
tmux

Scope: the holder is the Electron parent, not the embedded CLI

Walking the process chain of a Desktop-hosted session:

zsh
  -> ~/Library/Application Support/Claude/claude-code/2.1.165/.../claude   (embedded CLI binary)
  -> /Applications/Claude.app/Contents/Helpers/disclaimer
  -> /Applications/Claude.app/Contents/MacOS/Claude                       (Electron main — holds the masters)

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 claude as a standalone CLI in a normal terminal (installed via claude.ai/install.sh or 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 (sudo itself 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:

ceiling=$(sysctl -n kern.tty.ptmx_max)
used=$(lsof -n /dev/ptmx 2>/dev/null | awk 'NR>1' | wc -l | tr -d ' ')
echo "ptmx: $used/$ceiling used, $((ceiling-used)) free"

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_spawn and leaks it on the child_process fallback).

Happy to share the small headroom-monitor / preflight-gate scripts if useful.

kixelated · 1 month ago
sudo fs_usage -w -f exec,filesys 48432 2>/dev/null | grep -iE 'ptmx|posix_openpt| exec'

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.

ArangoGutierrez · 1 month ago

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/ptmx masters:

$ lsof -nP | grep '/dev/ptmx' | awk '{print $1, $2}' | sort | uniq -c | sort -rn
 426 Claude 83843
  84 Cursor 72962
   1 iTermServ 84040
   1 iTerm2 84034

$ lsof -nP -p 83843 | awk '$NF=="/dev/ptmx"{print $6}' | sort -u | wc -l
 426        # 426 distinct DEVICE minors -> 426 distinct PTYs, not duplicated fds

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 claude children), so nothing short of process exit releases them.

ArangoGutierrez · 1 month ago

Follow-up to my measurement above — a severity escalation observed at full exhaustion that triage should know about:

At the cap, sudo itself stops working. macOS 26 sudo allocates a fresh PTY for every command (use_pty), so once the pool is exhausted:

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

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:

$ osascript -e 'do shell script "/usr/sbin/sysctl -w kern.tty.ptmx_max=999" with administrator privileges'
kern.tty.ptmx_max: 511 -> 999

GUI password dialog, takes effect immediately. Verified recovery with a real forkpty: script -q /dev/null true succeeds again afterward.

Caveats: 999 is the hard ceiling (ttysNNN namespace), 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.

nbswgn100k-boop · 1 month ago

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/ptmx masters and the system hit full exhaustion (kern.tty.ptmx_max=511, default):

$ lsof -n -p <Claude main PID> | grep -c '/dev/ptmx'
499

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

$ ls /dev/ttys* | wc -l
527

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/ptmx masters 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/ptmx masters 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=999 mitigation is only reachable before the pool fills — at 511/511, sudo (use_pty on macOS 26) cannot allocate a PTY for itself. Our prevention until this is fixed is a daily desktop-app restart.

bratchenko · 1 month ago

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

  • Claude Desktop 1.11847.5, embedded Claude Code CLI 2.1.170
  • macOS 26.4.1 (build 25E253), Darwin 25.4.0, Apple Silicon (arm64)
  • kern.tty.ptmx_max=511 (default)

Measurement (2026-06-11, ~44 h app uptime)

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

$ lsof -nP | grep '/dev/ptmx' | awk '{print $1, $2}' | sort | uniq -c | sort -rn
 499 Claude 43421
  18 iTerm2 84950
   6 iTermServ 84959
   4 cmux 51380
   2 Codex 7886

$ ps -p 43421 -o lstart=,etime=,command=
Tue Jun  9 16:49:46 2026 01-19:48:35 /Applications/Claude.app/Contents/MacOS/Claude

The 499 masters are not backing live sessions — the process had only 20 children total, exactly 1 of them a shell:

$ pgrep -P 43421 | wc -l
      20        # Claude Helpers + disclaimer + 1 zsh

Two symptom strings not yet in this thread

  1. iTerm2 fails with a dialog: “A session ended very soon after starting. Check that the command in profile ‘Default’ is correct.” — worth recording verbatim because it actively misdirects the user toward debugging their iTerm2 profile and shell rc files instead of system-wide PTY exhaustion.
  2. Claude Desktop’s own embedded terminal panel fails with “Failed to spawn shell” — the app becomes a victim of its own leak (while its Bash tool keeps working, since that path runs over pipes and needs no PTY).

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.

eddie-timeglass · 1 month ago

Reproduced today with detailed measurements, adding as a data point since this hit the hard system limit and broke all terminals machine-wide.

Environment

  • macOS 26.4.1 (build 25E253), Apple Silicon
  • Claude desktop app 1.11847.5, bundled Claude Code 2.1.170
  • Heavy Bash-tool usage across ~12 desktop sessions

Measurements (after ~30h app uptime)

  • kern.tty.ptmx_max = 511 (default); ls /dev/ttys* | wc -l527
  • lsof /dev/ptmx showed the main Claude Electron process (single PID) holding 496 pty masters — every other app on the system held ≤6
  • Only 47 processes system-wide were attached to any pty slave, so ~450 of those masters were orphans whose shells had exited long ago — i.e. the master fd is never closed after the spawned shell terminates
  • The leak survives session completion: only 2 of 12 sessions were running, so cleanup isn't tied to session lifecycle — it looks like one leaked master per spawned shell/Bash invocation

Symptoms at exhaustion

  • zsh plugins: zpty: can't open pseudo terminal: device not configured (repeating), followed by is-at-least: maximum nested function level reached
  • Hyper: uncaught Error: forkpty(3) failed. from node-pty — no new terminal tabs could be opened anywhere on the system

Recovery
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).

mattpanther · 1 month ago

Another confirmed occurrence, current build.

Environment

  • macOS Darwin 25.5.0 (Apple Silicon)
  • Claude desktop app, bundled claude-code 2.1.170
  • App uptime at time of failure: ~2 days 21 hours, multiple sessions with heavy Bash tool use

Symptoms

  • Terminal.app: forkpty: Device not configured / Could not create a new process and open a pseudo-tty.
  • System-wide: all 511 dynamic PTY slots allocated (kern.tty.ptmx_max=511, ls /dev/ttys[0-9a-f][0-9a-f][0-9a-f] | wc -l → 511)

Attribution

$ lsof -n | grep -iE 'ptmx|/dev/ttys[0-9]' | awk '{print $1}' | sort | uniq -c
 511 Claude
$ ps -p 33170 -o pid,etime,comm
  PID     ELAPSED COMM
33170 02-21:40:24 /Applications/Claude.app/Contents/MacOS/Claude

A single Claude.app process held all 511 /dev/ptmx master fds. No slave-side holders visible; node mtimes on /dev/ttys0xx spanned 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.

benatwhopack · 29 days ago

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:

  • Claude desktop app v1.12603.1
  • macOS 26.5 (Darwin 25.5.0), Apple Silicon / arm64

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 -> 511
  • lsof /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:

  • New terminals, VS Code build/run tasks, and any PTY-needing process fail with

posix_openpt failed: Device not configured (ENXIO).

  • sudo itself fails (unable to allocate pty: Device not configured) because

sudoers use_pty needs a fresh PTY — so the natural fix
(sudo sysctl -w kern.tty.ptmx_max=...) is itself blocked. Effective
deadlock; recovery needs a GUI-auth workaround or an app/system restart.

Reproduction:

  1. Use the desktop app's shell-spawning features across a normal work session.
  2. Watch lsof /dev/ptmx | wc -l climb monotonically and never drop as shells exit.
  3. After ~511 spawns, all new PTY allocation fails system-wide.

Workaround (not a fix):

  • Quit & reopen the app -> releases all masters (count drops to ~1); or
  • Raise the cap via GUI auth (sudo is deadlocked):

osascript -e 'do shell script "/usr/sbin/sysctl -w kern.tty.ptmx_max=999" with administrator privileges'

Regression / persistence:

  • First reported via /bug on 2026-06-05 against v1.9659.4.
  • Re-confirmed 2026-06-17 on v1.12603.1 — still leaking. Updating the app does

NOT resolve it.

Expected behavior:

  • The app should close() each PTY master when its child shell exits, keeping the

open-master count bounded by the number of live shells.

tomchambers2 · 26 days ago

Another data point confirming this on macOS (Darwin 25.5.0, Apple Silicon):

  • kern.tty.ptmx_max = 511
  • A single long-lived Claude process (uptime ~26h) held 508 pty master fds; everything else combined ~3.
  • The leaking process had 0 live child processes while holding 508 pty fds — the shells had all exited and been reaped, but their pty masters were never closed. Classic per-invocation fd leak.
  • python3 -c "import os; os.openpty()" reproduced the failure: OSError: [Errno 6] Device not configured (same ENXIO as forkpty: Device not configured in new terminals).
$ lsof -n | grep /dev/ttys | awk '{print $1}' | sort | uniq -c | sort -rn | head
 508 Claude
  12 zsh
   3 Devin

Quitting and relaunching the app dropped the count from 508 to 1 and restored allocation. sudo sysctl -w kern.tty.ptmx_max=2048 works as a temporary mitigation between restarts.

kixelated · 23 days ago

Thanks @codebytere-ant