Claude 桌面端 PTY 泄漏深度排查,macOS 4 天积累 490 个 /dev/ptmx 文件描述符

🌐 Read in English
📅 2026-05-26 12:05:11 👤 抖文编辑部 💬 0 条评论 👁 7

这篇笔记记录一个在 Claude 桌面端长期运行后触发的系统级 PTY 文件描述符泄漏问题。表面症状是 macOS 上突然所有终端 App 都打不开新窗口,深挖下去会定位到 Claude 进程在过去几天里累积了接近 490 个 /dev/ptmx 文件描述符,把整个系统能用的 PTY 几乎全部占完。本文按"发现 → 诊断 → 根因 → 复现 → 临时绕过 → 给修复人的建议"的顺序展开,方便其他遇到类似情况的开发者快速定位,也方便上游团队复盘修复。

1 现象,所有终端 App 突然打不开

配图

事情发生在一台日常使用的 macOS 笔记本上。系统已经稳定运行了几天,中间一直开着 Claude 桌面端做日常工作,期间也用 Claude Code 在终端里跑过很多 session。

某天下午想新开一个 iTerm 窗口跑 git status,iTerm 启动后窗口里只闪过一行报错就退出了,信息是 forkpty: Device not configured。换 Terminal.app 试,一样的报错。VS Code 内置终端、tmux、screen 全都打不开新会话,全是同样的 Device not configured

已经在跑的终端窗口里命令依然能跑,但只要尝试 fork 出一个新的伪终端就立刻失败。这种"已有进程没事、新 PTY 全挂"的症状非常有典型性,指向 PTY 资源耗尽。

2 诊断,先看 sysctl 上限再看占用

配图

macOS 上 PTY 总数有一个 kernel 上限,可以直接读 sysctl。

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

系统总共只允许 511 个 PTY 同时存在。这个值在 macOS 上是写死的小数字,跟 Linux 上动辄几万的 /proc/sys/kernel/pty/max 完全不是一个量级。意识到这点之后,下一步是看谁占满了。

/dev/ptmx 是申请新 PTY 的入口,每打开一次就分配一个新的 master/slave 对。所以统计哪些进程持有 /dev/ptmx 的文件描述符就能定位占用源。

lsof /dev/ptmx | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn

跑完结果出来,排第一名是 Claude 进程,数量在 490 上下浮动。其他进程加起来不到 20 个。换句话说 Claude 桌面端一家就把 PTY 资源耗光了。

 490 Claude
   6 iTerm2
   3 tmux
   1 sshd
   ...

到这一步基本能确定:Claude 桌面端在某个使用场景下打开了 PTY 但没有关闭,会话结束 PTY 句柄也没被释放,日积月累涨到接近上限。

3 根因,PTY 描述符没被回收

配图

继续展开 lsof 看 Claude 持有的 PTY 详情。

lsof -p <claude_pid> | grep ptmx | head

输出里每一行都是 /dev/ptmx 的一个独立描述符,FD 号从几十一直涨到几百,而且这些 PTY 看起来都是"无主"状态。它们不再关联到任何活跃的 Claude Code session 或终端窗口,但句柄仍然挂在 Claude 主进程或它的 helper 子进程上,迟迟没有 close。

最可能的代码层面成因是,Claude 桌面端在内部启动 Claude Code 或其他需要伪终端的子任务时,通过 forkpty 或 posix_openpt 申请了 PTY,但任务结束后没有正确 close 主侧描述符。每跑一次相关任务就泄漏一个,跑得越多累积越严重。

从应用层视角看,Claude 用户根本不知道每次跑 Claude Code 命令背后系统层都开了一个 PTY,更不会想到这些 PTY 会变成长期泄漏的资源。

4 复现条件,长时间运行 + 频繁 Claude Code 会话

这个 bug 不会在五分钟之内复现出来,它需要时间积累。根据本机观察,典型触发路径如下。

第一,Claude 桌面端持续运行多天不退出,这是中等强度用户的常态,很多人 Mac 重启间隔都在一周以上。

