给 Codex 换张脸听起来是贴纸活,实际是 CDP 协议级的运行时注入

发布于 2026年07月16日 14:51 #OpenAI#Github 解读 原文链接

给 Codex 换张脸听起来是贴纸活,实际是 CDP 协议级的运行时注入 封面图
  • Codex Dream Skin 不改官方安装包和签名,通过 `--remote-debugging-port` 开 CDP 端口,用 `Runtime.evaluate` 在运行时注入 CSS 和 DOM,属于非侵入式 Electron 改造
  • 注入链路分三层,Shell 脚本负责定位 Codex 并复用其捆绑的 `cua_node/bin/node` 运行时,injector.mjs 通过 `127.0.0.1` 回环校验防止非本地连接
  • 皮肤持久化靠 MutationObserver 加 4 秒兜底重刷对抗 React 的 DOM diff 清理,明暗模式检测从 class 一路降级到采样背景色算亮度投票
  • 安全水位两个平台不一致,macos 版有 `validatedDebuggerUrl` 回环校验,Windows 版直接连 WebSocket 无任何 host 校验
  • 根目录无 LICENSE 文件(MIT 只在 macos 子目录),contributor 只有 1 人,顶部赞助商 Passion8 带 aff 推广码做 AI API 中转引流

大家好,我是若风。

一个 2026 年 7 月 15 日才创建的仓库,第二天就冲到 1500 Star、225 个 Fork。点开一看,叫 Codex Dream Skin,做的事情用一句话讲完,给 OpenAI Codex 桌面端换皮肤。

你大概会想,这能有多难。找个 CSS 文件改改颜色,或者像以前改 QQ 皮肤那样替换几张图,顶多算个折腾党的小玩具。我一开始也是这么想的。

真去读源码,发现事情没那么简单。这帮人没用「改文件」的老路子,而是用 Chrome DevTools Protocol 在运行时把 CSS 和 DOM 直接打进 Codex 的渲染进程。这是 Electron 安全研究者才会碰的那层东西,不是前端调调样式那么轻松。

值得拆一下。

它到底在做什么

一句话,Codex Dream Skin 是一个不改官方安装包、通过本机 CDP 注入给 Codex 桌面端做主题化的工具。

这里的关键不是「换肤」这三个字,是「不改安装包」。你想想看,传统给 Electron 应用换肤怎么做,解包 app.asar,改里面的 JS 和 CSS,重新打包,签名必然失效。每次 Codex 自动更新,你的修改全白费,还得再来一遍。更狠的是,改过的 asar 在企业环境下会被合规扫描盯上,说你有篡改嫌疑。

Codex Dream Skin 绕开了这一整条路。它的做法是,启动 Codex 时带上 --remote-debugging-port=9341,让 Codex 的 Chromium 内核开一个调试端口,然后自己写一个 CDP 客户端连上去,用 Runtime.evaluate 把主题代码注入进去。官方二进制一个字节没动,签名完好无损。

这个思路其实一点都不新鲜,做逆向、做自动化测试、做爬虫的人都熟。新鲜的是,有人把它拿来给一个 AI 编程工具做美化,还包装成了双击就能用的 .command 文件。

注入链路,从脚本到你的屏幕

整个注入链路分三层,从外到里依次是 Shell 脚本、Node 注入器、页面侧 payload。

最外层是 macos/scripts/start-dream-skin-macos.sh。它干三件事,找到 Codex 的 .app 包,用带 CDP 端口的方式启动它,然后拉起一个常驻的 injector 进程。这里有个细节我很喜欢,它不依赖系统装的 Node,而是复用 Codex 自己捆绑的 Node 运行时。看 common-macos.shrequire_macos_runtime 函数,路径写死指向 $CODEX_BUNDLE/Contents/Resources/cua_node/bin/node,还顺手做了两道 codesign 校验,一道验 Codex 主程序签名,一道验这个内置 Node 的签名,并且确认两者的 Team ID 都是 OpenAI 官方的 2DC432GLL2

这意味着什么,用户连 Node 都不用装,工具直接借 Codex 的壳跑。但也意味着,如果哪天 Codex 不再捆绑 Node,或者换了路径,这套东西当场报废。

中间层是 macos/scripts/injector.mjs,一个不到 400 行的 CDP 客户端。它先 fetch("http://127.0.0.1:9341/json/list") 拿到所有页面 target,过滤出 type === "page" 且 URL 以 app:// 开头的,然后对每个 target 建一条 WebSocket 连接,发 Runtime.evaluate 去探测这是不是 Codex 的界面。探测逻辑在 probeSession 函数里,它检查四个 DOM 标记,main.main-surfaceaside.app-shell-left-panel.composer-surface-chrome[role="main"],四个都中才算认准了 Codex,避免误伤同一个端口上的别的页面。

