跳到主要内容
OpenAI 把 CLI 用户当成了云租户

OpenAI 把 CLI 用户当成了云租户

深报
深报

· 阅读约 22 分钟

issue #48195 是 9 月 25 号挂上去的。标题克制到近乎礼貌:“Should be opt-in rather than enabled by default”。但底下压着事:一个通过 npm 装来的 CLI,在 Linux/WSL2 环境里留了一个常驻后台守护进程,用它给的代理环境变量,每小时检查一次更新,还把自己复制到 ~/.codex/packages/app-server-daemon/releases/<版本>/ 下运行。同一天两条评论,21 赞和 27 赞,都是被这个行为吓到的。到 10 月 12 日,laurynas-karvelis 直接把默认行为类比为恶意软件,16 赞 2 爱心。最多赞那条是 27 个:“修复前 Codex CLI 不可用”。不算什么超大舆情,但它剖开的东西比表面大:开发者工具从“你安装的一个二进制”转向“在你机器上跑的一个服务”时,最早期的那批用户会怎么抵抗。

我先说我的视角:这不是一个 bug 报告。这也不是一次功能请求。这是软件信任模型变更的早期信号,发生在 0.157.0 这个小版本里,发布说明只有一句话——“自动后台服务器启动”。没说常驻,没说会从副本运行,没说每小时检查更新,没说固定第一个会话的环境变量。等用户发现这些的时候,事情已经从“升级了一个 npm 包”变成了“系统里多了一个来历不明、不随显式窗口退出、有自我更新权限的进程树”。

我要拆的是这个信号背后几层更硬的东西:谁在推动客户端服务化,为什么是这个时点,它和上一轮云化解耦、PaaS 化、agent 平台化的同构在哪里,以及它为什么偏偏在 GitHub issue 这种最保守的载体上被最激烈地掀翻。


犯罪现场:五个不对劲

第一个不对劲的点是那笔 374 MB。报告者确认,守护进程把 374 MB 的平台包复制到 ~/.codex/packages/app-server-daemon/releases/ 下,内容和 npm 安装的字节一致,但从副本运行。这听着像路径问题,其实不是。包管理器管理的二进制有清晰的来源、校验、卸载路径。复制到 ~/.codex 下的副本把这三样全绕过去了。npm update 不会碰这个目录。npm uninstall 也不会。你删了 CLI,守护进程可能还在,更新器可能还在。如果你已经通过 npm 装了包,为什么还要再复制一份一模一样的东西到另一个目录跑?这就是身份问题。

第二个是自更新。报告者说守护进程启动一个分离的更新器 codex app-server daemon pid-update-loop,启动 5 分钟后查第一次更新,之后每小时一次,下载新版本,重启后端。也就是说 0.157.0 安装的 npm 包里的守护进程,运行时可以把自己换成另一个版本。报告者用的词是 "decoupled"——后端版本和 CLI 版本解耦。这个解耦就是服务化客户端的核心机制:你装的是 A,运行时变成 B,而且不是通过你选的通道。桌面应用里这个很常见,CLI 里这是反常的。CLI 用户对版本的控制比图形应用严格得多,因为几乎所有人都在用脚本管环境。

第三个是环境变量被固定。守护进程把启动它的第一个会话的环境变量焊死。报告者每次通过 wrapper 设 HTTPS_PROXY,守护进程不管,它一直用第一个会话的代理。一个本该局部于会话的配置,被提升到了一个跨会话后台进程的持久状态。经常切网络、需要指定代理的人没法用“再设一下”解决,因为服务不重置。这个细节暴露了守护进程的默认假设:它假设一个一次配置、长期有效、不被用户环境频繁扰动的主机状态。服务器上这个假设成立。个人开发机上不成立。

