---
title: 如何启动多个 Codex 浏览器会话
canonical: "https://xiaofeng.dev/en/writing/codex-browser-sessions/"
pubDate: 2026-07-18
author: 唐小锋 Xiaofeng TANG
description: 为了同时跑多个 Codex session，我写了一个浏览器启动器。真正动手前，我先追了三个问题：Chrome DevTools MCP 是怎么注入 Codex 的，运行中能不能改；Chrome 的 p
tags: [Codex, AI Coding]
---

# 如何启动多个 Codex 浏览器会话

为了同时跑多个 Codex session，我写了一个浏览器启动器。真正动手前，我先追了三个问题：Chrome DevTools MCP 是怎么注入 Codex 的，运行中能不能改；Chrome 的 profile 在哪里指定；CDP 端口又是怎么完成 auto connect 的。这三个问题搞清楚，方案也就差不多出来了。

我原本以为这只是多开两个 Chrome 窗口。实际跑起来，一个 session 会连到另一个 session 的浏览器；有时关掉一个 Codex，另一个 Chrome 也跟着没了。窗口开了两个，底下的连接却还是混的。

最后的实现很简单：每个 Codex session 使用自己的 CDP 端口和 Chrome profile，启动时再把对应的 MCP 配置传给 Codex。这三项必须成套，不能串。脚本还支持指定或复用 profile，并保留了 Codex 的 `resume` 能力。

## 先把连接关系画清楚

CDP（Chrome DevTools Protocol）是 Chrome 的调试和自动化接口。端口就是它的入口。`chrome-devtools-mcp` 则是 Codex 与 CDP 之间的 bridge，它把“打开页面、执行脚本、读取 DOM”这类工具调用转换成 CDP 请求。

两条会话的连接关系应该是这样的：

Codex session A ──▶ MCP bridge A ──▶ Chrome A

                     CDP :49101       profile: work-a

Codex session B ──▶ MCP bridge B ──▶ Chrome B

                     CDP :49102       profile: work-b

这里要隔离的是整条链路。单独多开窗口没有用，MCP、端口或 profile 只要有一处串了，两个 session 还是会互相影响。

## 先补一个前提：autoConnect 来自 Chrome 官方 MCP

`autoConnect` 不是这个启动器实现的，它是 Chrome 官方 `chrome-devtools-mcp` 提供的连接方式。

按照 Chrome 官方文档，Chrome 144 及以上版本可以在 `chrome://inspect/#remote-debugging` 开启远程调试，然后给 MCP server 加上 `--autoConnect`：

{

  "mcpServers"

:

{

    "chrome-devtools"

:

{

      "command"

:

"npx"

,

      "args"

:

[

"chrome-devtools-mcp@latest"

,

"--autoConnect"

]

    }

  }

}

这种方式适合让 Agent 接管已经打开的本机 Chrome，也能直接使用现有的登录状态。不过它有一个与多会话有关的前提：如果 Chrome 同时有多个活跃 profile，MCP 会连接 Chrome 判定的默认 profile，并访问这个 profile 下的所有窗口。

这正是我写启动器时要处理的问题。我要的不是“找到一个正在运行的 Chrome”，而是让每个 Codex session 都连到事先为它分配的 profile。于是脚本没有使用 `--autoConnect`，而是启动独立 Chrome，再通过 `--browserUrl` 指定连接目标。

## MCP 到底注入到哪里

我一开始对“注入”这个词也有点误解。`chrome-devtools-mcp` 并不会进入 Chrome，它是作为 MCP server 注册到 Codex，然后通过 CDP 去控制 Chrome。

Codex 可以在启动时用 `-c` 接收这份配置：

codex \

  -c

'mcp_servers.chrome-devtools.command="node"'

 \

  -c

"mcp_servers.chrome-devtools.args=[..., \"--browserUrl\",\"

$devtools_url

\"]"

第一行告诉 Codex 怎么启动 MCP server。第二行里的 `--browserUrl` 告诉 `chrome-devtools-mcp`，它该连接哪个 Chrome。

这也决定了配置应该在什么时候传入。要跑多个 session，就在启动 Codex 时为每个进程传入各自的 `browserUrl`。这份 MCP 配置只作用于当前 Codex 进程。

## profile 不是一个浏览器窗口

第二个问题是 profile。

Chrome 的 profile 本质上是一个可写目录。Cookie、缓存、扩展设置、Service Worker 和登录状态都在里面，Chrome 还会在目录中放锁，防止多个进程同时写入。

启动 Chrome 时，可以用 `--user-data-dir` 指定这个目录：

chrome \

  --remote-debugging-port=

"

$port

"

 \

  --user-data-dir=

"

$profile_dir

"

如果两个 Chrome 进程共用同一个目录，第二个进程通常会因为锁冲突而启动失败。即便勉强共用了，登录状态和缓存也会混在一起，问题反而更难查。

