macOS: `codex mcp login` can print OAuth URL without surfacing a browser window

Open 💬 1 comment Opened Aug 17, 2026 by xiaoshuo1988130

Summary

On macOS, codex mcp login <server> can print the OAuth authorization URL and wait for the loopback callback, but no browser window/tab becomes visible. The command does not print the documented fallback (Browser launch failed; please copy the URL above manually.), so the client appears to believe browser launch succeeded.

This creates a high-friction MCP OAuth flow: users think the command is stuck until they manually copy the one-time URL into a browser.

Environment

  • macOS 26.5.1 (arm64)
  • codex-cli 0.148.0-alpha.9 from the Codex desktop bundle

Reproduction

  1. Configure an OAuth-capable remote MCP server.
  2. Run codex mcp login <server> --scopes <scopes>.
  3. Observe that the terminal prints Authorize <server> by opening this URL in your browser: and waits.
  4. No browser window/tab is brought forward.
  5. Manually opening the printed URL completes OAuth and reaches the local callback successfully.

Expected

The authorization page should be visibly opened in the user's default browser, or the CLI should reliably report that opening it failed and make the manual next step explicit.

Notes

Current source calls webbrowser::open(auth_url) after printing the URL, but this observed path appears to return success without making a browser tab visible. No OAuth URL, code, state, or account data is included here.

View original on GitHub ↗

1 Comment

jdcodes1 · 11 days ago

Traced the call path, and the core problem is a message-contract gap: the CLI treats webbrowser::open(...) == Ok as "a browser tab is now visible", but on macOS that's not what Ok means — and can't be made to mean that with the API in use.

The call site. codex mcp login prints the URL, then only prints the fallback when open returns Err:

https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/rmcp-client/src/perform_oauth_login.rs#L604-L621

What Ok actually means on macOS. webbrowser 1.2.2 launches via the deprecated synchronous -[NSWorkspace openURLs:withApplicationAtURL:options:configuration:error:] — a deliberate choice, per the crate's own comment, because the modern completion-handler API "relies on the caller running a main run loop to actually dispatch the launch, which isn't the case for library consumers" (macos.rs @ v1.2.2). It returns Ok(()) as soon as LaunchServices hands back a running-app reference. When the default browser is already running, that means "the URL was delivered to the existing process" — whether a tab surfaces and whether the app is activated is then up to the browser and the OS. So Ok = "dispatch accepted", not "window visible", and a silent no-show (particularly on newer macOS releases, where deprecated LaunchServices entry points have become less reliable about activation) never trips the Err branch. The observed behavior — URL printed, command waits, no fallback line, manual open works — is exactly this gap.

Fix outline, cheapest first:

  1. Fix the message contract. Since Ok cannot certify visibility, print the manual next step unconditionally, e.g. after attempting launch: "If no browser window opened, copy the URL above into your browser." One line, removes the appears-stuck failure mode on every platform, and keeps the stronger wording for the true Err case.
  2. On macOS, prefer /usr/bin/open <url> (spawned as a child process). The open tool runs its own run loop and handles activation on current macOS releases; its exit status is also a slightly more honest signal than the deprecated in-process API.
  3. Upstream: if this reproduces deterministically on macOS 26 with the default browser already running vs. not running, that split is worth reporting to webbrowser-rs — the crate's sync-API choice is exactly the kind of thing that decays as Apple hollows out deprecated LaunchServices paths.

Worth noting the same over-trust exists at the other webbrowser::open call sites — TUI history links (tui/src/app/history_ui.rs#L212), onboarding auth (tui/src/onboarding/auth.rs#L1020), and login/src/server.rs#L187 (result discarded entirely) — so fix (1) is best applied as a shared helper rather than at one call site.