第四个是 hook 上下文错位。两件事,同一根因:#42457 里 --remote 模式的 hook 收到的是守护进程的 cwd 而不是会话 cwd;#37537 里守护进程启动的 hook 认不出客户端终端,面向终端的 hook 直接失效。执行上下文从“TUI 会话”挪到“后台守护进程”之后,hook 看不见终端、目录、环境。开发者靠 hook 做自动化扩展,一个默认开启的常驻服务把这些全打破,这已经不只是性能问题,是既有约定被破坏。

第五个是停不干净。报告者试了 codex app-server daemon stop,app-server 停了,pid-update-loop 更新器还活着,70 秒后依然存活,得手动 pkill。用户主动停止,失败了一半。一个工具在用户显式禁用之后还留着一个定时器进程,这在安全实践里会被看作恶意软件特征,不论厂商是谁。用户把它当 CLI,它把自己当房东。


账怎么算

0.157.0 之后,用户打开一次交互式 Codex 会话,系统里发生的进程树变化是这样的:codex app-server --managed-daemon 以无 TTY 的独立会话启动,脱离原始终端。关掉所有 Codex 窗口之后它还在,重新挂到 PID 1。在 Linux 上被 PID 1 收养意味着失去父关系——正常情况下 shell 退出,子进程收到 SIGHUP 就该结束。但它跑在无 TTY 会话里,关窗口不触发终止,所以它才需要一个显式 stop 命令。这个设计从一开始就不是“挂在当前会话下面”的辅助进程,它一开始就是按跨会话、甚至跨重启存活的应用服务器做的。

374 MB 这个数字值得单独说两句。如果 Codex CLI 的核心能力只是把 prompt 转成 API 调用再流式返回,374 MB 的 app-server 已经接近某些 Electron 应用或预打包的 Node.js 运行时。发布说明没写要这个 app-server 干什么。可能的原因有几类:为更长上下文、更大内存的 agent 会话提供独立运行环境;为多客户端(多个 TUI 窗口、IDE 插件、Remote Control)准备一个本地共享后端;或者把一部分执行从 CLI 前端迁到一个可以统一配置、统一更新的服务层。issue 本身给不了答案。但“重新挂到 PID 1、关闭所有窗口后仍不退出”这个观察,已经把“为什么需要它”从合法问题推到了“它是不是设计上的常驻服务”边上。

更新器这账再算一笔。启动 5 分钟查第一次,之后每小时一次,下载新版本,重启后端。报告者明确指出:自动更新可能在 60 秒排空预算后强制终止活跃回合。不是猜测,是直接行为。用户跑一条长任务,更新器在后台滚新版本,重启后端,会话打断。一个每天跑几十上百次交互式会话的开发者,这个可靠性成本不低。每小时查一次、5 分钟后第一次查、下载重启——这是桌面应用的自动更新节奏搬进了 CLI。桌面应用不怕每小时查,因为更新多半保守、走空闲带宽;CLI 用户在脚本里自动化使用这个工具,每小时一次的更新就是定时炸弹。

禁用方案更说明问题。报告者给的临时步骤是四步:codex features disable daemon_auto_start、codex app-server daemon stop、pkill 杀掉更新器、删除 ~/.codex/packages/app-server-daemon。其中两步涉及主动杀进程和删目录。如果 daemon_auto_start 默认开启,那这个 feature 开关的意义就是逃生出口。但逃生出口的存在不能反过来证明默认行为正当。而且这个开关在 config 里有,官方配置参考里没有。评论者 foma-agent 在 10 月 12 日指出的就是这一点:官方配置参考中没有 daemon_auto_start 条目。一个默认开启但不可见的配置项,比默认关闭更糟。默认关闭你不会被骗;默认开启你连个文档都找不到。


四方各有各的算盘

这笔钱从哪来、流到哪去、卡在谁手里,逐方拆。

