claude-code - 💡(How to fix) Fix Pty file-descriptor leak in Claude.app exhausts macOS `kern.tty.ptmx_max` after ~3 days, blocks all forkpty system-wide

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

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

Error Message

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.

  • Recovery requires either kernel knowledge (sysctl) or knowing to quit Claude.app — the error message gives no hint that Claude is the cause.

Root Cause

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.

Fix Action

Fix / Workaround

Workaround (non-destructive, no file edit, no reboot, no Claude.app restart)

Code Example

[forkpty: Device not configured]
[Konnte keinen neuen Prozess erstellen und ein Pseudo-TTY öffnen.]

---

$ 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

---

$ lsof /dev/ptmx 2>/dev/null | awk '{print $1, $2}' | sort | uniq -c | sort -rn
 511 Claude 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

---

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

---

do shell script "/usr/sbin/sysctl -w kern.tty.ptmx_max=999" with administrator privileges
RAW_BUFFERClick to expand / collapse

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

FieldValue
macOS26.3 (Tahoe), build 25D125
KernelDarwin 25.3.0 / xnu-12377.81.4 / arm64 / Apple M-series
Claude.app1.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 failure3d 12h
Mac uptime at failure9d 13h

Reproduction

  1. Launch Claude.app on macOS Tahoe.
  2. Use it actively for 2–3 days with frequent Skill / subagent / Bash-tool invocations (each spawns a child shell via pty).
  3. Eventually, lsof /dev/ptmx shows the Claude PID holding fds equal to or approaching sysctl kern.tty.ptmx_max.
  4. Open Terminal.app (or any other terminal emulator).
  5. 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=2048 is rejected with EINVAL on 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.spawn call site in the skill / subagent / Bash-tool execution path that does not register a close handler on the exit event 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

  1. Investigate the leak ((revoked) fd accumulation on the main Claude.app PID).
  2. Add a defensive close() on pty master fds when the child exits / errors.
  3. 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.
  4. 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.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

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.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING