1. Codex CLI 批量扫描大项目时 EMFILE 是怎么被触发的Codex CLI 在 macOS 或 Linux 上跑批量文件任务时报出Error: EMFILE: too many open files, open /path/to/project/src/xxx.ts这个错误的本质不是 Codex CLI 本身有缺陷而是操作系统对单个进程能同时持有的文件描述符数量设了上限。Codex CLI 在递归读取目录树、监听文件变化、并发分析多个源文件时会在短时间内打开大量文件句柄一旦峰值超过ulimit -n的限制内核就直接拒绝后续的open()调用抛出 EMFILE。这个报错在什么场景下最容易出现我实测下来有三类触发条件最典型。第一类是 monorepo 或文件数量达到几千甚至上万的工程Codex CLI 执行扫描整个项目这类宽范围任务时会一次性把大量文件加入读取队列。第二类是启用了文件监听功能比如让 Codex CLI 持续跟踪项目变化监听器本身就会为每个被监视的文件持有一个描述符。第三类是 macOS 环境因为 macOS 默认的单进程文件描述符限制通常只有 256而主流 Linux 发行版默认是 1024所以同样的项目在 Mac 上更容易崩。先看一张触发路径的梳理方便你建立排查直觉Codex CLI 执行大范围文件扫描/分析任务 ↓ 递归遍历目录树并发打开文件读取内容 ↓ 当前进程已打开的文件描述符数量是否超过 ulimit -n ├─ 未超过 → 正常继续处理 └─ 已超过 → EMFILE: too many open files这里有个容易混淆的点EMFILE 和 ENOSPC 是两回事。EMFILE 说的是同时打开的文件数量超限ENOSPC 说的是磁盘剩余空间不足两者是完全独立的系统资源维度报错信息和排查方向都不同不要混着查。还有一个现象值得注意同一个任务重新运行有时候成功有时候失败。这是因为文件描述符的峰值取决于 Codex CLI 当时的并发调度、文件读取顺序、以及系统里其他进程占用的句柄数并不是一个固定值。所以你不能因为重跑一次好了就认为问题解决了它只是暂时没撞到上限。适合谁看这篇如果你在用 Codex CLI 处理大型项目、遇到过 EMFILE、或者想把 Codex CLI 的 API 通道统一到 TaoToken 来管理 Key 和 endpoint这篇从排查到配置到验证都能直接跟做。下面我会先讲清楚 ulimit 怎么查怎么改再给出可复制的配置片段最后把 Codex CLI 的 endpoint 和 auth.json 接到 TaoToken 上让句柄限制和鉴权配置都不再分散。2. 排查前先把 Codex CLI 的 API 通道接到 TaoToken在动手调 ulimit 之前我建议先把 Codex CLI 的 API 通道理顺。原因很实际很多人的 Codex CLI 报 EMFILE 时同时还在被 401、鉴权失败、endpoint 配错这些问题干扰排查起来分不清是句柄问题还是配置问题。把 API 通道统一到 TaoToken 之后Key 和 Base URL 只有一处排障时变量更少。TaoToken 是一个统一的模型 API 接入服务官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。它的作用是让你用一套 Key 和统一的 endpoint 去调用不同的模型Codex CLI、Claude Code、Cline 这些工具可以共用同一个通道不用每个工具单独配一套鉴权。Codex CLI 的配置核心是auth.json和 endpoint 两项。auth.json里放 API Keyendpoint 决定请求发到哪里。把这两项指向 TaoTokenCodex CLI 的所有请求就走统一通道了。这里要写全三件套Base URL、Key、Model ID缺一个都跑不起来。先拿到你的 TaoToken API Key入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。创建好 Key 之后Codex CLI 的auth.json通常位于~/.codex/auth.json内容结构如下{ OPENAI_API_KEY: sk-你的TaoTokenKey, OPENAI_BASE_URL: https://taotoken.net/api }注意 Base URL 用https://taotoken.net/api不要加 UTM 参数UTM 只用于官网跳转归因。Model ID 按你实际要用的模型填比如在 Codex CLI 的配置或命令行参数里指定模型名。如果你用的是 Claude Code 或 Cline 这类工具配置思路一致都是把 Base URL 指向 TaoToken 的 API 入口Key 用同一把。这样多工具共用一套鉴权句柄限制的调整也只需要在一个环境里做。配置文档可以参考 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有各工具的接入说明。把通道理顺之后我们再回到 EMFILE 本身这时候如果还报错就能确定是句柄限制问题而不是鉴权或 endpoint 问题。3. 可复制的 ulimit 配置片段与 Codex CLI 并发调优这一节是核心操作部分我会给出可以直接复制的配置片段覆盖临时调整、永久调整、Docker 容器三种场景并说明 Codex CLI 并发打开文件数怎么定位。3.1 查看当前文件描述符限制第一步永远是先看当前值别急着改ulimit -n这个命令输出当前 shell 会话的软限制。你还可以看硬限制ulimit -Hn软限制是进程实际生效的上限硬限制是软限制能提升到的天花板。普通用户可以把软限制提到硬限制以内但超过硬限制就需要 root 权限。macOS 默认软限制常见是 256Linux 服务器常见是 1024。3.2 临时提高限制当前会话生效最直接的方式只对当前终端会话有效关掉终端就恢复ulimit -n 10240然后在这个终端里重新运行 Codex CLIcodex 帮我分析整个项目的代码结构这个方式适合快速验证是不是 ulimit 导致的 EMFILE。如果提高之后任务能跑完基本就确认了根因。3.3 macOS 永久提高系统级限制macOS 默认值偏低长期处理大项目建议做永久调整。先改系统级配置sudo tee -a /etc/sysctl.conf EOF kern.maxfiles65536 kern.maxfilesperproc32768 EOF然后在 shell 配置文件里设置默认 ulimit。如果你用 zshecho ulimit -n 10240 ~/.zshrc如果你用 bashecho ulimit -n 10240 ~/.bashrc/etc/sysctl.conf的改动需要重启电脑生效shell 配置的改动重新打开终端或source ~/.zshrc即可。3.4 Linux 永久提高限制Linux 上更规范的做法是改/etc/security/limits.confsudo tee -a /etc/security/limits.conf EOF * soft nofile 10240 * hard nofile 65536 EOF如果系统用 systemd还需要确认DefaultLimitNOFILEsudo tee -a /etc/systemd/system.conf EOF DefaultLimitNOFILE10240 EOF改完执行sudo systemctl daemon-reexec让 systemd 重新加载配置。3.5 Docker 容器场景单独配置容器不会自动继承主机的 ulimit 调整必须单独配。用docker run时docker run --ulimit nofile10240:10240 your-image codex ...用 docker-compose 时services: codex-runner: image: your-image ulimits: nofile: soft: 10240 hard: 10240soft和hard都设成同一个值避免容器内进程无法自行提升。3.6 定位 Codex CLI 并发打开文件数想知道 Codex CLI 到底开了多少句柄可以在任务运行时用lsof观察lsof -p $(pgrep -f codex) | wc -l这个数字就是当前 Codex CLI 进程持有的文件描述符总数。如果它逼近你设置的ulimit -n说明限制设得不够或者任务范围太大。你也可以持续观察峰值while true; do lsof -p $(pgrep -f codex) | wc -l; sleep 1; done跑任务时盯着这个数字就能看到峰值出现在哪个阶段。3.7 缩小任务范围与排除目录提高限制只是扩大安全边界更根本的做法是降低峰值需求。不要让 Codex CLI 一次性扫描整个超大项目把任务限定到具体子目录帮我检查整个项目里所有未使用的导入 帮我检查 src/modules/user 目录下所有未使用的导入同时确认.gitignore排除了不需要扫描的目录node_modules/ dist/ build/ .next/ coverage/这些目录往往包含海量文件是句柄被大量占用的常见来源。分模块处理不仅规避 EMFILE也让每次任务的反馈更聚焦、更容易验证。4. 验证请求与成功结果确认配置改完之后必须验证否则你不知道是配置没生效还是问题另有原因。验证分两层先确认 ulimit 生效再确认 Codex CLI 任务能跑通。4.1 验证 ulimit 是否生效新开一个终端执行ulimit -n如果输出是你设置的值比如 10240说明 shell 配置生效了。如果还是旧值检查你的 shell 配置文件路径对不对以及是否重新加载了配置。对于 macOS 的/etc/sysctl.conf改动重启后验证sysctl kern.maxfiles sysctl kern.maxfilesperproc输出应该对应你设置的值。4.2 验证 Codex CLI 能正常请求先做一个简单的请求确认 API 通道没问题codex 用一句话说明这个项目是做什么的如果这一步就报 401 或鉴权错误说明auth.json里的 Key 或 Base URL 有问题回到第 2 节检查。确认~/.codex/auth.json里OPENAI_BASE_URL是https://taotoken.net/apiOPENAI_API_KEY是你在 TaoToken 创建的 Key。你也可以用模型对话页面快速验证 Key 是否可用 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 在页面上发一条消息能正常返回就说明 Key 和通道没问题。4.3 验证大范围任务不再触发 EMFILE这是关键验证。用之前会触发 EMFILE 的那个任务重跑codex 帮我分析整个项目的代码结构同时在另一个终端观察句柄数while true; do lsof -p $(pgrep -f codex) | wc -l; sleep 1; done成功的结果是任务完整跑完没有出现EMFILE: too many open files句柄数峰值低于你设置的ulimit -n。如果任务跑完但句柄数峰值很接近上限说明你的限制还是偏紧建议再往上调一档或者把任务范围拆小。4.4 验证 Docker 容器内的限制如果 Codex CLI 跑在容器里进容器验证docker exec -it 容器名 bash ulimit -n输出应该是你在--ulimit或 compose 里设置的值。然后在容器内跑 Codex CLI 任务确认不再报 EMFILE。4.5 成功结果的判断标准一次成功的验证应该满足这几个条件ulimit -n输出符合预期Codex CLI 简单请求正常返回大范围任务完整跑完无 EMFILE句柄数峰值有安全余量。四个都满足才算真正解决。5. 本篇常见报错排查对照这一节把实际会遇到的报错和对应处理列出来方便你对照排查。5.1 EMFILE: too many open files这是本篇主问题。排查顺序先ulimit -n看当前值再lsof -p $(pgrep -f codex) | wc -l看实际句柄数如果实际值逼近限制就提高 ulimit 或缩小任务范围。macOS 用户优先做永久调整因为默认值太低。5.2 401 Unauthorized这个和 EMFILE 无关是鉴权问题。检查~/.codex/auth.json里的OPENAI_API_KEY是否是有效的 TaoToken Key以及OPENAI_BASE_URL是否指向https://taotoken.net/api。Key 过期或复制时带了空格都会导致 401。重新在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建一个 Key 替换试试。5.3 local proxy failed这个报错通常出现在你本地配了代理转发但代理进程没起来或端口不对。检查你的代理配置确认转发目标指向 TaoToken 的 API 入口。如果你没主动配代理检查环境变量里有没有残留的HTTP_PROXY、HTTPS_PROXY清掉再试unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy5.4 reading choices 相关报错这类报错通常是响应解析失败常见原因是 endpoint 返回的不是预期格式或者 Model ID 填错了。确认你在 Codex CLI 里指定的 Model ID 是 TaoToken 支持的模型名Base URL 没有多余路径。可以先用模型对话页面确认该模型能正常返回再回到 Codex CLI 排查。5.5 OAuth 相关报错如果你之前用 OAuth 方式登录过 Codex CLIauth.json里可能残留 OAuth token和 API Key 方式冲突。处理方式是清掉旧的 OAuth 凭据只保留 API Key 配置。确认auth.json里是OPENAI_API_KEY而不是 OAuth 相关字段。5.6 改了 ulimit 但重启后失效说明你只做了临时调整没写进 shell 配置文件。检查~/.zshrc或~/.bashrc里有没有ulimit -n 10240这一行以及当前 shell 是不是你配置的那个。macOS 还要确认/etc/sysctl.conf的改动已经重启生效。5.7 容器内改了但宿主机没改容器和宿主机的 ulimit 是独立的。你在宿主机改了limits.conf容器不会自动继承。必须在docker run --ulimit或 compose 的ulimits里单独配。反过来容器内改了也不影响宿主机。5.8 排查清单速查□ 1. ulimit -n 查看当前限制 □ 2. lsof -p $(pgrep -f codex) | wc -l 看实际句柄数 □ 3. 临时提高 ulimit 后重跑任务验证 □ 4. macOS 用户做永久系统级调整 □ 5. 检查 .gitignore 是否排除 node_modules 等大目录 □ 6. 把任务范围缩小到具体子目录 □ 7. 容器场景单独配 --ulimit □ 8. 确认 auth.json 的 Base URL 和 Key 指向 TaoToken6. 把 Codex CLI 的 Key 与句柄配置统一到 TaoToken 工作流走到这里EMFILE 的排查和修复路径已经完整了。最后我想说的是怎么把这件事变成长期稳定的工作流而不是每次出问题再临时救火。核心思路是把两个维度都统一起来。第一个维度是 API 通道Codex CLI、Claude Code、Cline 这些工具全部指向 TaoToken 的同一个 Base URL 和同一把 Keyauth.json只维护一份endpoint 只有一处。这样鉴权问题不会分散到多个工具里排查时变量最少。第二个维度是系统资源把 ulimit 配置写进团队的环境初始化脚本新机器一上来就是调好的状态不用每个人各自踩坑。对于长期跑编码任务和 Agent 场景的可以考虑用 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 把额度管理和通道配置一起管起来。如果你更想先验证模型效果可以到模型对话页面直接试 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 各工具的配置方式都有说明。我自己的习惯是处理超大项目时把任务范围拆到合理模块粒度当成默认操作而不是等 EMFILE 出现再拆。这不仅能规避文件描述符耗尽也让每次任务的执行结果更容易验证和把控。ulimit 调高是扩大安全边界任务拆分是降低峰值需求两者配合才是长期稳定的做法。最后给一个可以直接放进团队环境初始化脚本的片段把 ulimit 和 Codex CLI 配置一起搞定#!/bin/bash # 提高文件描述符限制 ulimit -n 10240 # 配置 Codex CLI 指向 TaoToken mkdir -p ~/.codex cat ~/.codex/auth.json EOF { OPENAI_API_KEY: sk-你的TaoTokenKey, OPENAI_BASE_URL: https://taotoken.net/api } EOF echo 环境初始化完成当前 ulimit -n $(ulimit -n)把这个脚本放进新机器的初始化流程Codex CLI 的句柄限制和 API 通道就都是统一状态了。