启动器会给每个 session 分配独立目录。直接运行 `./codex-browser` 时，它会用自动选择的端口生成 session 名，比如 `codex-49101`。这适合临时开一个全新的会话。

如果我想在下次启动时继续使用原来的登录状态，可以固定 `CODEX_BROWSER_SESSION`：

CODEX_BROWSER_SESSION=work-a ./codex-browser

`work-a` 会对应一个固定的 profile 目录。当前会话退出后，再用同一个 session 名启动，就能复用里面的 Cookie、登录状态和站点数据。`CODEX_BROWSER_PROFILE_ROOT` 还可以修改这些 profile 的存放位置，不过一般不需要设置。

复用发生在前后两次启动之间。两个并发会话仍然不能使用相同的 `CODEX_BROWSER_SESSION`，否则它们会争用同一个目录。

## 启动器如何连接到正确的 CDP 端口

第三个问题是 CDP 端口。

Chrome 通过 `--remote-debugging-port` 暴露 CDP，例如：

http://127.0.0.1:49101

默认情况下，脚本会向操作系统申请一个当前可用的本机端口。Chrome 启动后，脚本持续请求 `/json/version`，直到确认 CDP 已经可以访问。日常使用不需要关心具体端口。

`CODEX_CHROME_PORT` 可以把端口固定下来，但这是高级用法。只有外部工具需要连接一个已知的 CDP 地址，或者需要针对固定端口排错时，我才会设置它。

Chrome 就绪后，启动器已经知道刚刚选中的 `$devtools_url`，会把它作为 `--browserUrl` 传给 `chrome-devtools-mcp`。这一步使用的是官方 MCP 的指定地址连接能力，并不是 `--autoConnect`。

启动过程可以拆成三步：

- 1. 默认自动选择端口，必要时才手动指定；
- 2. 启动 Chrome，等待 CDP 就绪；
- 3. 把同一个 URL 传进当前 session 的 MCP 配置。

如果要复用一个已经启动的 Chrome，可以传 `--browser-url` 或 `CODEX_CHROME_DEVTOOLS_URL`。脚本会先检查对应的 `/json/version`，确认它确实是可用的 CDP 端点，然后再启动 Codex。

端口可以自动选，选完以后必须明确绑定。

## 三个参数要跟着同一个 session 走

回头看这三个问题，它们正好对应一条完整的连接：

Codex session ↔ MCP browserUrl ↔ CDP port ↔ Chrome profile

MCP 配置决定 bridge 连接哪个 `browserUrl`。CDP 端口指向具体的 Chrome 进程。profile 再决定这个 Chrome 使用哪份浏览器状态。

启动器做的事情，就是在启动时把这几项绑到一起。这样每个 Codex session 都有明确的浏览器归属，MCP 也不用临时猜测该连接哪个 Chrome。

## 谁启动 Chrome，谁负责关

连接隔离以后，还有一个容易被忽略的问题：退出时该关掉哪个 Chrome？

`codex-browser` 启动 Chrome 后会记录 PID，并注册 `trap ... EXIT INT TERM`。Codex 退出时，它只关闭自己创建的 Chrome。

如果我传入的是已有 `--browser-url`，启动器只负责连接，不会关闭那个 Chrome。因为它不是这次启动创建的进程。

端口或 profile 撞车时也应该直接失败。如果脚本偷偷复用一个已经存在的端口，当前 Codex 很可能连到别人的浏览器。这里报错反而更安全，也更容易排查。

## 我现在怎么启动

最简单的用法就是直接运行：

./codex-browser

要开多个会话，就在不同终端里分别运行同一条命令。每次启动都会自动选择端口，并创建独立的 profile。

脚本也支持恢复 Codex 会话：

./codex-browser r

./codex-browser last

`r` 会转换成 `codex resume`，可以选择要恢复的会话；`last` 会转换成 `codex resume --last`，直接继续最近一次会话。

如果某个会话以后还要继续用，给它一个固定的 session 名即可。端口仍然交给脚本自动分配：

CODEX_BROWSER_SESSION=work-a ./codex-browser

下一次使用同一个名字，就会打开 `work-a` 对应的 profile。它也可以和 `resume` 一起使用，同时恢复 Codex 会话和浏览器状态：

CODEX_BROWSER_SESSION=work-a ./codex-browser r

只有确实需要固定 CDP 地址时，才同时指定端口：

CODEX_CHROME_PORT=49101 CODEX_BROWSER_SESSION=work-a ./codex-browser

这次真正解决的并不是 Chrome 能不能多开。Chrome 本来就能多开。麻烦的是如何让每个 Codex 始终找到属于自己的那个实例，并且在退出时只清理自己的资源。把 MCP 配置、CDP 端口和 profile 放进同一个 session 生命周期里，多个浏览器会话才能稳定地同时运行。

我已经把这个启动器开发好了，代码和使用说明放在 GitHub：agent-browser-launchers。
