I wanted Codex to ask ChatGPT for a second opinion.

I also wanted the whole round trip automated. I give Codex a question or a PDF; it sends the request, waits for ChatGPT to finish, brings the answer back, and remembers the conversation for later follow-ups. I do not copy prompts, move files, track URLs, or approve the same request twice.

The version that worked uses my normal signed-in ChatGPT page. Codex operates it through Chrome, while a small skill runs the workflow and remembers each conversation.

flowchart TB
    U["User"]
    C["Codex task"]
    S["Codex skill<br/>workflow + single-send guard"]
    M["Chrome DevTools MCP"]
    B["Normal signed-in Chrome"]
    G["ChatGPT conversation"]
    R["Conversation registry<br/>friendly name → full ChatGPT chat URL"]

    U -->|"One request"| C
    C --> S
    S -->|"Browser tool calls"| M
    M --> B
    B --> G
    G -->|"Visible response"| B
    B --> M
    M --> C
    C -->|"Answer + chat URL"| U
    C <-->|"Read or update"| R

Figure 1. One request starts the automated round trip; the answer and conversation URL return to Codex.

The working setup at a glance

The full automation uses five pieces:

  1. Normal Chrome, already signed in to ChatGPT. No cookies or credentials are copied into the project.
  2. Chrome remote debugging enabled. Chrome keeps the connection local and can ask before a new client attaches.
  3. Chrome DevTools MCP registered in Codex. It provides page inspection, form filling, clicking, and file upload operations.
  4. A Codex skill with a single-send guard. It attaches the file, fills the exact prompt, verifies the current page, and clicks Send once without asking for another approval.
  5. A small conversation registry outside Git. It stores a friendly name, such as smoke-test-review, beside the full URL of that ChatGPT conversation, such as https://chatgpt.com/c/<conversation-id>. In this URL, /c/ means the page is a saved conversation, and the value after it identifies that conversation. A later Codex task can use the friendly name to reopen the same chat.

From my side, the workflow is simply: ask once and get the answer back. Under the hood, Codex opens ChatGPT, attaches the PDF when needed, sends once, waits for the completed response, and saves the conversation URL. The rest of this article explains how those steps became automatic.

What happens after one request

Codex completes the browser workflow for me:

  1. It selects a visible chatgpt.com tab.
  2. It fills the ChatGPT composer and optionally attaches a file.
  3. It verifies the destination, attachment, and prompt.
  4. It presses Send once without asking me to approve the request again.
  5. It waits until ChatGPT finishes.
  6. It reads the newest visible assistant response.

The resulting conversation appears in my normal ChatGPT sidebar, and Codex saves its URL so another task can continue it automatically.

The same mechanism works without a PDF. A text-only request simply skips the attachment step.

Why the first browser approaches failed

My first attempts launched dedicated Playwright or extension-controlled browser sessions. Those sessions were technically capable of opening ChatGPT, but they did not reliably reuse my normal login.

There were two recurring problems.

A browser profile is an authentication boundary. Being signed in inside my everyday browser did not sign in a separate automation profile. Google also rejected login attempts from some automated browser contexts as insecure. Even when I completed a login manually, a later process could reconnect to a different profile or fail to preserve the authenticated state.

The file chooser was a separate boundary. Some browser integrations could click ChatGPT’s attachment menu but could not hand the selected local file back to the native chooser. Enabling file-URL access for an extension did not solve that bridge.

Repeated login prompts were a sign that the architecture was wrong for this use case. I did not need another browser profile. I needed controlled access to the Chrome profile that was already open and authenticated.

Attach to the Chrome session that is already signed in

Recent Chrome versions expose an explicit remote-debugging switch at:

chrome://inspect/#remote-debugging

I enabled Allow remote debugging for this browser instance. Chrome then opened a localhost-only debugging server and retained control over whether an external client could connect.

Codex uses the official Chrome DevTools MCP server as the adapter between model tool calls and Chrome DevTools operations. I registered it globally in Codex:

[mcp_servers.chrome-devtools]
command = "cmd"
args = [
  "/c",
  "npx",
  "-y",
  "chrome-devtools-mcp@latest",
  "--auto-connect",
  "--no-usage-statistics",
  "--no-performance-crux",
  "--no-category-performance",
  "--no-category-emulation",
  "--no-category-network",
  "--redact-network-headers"
]
startup_timeout_ms = 20_000

[mcp_servers.chrome-devtools.env]
PROGRAMFILES = 'C:\Program Files'
SystemRoot = 'C:\Windows'

The --auto-connect option asks Chrome to connect to the running browser instance whose remote debugging is enabled. Chrome may show a permission dialog when a client attaches. The remaining flags remove capabilities I did not need for this workflow and reduce unnecessary telemetry or network detail.

This is a global Codex MCP configuration, so new Codex tasks can discover the same browser tools after Codex reloads its configuration.

Uploading the PDF required one more workaround

The DevTools MCP server exposes an upload_file operation, but two details still mattered.

The tool limits which local paths it can read

The first upload attempt pointed directly at a PDF inside my project directory. The browser tool rejected it because that path was outside its configured workspace roots.

Instead of granting broad filesystem access, Codex copied only the requested PDF into the Windows temporary directory:

%LOCALAPPDATA%\Temp\chatgpt-upload-smoke-test.pdf

It verified both source and destination paths before copying, uploaded the temporary copy, and left the original unchanged. This keeps the permission expansion narrow and visible.

ChatGPT hides the real file input

ChatGPT’s Add files and more menu displayed an Upload from computer row, but sending a file to that visible menu item did not trigger a chooser that the MCP tool could control.

The page already contained the real input:

<input id="upload-files" type="file" multiple>