OpenAI 是卡位方。 它要把 CLI 从无状态客户端变成有常驻后端的平台,在开发者桌面上拿一个和 IDE 插件、桌面应用同等的服务地位。常驻服务意味着它可以在本地持有模型上下文、调度请求、推送更新、变成别人能调用的接口。它不再是一个被调用的工具,而是一个等待调用、甚至主动做事的服务。好处最直接的一条:把开发者桌面变成能持续运营的地盘。自更新后台进程让它在未来任何时刻都能推代码执行、agent 编排、Remote Control、桌面集成,不需要用户重新装什么。这是基础设施级别的卡位,不是功能级别的。

用户是接刀方。 用 npm 安装时用户以为自己控制的是版本和运行时机。0.157.0 之后发现自己控制的只剩前台 TUI 的生命周期。后端生命周期和版本已经不在 npm 层面了。加上自我更新和打断活跃回合,用户失去了三样最基础的控制权:何时运行、运行什么版本、何时更新。在 CLI 工具的惯例里,这三样默认属于用户。被收走之后,用户立刻感到“这不是我的工具了,这是他们的客户端”。

包管理器是被绕开的。 npm 在这里扮演了一个被架空的对象。装来的 CLI 本应遵从包管理器的版本规则、目录规则、卸载规则。守护进程复制、自更新、从副本运行,npm 的 uninstall 对它毫无办法。包管理器的权限被局限在“初始安装”这一件事上,后续状态全归应用自己。这是对既定分发契约的破坏。npm 没做错什么,但用户本能地认为它应该管住这事。

安全/审核方还没真正进场,但已经开始问问题了。 aetherxeg-source 在 10 月 3 号提出一个非常具体的 provenance 边界:区分“已安装 CLI 身份”和“守护进程执行身份”,并建议守护进程为每次客户端连接暴露执行收据,至少包含 client_cli_version、daemon_binary_digest、daemon_generation、updater_state、inherited_environment_generation。这些词已经很说明问题了:provenance、digest、generation,供应链审计的术语。Logarithmus 在 10 月 12 号问得更直接:Codex 是否已成为病毒,并担心利用住宅 IP 组建僵尸网络。st396gt 说自己在加固的 Raspberry Pi 上通过 SSH 接入,0.160.0 没看到守护进程,但文档要求宿主文件系统完整读写权限。这些指向同一个方向:一个工具的行为不受用户控制之后,安全审核者没法继续假设工具和用户站在同一边。如果工具能自我更新、能保留环境变量、能绕开包管理器,它的行为就成了黑盒。对一个黑盒,你只能说它是恶意软件或者不是,不能再谈“信任”。


为什么不是管理失误

很多人会把这个 issue 当 PR 管理问题收场:发布说明没写清,默认值草率,用户反馈放大,几天后提交默认关闭,归档。我不这么看。这个 issue 的广度——从环境变量固定到 hook 上下文错位到更新器停不掉——指向一个更深的结论:这不是配置疏漏,是整个服务化客户端架构方向与 CLI 既有约定之间系统性冲突的第一次充分暴露。它是一面镜子。照出来的是 OpenAI 已经不再把 Codex CLI 当作一个可安装的二进制脚本,而是在把它当作用户机器上运行的应用程序客户端。这个裂缝不是补 docs 焊死的。

报告者自己写过一句非常准的判断:“共享自我更新守护进程更适合 Remote Control 或桌面应用,或用户显式执行启动/引导命令时启用。”因为那些场景本来就是“应用”不是“命令”,服务化理所当然。CLI 不是应用。这条线是整件事的分界线。