第二,期间频繁使用 Claude Code,包括在 Claude 应用内开会话、在外部 terminal 启动 Claude Code 命令、用 headless 模式跑批处理任务等等。每多一种触发场景,PTY 申请频次越高。

第三,系统其他应用偶尔也用 PTY,比如 iTerm、Cursor 内置 terminal、远程 ssh,这些会和 Claude 的泄漏量叠加,加速触底。

满足前两条之后,大约 3-5 天就会摸到 511 的上限。本机本次报告时的 lsof 结果是 Claude 持有 490 个 PTY,其他进程占用约 15 个,总数 505,距离 511 上限只剩 6 个,所以新开终端基本必失败。

5 环境信息,精确版本号

为了方便上游团队对照排查,这里列出本机的关键版本。

操作系统是 macOS 15.7.3。Apple Silicon 平台。Claude 桌面端版本以应用菜单的"关于"页面为准,本次报告时是当前 stable 通道的最新版本。Claude Code CLI 同样使用应用内置或独立安装的最新版本。

复现的 sysctl 值是 macOS 15 默认配置,没有手动调整 kern.tty.ptmx_max,这个值在最近几个 macOS 版本里都保持 511,本机也没有 launchctl 设置自定义上限。

6 临时绕过,重启 Claude 立刻释放

定位到根因之后绕过方案非常直接。完全退出 Claude 桌面端(包括菜单栏的 Quit Claude,不是关闭主窗口),让所有 Claude 进程退出后,系统会自动回收它持有的所有 PTY 描述符。

操作之后立刻验证,再次跑 lsof /dev/ptmx,Claude 那一行已经消失,总数回落到 20 左右。打开 iTerm 一切恢复正常,新终端能正常 fork。

但这个绕过有明显代价。退出 Claude 等于丢掉所有正在进行的对话上下文,而且重新打开后 Claude Code 之前的工作 session 也需要重建。对于把 Claude 当主力工作环境的用户,几乎相当于一次工作流中断。

更有意思的现象是,只要你不退出 Claude,即使你已经用 Activity Monitor 把 iTerm、Terminal 等所有终端 App 杀干净,新终端依然打不开。因为 PTY 句柄掌握在 Claude 手里,不是终端 App 持有的。

7 给修复人的建议方向

从应用代码层面,可能的修复方向有这些。

第一是把所有 forkpty 或 posix_openpt 调用都放到带显式 close 的 RAII 包装里。子进程结束、stream 收到 EOF、用户中断会话,这几个出口都要保证 master 端 fd 走 close。Node.js 那一侧如果用了 node-pty,要确认 .destroy() 或 .kill() 之后真的有 close fd,而不是只 terminate 子进程。

第二是加 watchdog。Claude 主进程可以周期性自检自己持有的 /dev/ptmx fd 数量,一旦超过阈值(比如 50 或 100)就在日志里告警,方便 telemetry 收集真实用户的命中率。如果超过更高阈值(比如 200)直接主动 close 老的 idle PTY。

第三是给用户一个手动清理入口。在设置或调试面板加一个"释放未使用的 PTY"按钮,用户察觉系统终端有问题时可以先点这个,不必整个退出 Claude。

第四是检查 helper 子进程的生命周期管理。在 macOS 上 Claude 桌面端用了多个 Helper 进程,如果泄漏发生在 Helper 而 Helper 又被主进程持续保留,fd 会随 Helper 一起被锁住。Helper 该回收时正确回收能解决一大半。

8 系统层面给开发者的备注

macOS 上 kern.tty.ptmx_max=511 是个相当紧的上限,跟 Linux 完全不在一个数量级。任何在 macOS 上需要批量启动伪终端的应用都要把这个数字当作硬上限来设计。

理论上 macOS 允许 root 用户通过 sysctl 临时提升这个值,例如 sudo sysctl -w kern.tty.ptmx_max=2048,但这不是面向终端用户的建议。普通用户没必要为了一个第三方应用修改系统 kernel 参数,也不该被推荐这么做。