最内层是 macos/assets/renderer-inject.js,一段被打包成字符串塞进页面的 IIFE。这才是真正「画脸」的地方。它往 document.documentElement 上加 class、注入一个 <style id="codex-dream-skin-style">、再创建一个 <div id="codex-dream-skin-chrome"> 覆盖层,把品牌名、状态栏、装饰粒子全部塞进去。主题的图片被转成 Blob URL 挂到 --dream-skin-art 这个 CSS 变量上,背景图就靠它显示。

三层之间靠 theme.json 传递配色。这个 JSON 有个 schemaVersion: 1 字段,loadTheme 函数会严格校验,版本不对直接报错,图片大小卡在 16MB 以内,格式只收 png/jpg/jpeg/webp,文件名还必须纯 basename 不含路径。这些校验看着啰嗦,其实是为了防止主题包里塞个 ../../ 的路径穿越。

皮肤会掉,他们怎么让它粘住

Electron 应用是 SPA,路由切换的时候 React 会把整个视图树重新渲染。你刚注入的 DOM 节点,用户点一下「设置」再回来,可能就被 React 的 diff 逻辑给清掉了。皮肤就「掉」了。

Codex Dream Skin 的解法在 renderer-inject.js 里,简单粗暴但有效,两套机制并行。

第一套是 MutationObserver,挂在 document.documentElement 上,监听 childListsubtree、还有 class/data-theme/data-appearance 等属性变化。一旦 DOM 有任何风吹草动,就触发 scheduleEnsure,用一个 180ms 的 debounce 重新跑一遍 ensure 函数,把缺失的 class 和节点补回来。

第二套是兜底的 setInterval(ensure, 4000),每 4 秒无条件重刷一次。哪怕 Observer 漏了,4 秒后也会自愈。

你可以说这不优雅,但你想想看,在人家 React 的地盘上搞 DOM 劫持,不用这种「暴力重刷」还真压不住。这其实是所有浏览器插件、油猴脚本做深度页面改造时都会遇到的同一个问题,解法也都大同小异。Codex Dream Skin 只是把这套工业化了,加上 verify 模式做自动验收。

明暗模式,一个比想象中难的问题

主题要适配 Codex 的明暗模式,听起来简单,prefers-color-scheme 媒体查询嘛。但 Codex 是应用级主题切换,不一定走系统设置,用户在设置面板里点的那个单选框,改的是 React state,不一定反映到 prefers-color-scheme

detectShellMode 函数展示了他们的排查路径,层层降级。先看 <html><body> 的 class 里有没有 dark/light,没有就看 data-themedata-appearancedata-color-mode 这些 data 属性,再看设置面板里那个 input[name="appearance-theme"]:checked 单选框,连 aria-label 里的「暗」「浅」「系统」都解析。这些全失败,最后使出一招「采样投票」,取 bodymain.main-surfaceaside.app-shell-left-panel 三个元素的背景色,算亮度 luminance,大于 0.55 投亮色票,小于 0.25 投暗色票,谁的票多听谁的。

这招很实际。当所有「语义化」的标记都被 React 框架藏在运行时里、抓不到的时候,直接量屏幕实际呈现的颜色,反而成了最可靠的信号。这种「从结果反推状态」的思路,做逆向工程的人应该很熟悉。

安全设计,做了一半的好活

现在说批判的部分,这部分也是我把源码读细之后才看清楚的。

先说做对的地方。CDP 是个危险东西,谁连上 9341 端口,谁就能用 Runtime.evaluate 在 Codex 里执行任意 JS,等于完全控制渲染进程,你跟 AI 的对话内容、token、剪贴板,全能读到。Codex Dream Skin 的 macos 版 injector.mjs 里有个 validatedDebuggerUrl 函数,它强制校验 WebSocket 的 URL 必须是 ws: 协议、host 必须在 LOOPBACK_HOSTS(127.0.0.1localhost[::1])集合里、端口必须等于启动时指定的端口。任何一项不符直接 throw。这是对的,防止 DNS 重绑定之类的攻击把流量引到外部。

但去读 Windows 版的 windows/scripts/injector.mjs,同样是 CdpSession 类,构造函数里直接 new WebSocket(target.webSocketDebuggerUrl),没有任何 host 校验。macos 版的 parseArgs 还对 timeoutMs 做了 250 到 120000 的上下限校验,Windows 版连这个都没有。两个平台的安全水位是不一致的,Windows 用户拿到的保护明显更弱。这不是 README 会告诉你的事。