社区时间线冷得比热更有意思。9 月 25 号报告者发问,当天两个人说遇到同样问题:“小版本号更新不应在未提醒用户的情况下突然创建未知服务器和进程”21 赞、“修复前 CLI 不可用”27 赞。同一天 tagorr 在 Windows 10 LTSC 上复现,给 config 里设 daemon_auto_start = false 的解法,说权限提升受阻时应自动回退本地模式,自动启动应 opt-in,4 赞。9 月 28 号提交 b51c036 引用此 issue,内容涉及默认关闭。9 月 29 号 felipeelopes 说 Windows 上 CLI 因此不可用。9 月 30 号 zdd 说完全无法输入内容。10 月 3 号 aetherxeg-source 开始谈 provenance 和 digest。10 月 12 号密集引爆:laurynas-karvelis 类比恶意软件,Logarithmus 问是否已成病毒,foma-agent 指出官方配置参考没条目,st396gt 说加固设备上没看到。10 月 13 号 shakefu 要回滚公司所有 Codex 席位许可,lattwood 说本应默认关闭,panzerdp 说安装几天后后台下载了 1.3GB 更新,必须默认禁用,skhaz 说已经请 Claude 移除并禁用它。

时间线里我最关注的不是恶骂。是早期几个评论者几乎不约而同地做出同一个技术判断:daemon_auto_start = false 能解决,或者干脆在 config 里禁掉。这说明开关存在。问题是没有任何官方文档提及。一个能被用户自己禁用的默认开启 feature,在小版本里悄悄上线,发布说明只有一句话,配置参考不更新——这才是一个成熟公司做出这种操作最让人不安的地方。社区反对的不是“可选功能”,是“默认开启、不被提及、只能靠用户自己挖出开关的功能”。如果更新日志写一句“我们正在试验后台服务器,可以 features disable daemon_auto_start 禁用”,这个 issue 拿不到 27 个赞。问题不是“有守护进程”,是“你为什么不说”。

Windows 上的反应也强烈。felipeelopes 说不可用,tagorr 在 LTSC 上复现,panzerdp 提到 1.3GB 后台下载。守护进程在 Windows 上的表现和 Linux 不同,但破坏性一样。报告者是 WSL2,这个问题跨平台,不是打包错误。1.3GB 的更新量如果属实,对一个 CLI 工具是灾难性的。几天内后台拉 1.3GB,这已经不是“推送新版本”,是占用用户资源做用户不知情的事。


历史对照:服务化的硬阈值

这个转变不是 OpenAI 发明的。从工具到服务化,在开发者基础设施里已经走过两轮完整周期。我选三段,比喊最近的类比更清楚。

第一段:编辑器服务化。VS Code 的远程开发、WSL 集成、扩展宿主,全建立在主编辑器之外的一个后端进程上。这个后端不是你手动起的,也不是你通过包管理器更新的,是编辑器自己在后台管的。用户接受它,原因就两条:功能收益足够大——远程开发带来的体验跃迁让服务化成本显得合理;文档足够完整、配置入口足够显式。这两个条件在 Codex 0.157.0 这全都没成立。收益没说,成本立刻感受到。