Its parent element was hidden. Codex temporarily exposed that existing input, added an accessible label, took a fresh accessibility snapshot, and called upload_file on the input’s semantic identifier. Immediately afterward, it restored the original hidden state.

The important boundary is that this workaround uses the page’s existing upload input. It does not invent a replacement control, call an undocumented ChatGPT API, or extract authentication data.

Verify first, then send once

A browser agent can click faster than a person can notice a mistake. The workflow therefore verifies the final page state immediately before a single Send click, but it does not ask me to approve the same request twice.

The skill follows this sequence:

Open destination chat

Attach PDF, if any

Fill exact prompt

Verify filename and prompt

Revalidate current page state

Click Send exactly once

An explicit request such as “send this PDF to ChatGPT” authorizes one outbound message. Codex does not pause after staging to request another confirmation. A prepare-only request still stops before Send.

If the click result is uncertain, Codex inspects the existing conversation instead of clicking again. This prevents duplicate messages when a page navigation or tool response times out after the first click succeeds.

How Codex knows when ChatGPT is finished

After Send, ChatGPT exposes a Stop answering button while generation is active. Codex checks the visible page state until that control disappears, then reads the latest completed assistant response from the conversation.

In the smoke test, the PDF contained four workflow steps. ChatGPT returned all four, Codex extracted the response, and the new conversation appeared in the normal sidebar. The final URL looked like this:

https://chatgpt.com/c/<conversation-id>

That URL is more useful than a temporary browser tab identifier. It remains valid after the MCP process disconnects and after the original Codex task ends.

Three kinds of session state

This setup became much clearer when I separated three meanings of “session.”

State What identifies it How long it should last
Chrome authentication The user’s normal Chrome profile Until ChatGPT signs out or the user clears it
ChatGPT conversation Its full https://chatgpt.com/c/<conversation-id> URL Until the conversation is deleted or access changes
Codex execution One task and its MCP process Only while that operation is running

The Codex process does not need to stay alive just to preserve a ChatGPT conversation. A later Codex task can reopen the canonical URL in the same signed-in Chrome profile.

Track conversations across Codex tasks

To avoid copying URLs manually, the skill stores a small registry at:

%USERPROFILE%\.codex\chatgpt-pdf-review\conversations.json

Each entry uses a readable alias:

{
  "version": 1,
  "conversations": {
    "smoke-test-review": {
      "url": "https://chatgpt.com/c/example",
      "title": "ChatGPT PDF upload smoke test",
      "last_pdf": "chatgpt-upload-smoke-test.pdf",
      "last_status": "completed",
      "updated_at": "2026-08-22T18:00:00Z"
    }
  }
}

The registry stores no cookies, tokens, prompt bodies, or response bodies. It only maps a human-readable name to the durable ChatGPT URL and a small amount of status metadata.

This lets a later Codex task understand requests such as:

Read the latest response from smoke-test-review.

or:

Attach report-v2.pdf to smoke-test-review and ask what changed.

For a text-only follow-up:

Continue smoke-test-review and ask whether step three can be improved.

Every explicit request to send authorizes one outbound message; no second approval is required.

Put the operational knowledge in a Codex skill

Configuring the MCP server gives Codex browser primitives such as listing pages, taking accessibility snapshots, filling controls, clicking buttons, evaluating small page scripts, and uploading files. It does not by itself explain which sequence is safe or how to recover from this specific file-input problem.

I packaged that operational knowledge as a Codex skill. The skill records the decisions that another Codex task should not have to rediscover:

  • prefer the already-signed-in Chrome route;
  • inspect only the intended ChatGPT tab;
  • reuse a tracked conversation URL when supplied;
  • use the temporary-directory copy only when path restrictions require it;
  • expose and restore ChatGPT’s existing hidden file input when necessary;
  • verify the final page state and send once without a second approval;
  • click once, wait, return the response, and save the canonical URL.

The skill is packaged inside a personal Codex plugin so it can be refreshed and loaded by new tasks. OpenAI’s Codex MCP documentation describes how MCP servers add external tools, while Codex skills provide reusable task instructions.

Security boundary

Remote debugging grants broad control over the connected Chrome profile. That is the main tradeoff in this design.

I use the following rules:

  • Chrome must ask before a new debugging client connects.
  • The agent selects and inspects only the intended chatgpt.com tab.
  • It never reads cookies, local storage, passwords, authorization headers, or unrelated tabs.
  • Login, MFA, CAPTCHA, account recovery, and verification remain manual.
  • An explicit send request authorizes one message; the agent does not ask for duplicate approval.
  • The MCP client disconnects when the operation finishes.
  • Conversation tracking stores URLs and status, not authentication material or message contents.

This does not make remote debugging low privilege. It makes the elevated access explicit, temporary, and narrowly used.

What this setup can and cannot do

It can:

  • send a normal text prompt to ChatGPT;
  • attach a PDF and ask ChatGPT to review it;
  • wait for and extract the completed response;
  • return a human-verifiable conversation URL;
  • reopen the same conversation from another Codex task;
  • send a later PDF or text follow-up to that same chat.

It cannot receive a push notification after every future ChatGPT change. A running Codex task can poll until the current answer finishes, or a later task can reopen the tracked URL and check again. It also cannot continue if Chrome is closed, remote debugging is disabled, the ChatGPT login expires, or the conversation is no longer accessible.

Conclusion

The successful design was simpler than the earlier browser-profile experiments:

Codex skill
+ Chrome DevTools MCP
+ normal signed-in Chrome
+ ChatGPT conversation URL
+ small local registry

Chrome preserves authentication. ChatGPT preserves the conversation. The registry gives future Codex tasks a stable name for that conversation. The skill preserves the safety and recovery rules.

The result is not a secret model-to-model channel. It is a visible, inspectable browser workflow that lets one agent ask another system for help without losing control of what gets sent.