Codex 记忆功能的暗门:本地模型的聊天,正在被悄悄发给 OpenAI

设想这样一个场景:你格外在意隐私,于是把 Codex 接到了一个跑在本地的模型上,所有对话都不出你的机器。你还顺手在配置里关掉了 analytics,把 OpenTelemetry 的 exporter 全部设成 none。你觉得自己已经把能关的都关了。可突然有一天,你收到了 OpenAI 发来的账户警告,说你「滥用」。你翻遍了所有通过 OpenAI 进行的对话,找不到任何能解释这条警告的内容。那 OpenAI 到底看到了什么?
2026 年 8 月 30 日,一位安全研究者在 OpenAI 开源的 codex 仓库提交了一份数千字的 issue(编号 #41711),用一个精心设计的受控实验给出了答案:问题出在 Codex 的记忆功能(Memories)上。它在挑选「哪些历史对话值得生成记忆」时,并没有把「这段对话来自哪个模型供应商」纳入筛选条件。结果,你在本地模型下的聊天内容,可能被随后一次由 OpenAI 支撑的会话静默打包,发送到 chatgpt.com 的服务器。
一次受控实验,把这条通道钉死了#
研究者没有停留在「感觉这里有隐患」的层面,而是做了一次干净的复现。他用的是 Windows 版的 Codex 二进制文件 0.150.0-alpha.12.2,在隔离的环境里开启 Memories,同时显式关掉 analytics 和所有 OpenTelemetry exporter。他构造了一段带有唯一「金丝雀」标记的合成对话,并把它的供应商标签设为非 OpenAI 的本地来源。
随后,他触发了一次由 OpenAI 账户支撑的 Codex 会话。抓包结果非常直接:Codex 向 chatgpt.com/backend-api/codex/responses 发出了一帧 38,095 字节的 WebSocket 请求,其中 request_kind 明确写着 memory。请求里包含了五个被保留的源对话条目。服务端返回的记忆文本里,那些唯一金丝雀被原样复现了多次。这说明那一段「本地来源」的对话内容,确实被送到了 OpenAI,并被模型处理了。
这段请求的细节也很能说明问题:31,000 字符的记忆指令,外加一个 3,817 字节的包装层,里面裹着 3,092 字节的源对话内容。服务端用模型 gpt-5.6-luna 处理,消耗了 7,337 个输入 token。换句话说,这不是一次「试探性连接」,而是一次真正的内容级处理。
源码里的三段路,解释了这一切为什么会发生#
如果只看现象,你可能会以为这是偶发的 bug。但翻开源码,这条路径是结构性的。记忆系统的工作可以拆成三个阶段,每一段都藏着同一个设计漏洞。
第一阶段,挑选候选对话。记忆任务会调用一个 rollout 选择器,但调用时把 model_providers 传成了 None。也就是说,挑选「哪些旧对话值得总结」时,它只看记忆模式、对话年龄、空闲状态、来源类型这些条件,唯独不看这段对话当初是在哪个供应商下产生的。
第二阶段,决定用哪个模型来生成记忆。这一步用的是「当前触发会话」的活跃供应商。换句话说,你此刻用 OpenAI 账户开着 Codex,那记忆任务就会用 OpenAI 的模型来处理。
第三阶段,序列化并发送。选中的那段旧对话被加载出来,过滤、序列化之后,塞进一条新的用户消息,再通过记忆客户端发出去。
把三段连起来看,逻辑就清楚了:一段本地模型的旧对话,因为「年龄够了、空闲够了」,被记忆任务挑中;然后这个任务用的是你当前 OpenAI 会话的供应商;最后这段本地对话被序列化,发给了 OpenAI。三个看似各司其职的环节,拼出了一个跨供应商的数据通道。
到底会泄露多少内容?#
答案是,对一次普通的短对话来说,几乎是全部。被保留的条目类型非常宽泛:普通的用户和助手消息、agent 消息、本地 shell 调用、函数调用及其输出、工具搜索、自定义工具、网页搜索,甚至 agent 之间的通信。被排除的只有开发消息、推理记录、压缩记录和一部分记账信息。
对于较长的输入,系统会在 70% 的模型上下文窗口处做「中间截断」,保留开头和结尾,兜底值是 15 万 token。但对于一条足够短的普通对话,这一套下来,发送出去的基本就是你整段对话,外加工具的参数、结果、错误信息、环境上下文,还有本地的文件路径。
那个「脱敏」,其实保护不了你#
有人可能会说,序列化之前不是有个 redact_secrets 吗?确实有,但它的覆盖面窄得让人失望。它只识别几类非常明确的密钥形态:sk- 开头的 key、AWS 的 AKIA 标识、足够长的 bearer token,以及赋值给 api_key、token、secret、password 这类名字的值。
研究者做了一次对照测试:这个过滤器能干净地删掉传统的假 OpenAI key、bearer token 和名为 api_key 的赋值;但对带下划线的非典型 key、cookie 形态的值、邮箱地址、电话号码、私有密钥的头部、私有的文字内容、Windows 路径,它统统放行。简单说,这是一个「密钥模式过滤器」,不是「隐私脱敏器」。它防的是密钥泄露,不是内容泄露。
关掉遥测,也拦不住这条路#
一个很容易让人误判的细节是:这次受控实验是在 analytics 关闭、所有 OpenTelemetry exporter 设为 none 的前提下完成的,但请求照样发出去了。原因是这两个开关管的是另一套子系统。真正能掐断这条通道的全局开关只有一个,就是功能开关 [features] memories = false。目前并不存在「保留 Memories、但禁止跨供应商发送」的单独选项。你要么全关,要么接受这条通道存在。
更刺眼的,是「账号治理」这条线#
技术层面的跨供应商发送,已经被源码、受控实验和独立复现三方坐实。但这份 issue 里还有一条更重的指控。研究者称,自己此前收到过 OpenAI 的「网络滥用」账户警告,而他在 OpenAI 侧的对话里找不到任何能解释这条警告的内容,相关活动只存在于他故意路由到本地私有模型的那些聊天里。基于这个事实和抓到的记忆通道,他给出了一句很重的判断:OpenAI 可能在用「它本无权收到」的内容做账号层面的治理。
这里需要把两件事分开。技术事实是硬的:跨供应商发送确实存在。而「这些内容是否被用于审核、封禁、警告」这一层,属于研究者的强证据支撑的推断,但正如另一位评论者所指出的,它还需要不同的证据来确认。评论区的两位开发者独立复现了源码层面的跨供应商路径,确认它在当前 main 分支上依然结构性存在,但也谨慎地注明:关于审核用途、合法性、服务端留存和受影响人数的说法,无法仅凭源码检查来证明。
我的看法是,无论「用于审核」这条最后是否成立,技术层面的问题本身就足够严重。一个用户明确选择了本地供应商,就意味着他希望这段内容留在本地。系统在没有明确告知的情况下,把它送到了另一个供应商的服务器上,这本身就已经越过了那条边界。至于是有意为之还是疏忽,改变不了「内容确实跨过了边界」这个事实。
如果不想被这条通道命中,能做什么?#
短期最直接的做法,就是把 Memories 关掉:在配置里把 [features] memories 设为 false。这同时也意味着你会失去记忆功能带来的便利。另一个更现实的做法是,不要在「混合供应商」的场景下使用 Memories,也就是别在本地模型和 OpenAI 之间来回切换,同时开着记忆。至于长远,这起事件最值得期待的,是 OpenAI 能否给出一个明确的公开回应:这个跨供应商发送到底是设计如此还是 bug,哪些版本受影响,受影响用户如何识别并删除已经生成的远程记忆。
一点延伸的思考#
这件事背后藏着一个更普遍的问题:当一个 AI agent 同时对接多个模型供应商时,「数据留在哪个边界之内」这个假设,正在变得越来越不可靠。用户天然会认为,我选了本地模型,我的内容就该留在本地;我关了遥测,就没有隐藏的上传。但这些假设都建立在「系统会按你的预期隔离数据」的前提上。而这次的记忆功能证明,隔离并不总是默认的。随着 agent 变得越来越自主、后台任务越来越多,类似「哪个子系统在替你往外发数据」的问题,会一次又一次地冒出来。也许我们需要的,不是更多的开关,而是让每一个「数据即将离开本机」的动作,都变成一次可见、可审计、需要明确授权的决定。
参考来源:OpenAI codex 仓库 issue #41711「PRIVATE CHAT THREAD EXFILTRATION: Memories exfiltrates local-provider chat content to OpenAI without notice」及该 issue 的评论区(2026 年 8 月 30 日)。