更深一层的问题是 README 自己也只轻描淡写了一句「主题运行期间勿跑来路不明的本机程序」。话没错,但责任完全甩给用户了。实际场景是,Codex 开发者本机往往跑着一堆东西,各种 MCP server、Agent 框架、第三方 CLI,只要其中任何一个被供应链污染,它就能顺手连上 9341,把 Codex 当后门用。这个攻击面,Codex Dream Skin 没有做任何提示,也没提供「用完即关端口」的自动机制,injector 是常驻的 launchd 服务。

协议、贡献者、赞助,三笔糊涂账

除了安全,还有几个点我觉得读者有权知道。

第一,LICENSE 的事。README 的「许可与声明」一节写着「见 macos/LICENSE(MIT)」,我顺着去查仓库根目录的 license 接口,GitHub 返回 404。也就是说,这个仓库根目录没有 LICENSE 文件,MIT 许可只存在于 macos/ 子目录里。从开源协议的角度,根目录无 LICENSE 意味着默认「保留所有权利」,你 Fork 之后的使用权其实是个灰色地带。225 个 Fork 的人,大概率没注意到这个细节。Windows 目录更是连 LICENSE 都没有,法律状态完全空白。

第二,贡献者集中度。API 返回 contributors 数量是 1。这个项目目前是单人维护,bus factor 等于 1。考虑到它才创建一天,这个数字本身不算致命,但一个 1500 Star 的项目如果长期只有一个人推,可持续性是要打问号的。Issue 区已经有人提「预设一些资源吧」「支持 GitHub 导入」,维护压力马上就来了。

第三,也是我最想说的,赞助商 Passion8。打开 README,顶部最大幅的图片不是效果预览,是 Passion8 的广告,带 aff=TuPe 推广码,文案写着「满血 AI 中转,官方模型直连,无降智无套壳,一行配置接入 Codex / Claude Code / Grok」。

一个给 Codex 换肤的项目,顶部挂的是 AI API 中转站的推广。docs/PROJECT.md 第 7 节更是白纸黑字记录了赞助关系和商业调性。README 虽然反复强调「换肤与 API 配置互相独立,本项目不会自动改写你的模型供应商设置」,这话是真的,源码里确实没有写入 Base URL 的逻辑。但你想想这个流量漏斗,用户被换肤效果吸引来 → 看到顶部中转站广告 → 点推广链接注册 → 作者拿返佣。换肤是饵,中转才是生意。

我不觉得这有什么丢人的,开源项目要吃饭,赞助很正常。但作为读者,你有权知道这个项目的动机结构,它不是一个纯靠爱发电的主题工具,它是一个带商业引流的流量产品。带着这个认知去用,判断会清醒很多。

一个可迁移的判断,CDP 注入作为非侵入式改造范式

拆完这个项目,我觉得最值得带走的不是它的 CSS 写得多好看,是一个可命名的范式

我把这套做法叫**「非侵入式 Electron 改造」**。核心就三条,第一,不碰二进制和签名,用 --remote-debugging-port 开调试端口。第二,复用宿主捆绑的运行时,自己不带依赖。第三,运行时注入 + Observer 持久化,对抗 SPA 的 diff 清理。

这个范式的价值在于,它把「改造一个我不拥有源码的 Electron 应用」这件事,从「改 asar 这种破坏性操作」降级成了「运行时 hook 这种可逆操作」。升级不失效,合规能过,一键还原。

适用边界也要说清楚。它只适合你自己机器上、你自己信任的应用,因为 CDP 端口一开,攻击面就摆在那。它不适合拿去做企业级分发,你没法要求每个用户都懂「主题运行期间勿跑来路不明的程序」。它也不适合做需要持久生效的改造,因为 injector 必须常驻,关了就没了,这是运行时注入天生的代价。

所以如果你问我什么场景该用 Codex Dream Skin,我的建议很简单。你是 Codex 重度用户,本机环境相对干净,想给自己的工作台加点个人氛围,那它确实是目前最优雅的换肤方案,比改 asar 强一个量级。但如果你是在企业受限环境里跑 Codex,或者本机跑了一堆来路不明的 Agent 工具,那这个 CDP 端口就是个定时炸弹,换张脸不值得冒这个险。

开源世界从来不缺好玩的点子,缺的是把点子的边界讲清楚的人。Codex Dream Skin 的技术活做得漂亮,商业底色也不必回避,把这些一起摊开看,你才算真的读懂了它。

评论互动

© 2026 王若风的技术博客 · Powered by Astro