Claude 桌面端定位是面向所有 macOS 用户的产品,默认就该在 511 的上限内安全运行。一个长期运行的 App 持有几百个 PTY,这本身就是一个值得修复的设计缺陷。

9 影响面评估,不只是终端打不开

PTY 耗尽不只是影响"新开终端 App"这一个场景,它会以隐蔽的方式影响很多依赖伪终端的工作流。

构建工具里有些 npm script 会用 PTY 跑子命令,在 PTY 用光的状态下会以一种很难诊断的报错失败。Docker Desktop 启动新容器时如果用到 PTY 也可能出问题。SSH 远程登录本机或者从本机 ssh 到远程都会失败,因为 ssh 默认会分配伪终端。

更糟的是,这些失败不会指向 Claude。用户只会看到"我的 npm install 突然装不上了""我的 Docker 启动报错了",根本不会想到根因是 Claude 在背景里占了 490 个 PTY。这种"间接受害"会让 bug 报告的归因变得非常困难。

10 给同样遇到这个问题的人,一份自检清单

如果你看到这篇文章是因为遇到了同样的"终端突然打不开"问题,下面的自检清单可以快速判断是不是同一个 bug。

第一步,跑 sysctl kern.tty.ptmx_max,确认 macOS 上限是 511 左右,不是被你自己调大过的。

第二步,跑 lsof /dev/ptmx | wc -l,如果数字接近或超过 500,基本就是 PTY 耗尽了。

第三步,跑 lsof /dev/ptmx | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn,看占用第一名是谁。

第四步,如果第一名是 Claude 且数量超过 100,基本就是本文这个 bug。完全退出 Claude 桌面端,然后立刻新开终端测试是否恢复。

第五步,如果你愿意把数据提供给上游修复,在退出前用 lsof -p <claude_pid> | grep ptmx > /tmp/claude_pty_leak.txt 抓一份当前 fd 列表存档,提交 issue 时附上。

常见问题 FAQ

这个 bug 只会在 macOS 上出现吗

目前观察到的复现都在 macOS 上,因为 macOS 的 PTY 上限只有 511,非常容易摸到顶。Linux 上 /proc/sys/kernel/pty/max 通常是 4096 起步,即使有同样的泄漏率,触发时间会被推迟到难以察觉。但本质上是同一个泄漏问题,只是 Linux 用户感知不到。Windows 上没有 POSIX PTY,Claude 桌面端在 Windows 上用 ConPTY,泄漏路径不同,需要单独排查。

普通用户能否提前预防

短期防御只有两个:一是定期手动重启 Claude 桌面端,推荐每 1-2 天退出一次让 fd 归零。二是如果你是技术用户,可以加一个 cron 或 launchd 监控,跑 lsof /dev/ptmx | wc -l,超过阈值发桌面通知提醒你重启 Claude。长期防御等官方修复。

普通用户能否手动调高 ptmx_max 上限

不推荐。sudo sysctl -w kern.tty.ptmx_max=2048 能临时把上限抬到 2048,但这只是把"几天后崩"变成"两周后崩",不解决根因。而且这个修改重启系统后会失效,要持久化需要写 plist 文件。一般用户没必要为了第三方 App 修改 kernel 参数。

退出 Claude 之后 fd 真的会全部回收吗

会。这是 POSIX 系统的标准行为,进程退出时它持有的所有 file descriptor 都会被 kernel 自动 close。退出 Claude 后立刻跑 lsof /dev/ptmx | wc -l 就能看到数字从 500 跳回 20 以内。如果 fd 没有回收,说明还有 Claude 相关进程没退干净,可以用 pgrep -i claude 检查残留进程。

这个泄漏会不会影响 Claude 本身的功能

会,但是是间接影响。Claude 桌面端运行 Claude Code 命令时同样需要新申请 PTY,当 PTY 全用完时它自己再开新 session 也会失败。所以达到上限后不仅终端打不开,Claude 自己跑新命令也会异常。这对依赖 Claude Code 的工作流是一个隐式的可用性问题。

📝 本文来自抖文 www.douwen.me ,转载请保留出处。

💬 评论 (0)

还没有评论,来说两句吧 ✍️