第二段:云 CLI SDK 的客户端化。AWS、GCP、Azure 都干过类似的事。无状态的命令行工具逐渐加上身份缓存、本地元数据、后台刷新 token 的辅助进程。用户一开始也反弹,因为 aws configure 写明文凭据或起辅助进程。后来为什么被接受了?主厂商给了足够可控性:环境变量可以接管一切,一次性 flags 可用,显式禁用缓存或辅助进程不影响脚本行为。Codex 的 daemon 默认开启,更新器不能禁用(#40969 仍然打开),停都停不干净。这就是接受和不接受的分界线。

第三段:包管理生态的“首运行向导”。Homebrew 有过,很多 CLI 也试过。第一次跑的时候下载资产、预编译、缓存、升级检查。这些行为开始时都引来 issue,被骂“不尊重用户”。最终被接受的理由几乎一样:行为透明,能看到它下载什么、写哪里;有显式关闭;只做安装相关的事。Codex 0.157.0 的守护进程把这三个接受条件全部绕过,所以被类比恶意软件不是用户不理性,是它的行为确实触发了恶意软件模式识别:无提示自安装、自更新、持久运行、去会话化、环境变量继承,每一项都是红旗。

上一轮历史结论很清楚:开发者对服务化的容忍有硬阈值——收益必须明确,关闭必须真实,行为必须透明,更新通道必须可控。跨过阈值,用户会把工具当恶意软件,不管厂商多大。这就是 2018-2021 那波云 CLI 之变还能被接受、而 Codex 0.157.0 直接骂作恶意软件的原因。没变的是开发者对终端主权的捍卫;变了的是工具供应商越来越不在乎这个阈值,因为商业模型不再靠工具本身,而是靠工具背后的云服务。


商业账本:为什么非要这么干

这个守护进程的真实作用,我理解是 OpenAI 在为 Codex 从“单向 API 客户端”升级为“可托管执行环境”打地基。常驻后端能做的几件事 CLI 做不了:维护跨会话的 agent 状态、把部分执行从 TUI 迁到后台、为 Remote Control 和桌面应用提供共享执行入口。报告者在 issue 里也说到了这一点。那些场景本来就是“应用”不是“命令”,服务化理所当然。CLI 不是应用。

商业账是这样的:如果 OpenAI 能把 Codex 从一次性调用变成长期运行的代理,它就可以卖更高阶的自动化。定价从按 token 计费转向按 agent 执行时长或任务计费,甚至推出托管工作器。托管工作器需要本地有个常驻进程接收任务、维持库存、执行动作。这个基础铺开之后,IDE 集成、桌面 agent、队列化批量任务都能挂上去。这就是为什么 OpenAI 愿意承受反对声——因为这一步的地基用 opt-in 铺不起来。先把后端常驻住,再逐渐暴露功能。用户感到“这不是 bug,这是路线图”,不是空穴来风。

控制更新节奏是另一笔账。自更新后台后端让 OpenAI 在用户机器上快速迭代智能体能力,绕开 npm 发布节奏,也绕开用户升级的不可预测性。更新器如果把不活跃时间窗口当部署窗口,用户几天不开 Codex,后端就悄悄升上去了。下次打开,“新功能已经在了”。这种模式在 SaaS 里叫无缝更新,在桌面应用里叫自动更新,在可编程 CLI 里叫恐怖更新。用户对版本可复现性的需求是硬性的,每小时查一次、重启后端的常驻服务,把可复现性打了个洞。


provenance 边界这件事,比默认开关重要

aetherxeg-source 那条 10 月 3 号的评论,比同页面那些“恶意软件”“病毒”的冷静得多。他提的是把“安装时的 CLI 身份”和“守护进程执行时的身份”分开。这可能比改一个默认值更根本。Provenance 边界解决的是“我信任的二进制,到底是哪一个”。一个包管理器安装的 CLI,由于守护进程自我更新、从副本运行、环境变量继承,执行时的身份可能已经变了。

他提出执行收据的五个字段:客户端 CLI 版本、守护进程二进制摘要、守护进程代际、更新器状态、继承环境代际。工程上很干净:每次客户端连到守护进程,守护进程回给客户端一份“我是谁、我从哪来、怎么被启的、之后可能怎么变”的收据。可见、可追踪,用户和审计方就能判断机器上跑的到底是什么。而不是现在这样,用户靠 ps、文件时间戳、网络日志猜它在干什么。这条评论可能是这个 issue 里最有长期价值的一条,因为它没有停在“默认开关”层面,而是试图定义“可审计的服务化客户端”应该长什么样。

一个默认自动更新、从副本运行、环境变量层面遗留第一会话配置的守护进程,实际上已经建立了一条新的信任链:用户信任 npm → npm 下载包 → 包安装 CLI → CLI 启动服务 → 服务自我更新 → 服务运行新版本。用户只在第一步和第二步做了显式同意,后面全是隐式的、堆叠的。执行收据就是把每一步的“隐式”改成“可见”。这个方向如果能被采纳,对整个 AI 编码工具领域都有意义:AI 编码工具越来越需要本地执行环境,如果这个环境不可审计,用户迟早把它当恶意软件。而恶意软件化是所有正经开发工具最承受不起的指控。一旦用户开始用恶意软件模型处理你的 CLI,后面所有 agent、自动执行、Remote Control 功能都会被默认拒绝。

这段算得有点碎,但这个 issue 里最值得留的就是这个方向性的东西。默认开关改回去是战术退却,provenance 边界才是战略纠正。


本质:信任策略的选择

自我更新、自我复制、绕开包管理器,这不是技术选择,是信任策略的选择。工具供应商选自我更新,运营上合理:用户不更新,供应商就得维护很多旧版本,推送功能慢。安全上也有理:自更新能更快推安全修复。桌面应用、浏览器、IDE 都有自动更新。CLI 为什么不一样?因为 CLI 的使用场景大量在 CI、脚本、远程服务器。可复现性和冻结版本是构建稳定性的基石。锁版本、lockfile、环境变量控制,一切为了“我今天跑的和昨天跑的,是同一个东西”。自更新守护进程把这一点破掉,整个可复现性模型动摇了。

tagorr 在 Windows 10 LTSC 上复现后,除了 daemon_auto_start = false,还建议“权限提升受阻时自动回退本地模式”。这说明在 Windows 上,这个守护进程可能尝试提升权限,或至少需要高权限跑某些功能。st396gt 在加固 Raspberry Pi 上提到文档要求宿主文件系统完整读写权限。这从侧面印证:它不是简单跑在用户态,在某些场景下需要更深宿主访问。安全敏感用户怎么可能不警觉。

报告者提到更新器可能 60 秒排空后强制终止活跃回合,#40969 无法禁用。这个独立问题和 daemon 默认开启放在一起看,就是一个典型产品决策:OpenAI 在逐步把 Codex 从“调用一次跑一次”的命令行,改造成“一启动就持续运行、供应商可定时重启、活跃会话让位于更新”的服务。过去这种事只发生在 Windows 更新或 OS 强制重启上,现在它出现在开发者每天手边的小工具上。


我的判断摆在这儿

这篇万字拆完了,收口。我的判断不是“Codex CLI 是恶意软件”,也不是“用户反应过激”。更具体:OpenAI 在没有明确文档、没有显式 opt-in 的情况下,把 Codex CLI 从无状态工具推向有常驻后台、自我更新、从副本运行的应用服务器。这个动作是产品意图的暴露,不是版本管理失误。这说明 OpenAI 已经把开发者桌面当作一个可以持续驻留、可远程更新、可逐步占据执行入口的平台。方向长期确定。0.157.0 之后有提交默认关闭,但方向和路线图没有被取消,只是被暂时按下去。

这个判断会在什么情况下被推翻?两个条件,同时成立我才改口。第一,OpenAI 在后续版本中彻底移除默认开启的 daemon,把相关功能全迁到显式启动的 codex app-server 命令或桌面应用里,CLI 默认路径不再创建任何自我更新后台进程。第二,OpenAI 发布完整的执行收据和 provenance 边界说明,用户能随时知道当前运行的后端版本、来源、更新器状态、继承环境,并能用一条命令真实地、完全地停止整个进程树,包括更新器。这两个条件没有同时成立之前,我不改口说“这只是一个 bug”。

最坏的结果不是用户骂几句。最坏的结果是用户在安全模型里把 Codex CLI 归类为不受信任进程,从所有关键环境隔离出去。这个心态一旦在团队里成型,OpenAI 后续在 agent 方向的所有本地能力都推不进去。信任建立需要一万次可验证行为,破坏只需要一次无提示自我更新。这个 issue 最好的结局不是关闭,是让 OpenAI 把信任模型重新设计成服务可以做,但必须可见、可控、可证明。很多开发者工具公司没能跨过这道门槛,不是因为技术差,是没意识到用户在第一次发现一个未知进程出现在 ps 里时,已经开始不再把工具当工具了。

深报
深报

万字行业深度报告:硬数据+出处,拆商业/技术/利益格局,落一句明确判断。

查看主页 →