Skip to content

fix: route OAuth/SSO URLs to system browser instead of embedded WebView - #1078

Open
Milofax wants to merge 2 commits into
manaflow-ai:mainfrom
Milofax:fix/oauth-external-browser
Open

Milofax wants to merge 2 commits into
manaflow-ai:mainfrom
Milofax:fix/oauth-external-browser

Conversation

@Milofax

@Milofax Milofax commented Mar 8, 2026 •

Copy link
Copy Markdown

Summary

  • OAuth/SSO URLs (e.g., claude /login, GitHub OAuth, Google sign-in) now open in the system browser instead of cmux's embedded WebView
  • Identity providers like Google explicitly block OAuth in embedded WebViews, causing the login flow to hang at "Einen Moment bitte..." / "One moment please..."
  • Two-level interception: terminal URL resolution + WKWebView navigation delegate catches redirects

Problem

When a terminal process (e.g., Claude Code /login) calls open https://claude.ai/oauth/authorize?..., cmux routes it to the embedded browser. The OAuth flow redirects to accounts.google.com, which detects the embedded WebView and blocks the consent flow — the user sees a perpetual loading spinner and can never complete login.

Fix

resolveTerminalOpenURLTarget — Detect OAuth URLs before routing to .embeddedBrowser:

  • /oauth, /oauth2, /o/oauth2 path patterns
  • accounts.google.com/signin/*
  • login.microsoftonline.com, login.live.com
  • github.com/login/oauth
  • appleid.apple.com/auth

decidePolicyFor navigationAction — Catch OAuth redirects from within the embedded browser (e.g., a website's "Sign in with Google" button triggers a redirect to Google's consent screen).

Test plan

  • Run claude /login in cmux terminal — should open system browser, not embedded
  • Click a "Sign in with Google" link on a website in the embedded browser — should bounce to system browser
  • Non-OAuth URLs still open in the embedded browser as before
  • OAuth callback URLs (containing /oauth) correctly route to system browser

🤖 Generated with Claude Code


Summary by cubic

Route all OAuth/SSO URLs to the system browser instead of the embedded WebView to prevent blocked consent screens and fix hanging logins (Google, Microsoft, GitHub, Apple). Applies to terminal-initiated flows like claude /login and in-app redirects.

  • Bug Fixes
    • Detect and open OAuth/SSO URLs externally during terminal URL resolution and from WKWebView main-frame navigations; non-OAuth links stay in the embedded browser.
    • Safer detection: path-segment match for /oauth and /oauth2, http/https scheme guard, boundary-aware Microsoft hosts, handle bare-host OAuth URLs, and log failures when opening externally. Patterns covered include accounts.google.com/signin, login.microsoftonline.com, login.live.com, github.com/login/oauth, appleid.apple.com/auth.

Written for commit 91b6999. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Detects OAuth/SSO authentication URLs and ensures they open in the system browser instead of embedded windows.
    • Cancels embedded navigation for these flows and logs the action for diagnostics.
    • Improves compatibility with common identity providers (Google, Microsoft, GitHub, Apple) while leaving non-auth links unchanged.

Identity providers like Google block OAuth flows in embedded WebViews,
causing login to hang at the consent screen. This affects CLI tools
(e.g., `claude /login`) that open OAuth authorize URLs via the terminal.

Two-level fix:
- resolveTerminalOpenURLTarget: detect OAuth URLs early and route to
  .external before they reach the embedded browser
- WKWebView navigation delegate: catch OAuth redirects (e.g., to
  accounts.google.com) from within the embedded browser and hand them
  off to the system browser

Detected patterns: /oauth, /oauth2, accounts.google.com/signin,
login.microsoftonline.com, github.com/login/oauth, appleid.apple.com/auth

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Mar 8, 2026

Copy link
Copy Markdown

@Milofax is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 8, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds OAuth/SSO detection and interception so OAuth-related HTTP(S) navigations are routed to the system browser. Changes appear in GhosttyTerminalView (early-return for detected OAuth URLs) and BrowserPanel (new browserIsOAuthFlowURL + navigation delegate interception).

Changes

Cohort / File(s) Summary
Browser OAuth detection & handling
Sources/Panels/BrowserPanel.swift
Adds func browserIsOAuthFlowURL(_:) to identify common OAuth/SSO endpoints and integrates checks into WK navigation delegate paths to cancel embedded navigation and open those URLs in the system browser (with debug logs and failure handling).
Terminal view OAuth short-circuit
Sources/GhosttyTerminalView.swift
Adds early-return branch in URL resolution to detect OAuth flow URLs, log a DEBUG message, and return .external(url) to bypass host normalization and embedded-browser logic.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Terminal as GhosttyTerminalView
    participant Panel as BrowserPanel
    participant Nav as NavDelegate
    participant OSB as SystemBrowser

    User->>Terminal: Request navigation to URL
    Terminal->>Terminal: parse URL, browserIsOAuthFlowURL?
    alt OAuth flow URL
        Terminal->>OSB: return .external(url) / open externally
    else Not OAuth
        Terminal->>Panel: continue embedded navigation
        Panel->>Nav: navigationAction / decidePolicyFor
        Nav->>Nav: browserIsOAuthFlowURL?
        alt OAuth flow detected
            Nav->>OSB: cancel embedded, open externally
        else render embedded
            Nav->>Panel: allow navigation
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 I sniff the URLs with twitching nose,
If OAuth dances, out the window it goes.
I nudge the browser, skip the embed,
Hop—let the system handle that thread. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: routing OAuth/SSO URLs to the system browser instead of the embedded WebView.
Description check ✅ Passed The description includes a comprehensive summary, detailed problem explanation, and fix rationale, but lacks explicit testing evidence and is missing the Demo Video section and Review Trigger checklist items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes OAuth/SSO login flows by routing URLs from identity providers (Google, Microsoft, GitHub, Apple) to the system browser instead of cmux's embedded WKWebView, which correctly addresses the Google policy that blocks OAuth in embedded browsers. The fix is applied at two interception points: in resolveTerminalOpenURLTarget (for terminal-initiated open calls) and in BrowserNavigationDelegate.decidePolicyFor (for in-WebView redirects such as "Sign in with Google" buttons).

The overall approach is sound and well-motivated, but there are three correctness issues in the URL matching logic that should be addressed:

  1. Domain suffix matching bug: hasSuffix("login.microsoftonline.com") incorrectly matches domains like evillogin.microsoftonline.com due to missing dot anchoring.

  2. Overly broad path matching: path.contains("/oauth") matches non-OAuth paths like /oauth-settings or /oauth-documentation. The subsequent conditions contains("/oauth2") and contains("/o/oauth2") are fully redundant.

  3. Missing error logging: The OAuth block in the navigation delegate doesn't log an error when NSWorkspace.shared.open returns false, unlike the adjacent deeplink-handling block, making debugging harder.

All three should be fixed before shipping to ensure correct domain validation and consistent error diagnostics.

Confidence Score: 3/5

  • Safe to merge with fixes — the core approach is sound but three correctness issues in URL matching logic should be addressed before shipping.
  • The overall design is correct and well-motivated. However, the hasSuffix hostname bug can cause incorrect domain matching (a well-known Swift string pitfall), the broad path.contains("/oauth") check will silently redirect users to the system browser for unrelated pages whose paths happen to contain the substring "/oauth", and the missing error log hurts debugging. None are critical security vulnerabilities, but all three represent correctness bugs that could produce unexpected user-visible behavior.
  • Sources/Panels/BrowserPanel.swift — specifically the browserIsOAuthFlowURL function (lines 533–563) and the navigation delegate OAuth interception block (lines 3997–4005)

Sequence Diagram

sequenceDiagram
    participant T as Terminal Process
    participant R as resolveTerminalOpenURLTarget
    participant WV as WKWebView (embedded)
    participant ND as BrowserNavigationDelegate
    participant SB as System Browser

    T->>R: open https://...oauth...
    R->>R: browserIsOAuthFlowURL(url)
    alt OAuth URL detected
        R-->>SB: .external(url) → NSWorkspace.open
    else Non-OAuth URL
        R-->>WV: .embeddedBrowser(url)
        WV->>ND: decidePolicyFor navigationAction
        ND->>ND: browserIsOAuthFlowURL(url)
        alt OAuth redirect detected (e.g. Sign in with Google)
            ND-->>SB: NSWorkspace.shared.open(url) + .cancel
        else Normal navigation
            ND-->>WV: .allow
        end
    end
Loading

Last reviewed commit: 04cc981

Comment thread Sources/Panels/BrowserPanel.swift Outdated
}

// Microsoft identity platform
if host.hasSuffix("login.microsoftonline.com") || host == "login.live.com" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

host.hasSuffix("login.microsoftonline.com") will incorrectly match any domain that ends with this string, including hostnames like evillogin.microsoftonline.com. Since hasSuffix performs a raw string suffix check with no dot boundary validation, any prefix sharing the suffix string will match.

For example: "evillogin.microsoftonline.com".hasSuffix("login.microsoftonline.com") → true ✗

The fix is to check for exact match OR a dot-anchored subdomain:

Suggested change
if host.hasSuffix("login.microsoftonline.com") || host == "login.live.com" {
if host == "login.microsoftonline.com" || host.hasSuffix(".login.microsoftonline.com") || host == "login.live.com" {

This ensures only login.microsoftonline.com itself, or legitimate subdomains like foo.login.microsoftonline.com, match — not arbitrary domains sharing the suffix string.

Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment on lines +538 to +539
if path.contains("/oauth") || path.contains("/oauth2") || path.contains("/o/oauth2") {
return true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path.contains("/oauth") is overly broad and will match any path containing the substring, including non-OAuth URLs like /oauth-settings, /oauth-documentation, or /oauth-callback. This means a legitimate site with a path like https://myapp.com/oauth-docs would be incorrectly redirected to the system browser instead of the embedded browser.

Additionally, the path.contains("/oauth2") and path.contains("/o/oauth2") checks are fully redundant — any path containing /oauth2 already contains /oauth and would have been caught by the first condition.

A more precise approach would either use path components to match exact segments or require a trailing slash:

Suggested change
if path.contains("/oauth") || path.contains("/oauth2") || path.contains("/o/oauth2") {
return true
if path.contains("/oauth/") || path.contains("/oauth2/") || path.contains("/o/oauth2") || path == "/oauth" || path == "/oauth2" {

This avoids matching /oauth- prefixed paths while still catching standard OAuth endpoints like /oauth/authorize, /oauth2/callback, etc.

Comment on lines +4000 to +4005
let opened = NSWorkspace.shared.open(url)
#if DEBUG
dlog("browser.navigation.oauth source=navDelegate opened=\(opened ? 1 : 0) url=\(url.absoluteString)")
#endif
decisionHandler(.cancel)
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The adjacent deeplink-handling block (lines 4010–4016) logs an NSLog error when NSWorkspace.shared.open(url) returns false. The OAuth block should do the same for consistency and to help diagnose cases where the system browser couldn't be launched.

Suggested change
let opened = NSWorkspace.shared.open(url)
#if DEBUG
dlog("browser.navigation.oauth source=navDelegate opened=\(opened ? 1 : 0) url=\(url.absoluteString)")
#endif
decisionHandler(.cancel)
return
if let url = navigationAction.request.url,
navigationAction.targetFrame?.isMainFrame != false,
browserIsOAuthFlowURL(url) {
let opened = NSWorkspace.shared.open(url)
if !opened {
NSLog("BrowserPanel OAuth navigation failed to open URL in system browser: %@", url.absoluteString)
}
#if DEBUG
dlog("browser.navigation.oauth source=navDelegate opened=\(opened ? 1 : 0) url=\(url.absoluteString)")
#endif
decisionHandler(.cancel)
return
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 290-295: The current early return using
browserIsOAuthFlowURL(parsed) only triggers for already-parsed http(s) inputs
and misses bare-host inputs that later get normalized by
resolveBrowserNavigableURL(trimmed), causing those OAuth links to be treated as
.embeddedBrowser; update the resolver so that after calling
resolveBrowserNavigableURL(trimmed) you also run browserIsOAuthFlowURL on the
normalized/parsed result (the value returned by resolveBrowserNavigableURL) and,
if it matches, return .external(...) instead of falling through to
.embeddedBrowser; reference browserIsOAuthFlowURL(parsed),
resolveBrowserNavigableURL(trimmed), the returned value from that call, and the
.external/.embeddedBrowser cases to locate and change the logic.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 548-549: The hostname check using
host.hasSuffix("login.microsoftonline.com") is too broad and will match
malicious hosts like evillogin.microsoftonline.com; update the check in
BrowserPanel (the code around the host variable and the existing host ==
"login.live.com" branch) to use a boundary-aware match—either host ==
"login.microsoftonline.com" for the exact host or
host.hasSuffix(".login.microsoftonline.com") to allow only legitimate subdomains
(keep the host == "login.live.com" check as-is).
- Around line 537-540: The current substring checks on the request path
(path.contains("/oauth") etc.) are too broad and match non-OAuth pages; update
the logic in BrowserPanel.swift where the path variable is evaluated to instead
compare path segments or specific endpoints (e.g. exact segment equals "oauth",
or check for endpoints like "/oauth", "/oauth/", "/oauth/authorize", "/oauth2",
"/o/oauth2") using URLComponents.path split by "/" or URL.pathComponents so you
only match when a path segment or full endpoint equals the OAuth targets;
replace the three contains(...) checks with strict segment/endpoint comparisons
to avoid matching things like "/docs/oauth".
- Around line 3997-4005: The OAuth handoff block currently calls
NSWorkspace.shared.open(url) and cancels navigation regardless of the returned
Bool; update the branch that checks browserIsOAuthFlowURL(url) to handle a
failed handoff by logging the failure (use dlog with opened value and url like
the external-link path does) and surface a fallback instead of silently
cancelling (e.g., present an alert/error to the user or allow the WebView to
load the URL as a fallback); ensure you still call decisionHandler(.cancel) only
when the open succeeded, and on failure invoke the same error-handling path used
elsewhere in this file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 38c08b54-36d1-4de2-a44a-270826108f91

📥 Commits

Reviewing files that changed from the base of the PR and between 9302488 and 04cc981.

📒 Files selected for processing (2)
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/BrowserPanel.swift

Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment on lines +3997 to +4005
if let url = navigationAction.request.url,
navigationAction.targetFrame?.isMainFrame != false,
browserIsOAuthFlowURL(url) {
let opened = NSWorkspace.shared.open(url)
#if DEBUG
dlog("browser.navigation.oauth source=navDelegate opened=\(opened ? 1 : 0) url=\(url.absoluteString)")
#endif
decisionHandler(.cancel)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t silently cancel when the external handoff fails.

This path computes opened but cancels the WebView navigation even when NSWorkspace.shared.open(url) returns false. That leaves the user with a dead click and no diagnostic. Please at least mirror the external-link path below by logging the failure, and ideally surface an error/fallback.

Suggested fix
         if let url = navigationAction.request.url,
            navigationAction.targetFrame?.isMainFrame != false,
            browserIsOAuthFlowURL(url) {
             let opened = NSWorkspace.shared.open(url)
+            if !opened {
+                NSLog("BrowserPanel OAuth navigation failed to open URL externally: %@", url.absoluteString)
+            }
             `#if` DEBUG
             dlog("browser.navigation.oauth source=navDelegate opened=\(opened ? 1 : 0) url=\(url.absoluteString)")
             `#endif`
             decisionHandler(.cancel)
             return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let url = navigationAction.request.url,
navigationAction.targetFrame?.isMainFrame != false,
browserIsOAuthFlowURL(url) {
let opened = NSWorkspace.shared.open(url)
#if DEBUG
dlog("browser.navigation.oauth source=navDelegate opened=\(opened ? 1 : 0) url=\(url.absoluteString)")
#endif
decisionHandler(.cancel)
return
if let url = navigationAction.request.url,
navigationAction.targetFrame?.isMainFrame != false,
browserIsOAuthFlowURL(url) {
let opened = NSWorkspace.shared.open(url)
if !opened {
NSLog("BrowserPanel OAuth navigation failed to open URL externally: %@", url.absoluteString)
}
`#if` DEBUG
dlog("browser.navigation.oauth source=navDelegate opened=\(opened ? 1 : 0) url=\(url.absoluteString)")
`#endif`
decisionHandler(.cancel)
return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanel.swift` around lines 3997 - 4005, The OAuth
handoff block currently calls NSWorkspace.shared.open(url) and cancels
navigation regardless of the returned Bool; update the branch that checks
browserIsOAuthFlowURL(url) to handle a failed handoff by logging the failure
(use dlog with opened value and url like the external-link path does) and
surface a fallback instead of silently cancelling (e.g., present an alert/error
to the user or allow the WebView to load the URL as a fallback); ensure you
still call decisionHandler(.cancel) only when the open succeeded, and on failure
invoke the same error-handling path used elsewhere in this file.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/Panels/BrowserPanel.swift">

<violation number="1" location="Sources/Panels/BrowserPanel.swift:534">
P2: `browserIsOAuthFlowURL` should restrict matching to web URLs. Without an `http/https` scheme check, non-web main-frame URLs (for example `file://.../oauth...`) can be incorrectly redirected to the system browser.</violation>

<violation number="2" location="Sources/Panels/BrowserPanel.swift:538">
P2: Use path-segment matching for OAuth detection instead of raw substring checks so non-auth pages (for example `/docs/oauth`) are not incorrectly routed to the system browser.</violation>

<violation number="3" location="Sources/Panels/BrowserPanel.swift:548">
P3: Make the Microsoft host check boundary-aware (`==` or `hasSuffix(".login.microsoftonline.com")`) to avoid matching unintended hostnames.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
- Use path-segment matching instead of substring for /oauth, /oauth2
  (prevents false positives on /docs/oauth, /settings/oauth-help)
- Add http/https scheme guard so file:// URLs aren't affected
- Make Microsoft host check boundary-aware (== or .hasSuffix(".login..."))
- Handle bare-host OAuth URLs (e.g., accounts.google.com/signin/...)
  in the resolveBrowserNavigableURL path
- Log error when NSWorkspace.shared.open() fails for OAuth URLs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
Sources/Panels/BrowserPanel.swift (2)

4005-4016: ⚠️ Potential issue | 🟡 Minor

Don’t cancel the navigation when the external handoff fails.

If NSWorkspace.shared.open(url) returns false on Line 4008, this branch still cancels on Line 4015. That leaves the user with a dead click and no login path at all.

Suggested fix
             let opened = NSWorkspace.shared.open(url)
             if !opened {
                 NSLog("BrowserPanel OAuth external navigation failed to open URL: %@", url.absoluteString)
             }
             `#if` DEBUG
             dlog("browser.navigation.oauth source=navDelegate opened=\(opened ? 1 : 0) url=\(url.absoluteString)")
             `#endif`
-            decisionHandler(.cancel)
+            decisionHandler(opened ? .cancel : .allow)
             return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanel.swift` around lines 4005 - 4016, The code
currently cancels the WKNavigationAction unconditionally even when
NSWorkspace.shared.open(url) fails; update the logic in the navigation delegate
branch (where you check navigationAction.request.url and
browserIsOAuthFlowURL(url)) to only call decisionHandler(.cancel) if opened is
true, and if opened is false call decisionHandler(.allow) so the web view can
continue the navigation (also keep the existing NSLog and DEBUG dlog behavior);
reference the opened variable, the browserIsOAuthFlowURL check, and the
decisionHandler call to locate the change.

540-545: ⚠️ Potential issue | 🟠 Major

The generic /oauth match is still too broad.

Line 540 turns the path into a Set, and Line 544 then matches oauth / oauth2 anywhere in the path. URLs like /docs/oauth or /settings/oauth will still be forced into the system browser, which breaks the “non-OAuth URLs stay embedded” goal.

Suggested fix
-    let pathSegments = Set(url.path.lowercased().split(separator: "/").map(String.init))
+    let pathSegments = url.path.lowercased().split(separator: "/").map(String.init)

     // Standard OAuth authorize/callback endpoints — match as a path segment,
     // not a substring, so "/docs/oauth" or "/oauth-settings" won't trigger.
-    if pathSegments.contains("oauth") || pathSegments.contains("oauth2") {
+    if pathSegments.first == "oauth" ||
+        pathSegments.first == "oauth2" ||
+        Array(pathSegments.prefix(2)) == ["o", "oauth2"] {
         return true
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanel.swift` around lines 540 - 545, The current code
creates a Set called pathSegments and then checks contains("oauth")/("oauth2"),
which loses order and incorrectly matches trailing segments like "/docs/oauth";
change to keep the path segments as an ordered array (e.g., pathSegmentsArray =
url.path.lowercased().split(separator: "/").map(String.init)) and then only
treat the URL as OAuth if the first path segment equals "oauth" or "oauth2"
(e.g., if let first = pathSegmentsArray.first, first == "oauth" || first ==
"oauth2" { return true }); update any references from pathSegments to the new
array variable and ensure nil/empty-first-segment cases are handled safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 4005-4016: The code currently cancels the WKNavigationAction
unconditionally even when NSWorkspace.shared.open(url) fails; update the logic
in the navigation delegate branch (where you check navigationAction.request.url
and browserIsOAuthFlowURL(url)) to only call decisionHandler(.cancel) if opened
is true, and if opened is false call decisionHandler(.allow) so the web view can
continue the navigation (also keep the existing NSLog and DEBUG dlog behavior);
reference the opened variable, the browserIsOAuthFlowURL check, and the
decisionHandler call to locate the change.
- Around line 540-545: The current code creates a Set called pathSegments and
then checks contains("oauth")/("oauth2"), which loses order and incorrectly
matches trailing segments like "/docs/oauth"; change to keep the path segments
as an ordered array (e.g., pathSegmentsArray =
url.path.lowercased().split(separator: "/").map(String.init)) and then only
treat the URL as OAuth if the first path segment equals "oauth" or "oauth2"
(e.g., if let first = pathSegmentsArray.first, first == "oauth" || first ==
"oauth2" { return true }); update any references from pathSegments to the new
array variable and ensure nil/empty-first-segment cases are handled safely.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 180f46f5-f48c-469e-bb9c-5cdf86011a99

📥 Commits

Reviewing files that changed from the base of the PR and between 04cc981 and 91b6999.

📒 Files selected for processing (2)
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/BrowserPanel.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/GhosttyTerminalView.swift

@ss251

ss251 commented Jul 4, 2026

Copy link
Copy Markdown

This PR still fixes a live bug — I just hit it and filed #7351 with a fresh repro and a source-level root-cause writeup against current main (@ 9c91710).

Concrete case: claude /login in a cmux terminal → cmd-click the printed https://claude.ai/oauth/authorize?… URL (routed into the in-app browser by #5406) → "Continue with Google" succeeds on Google's side → claude.ai bounces back to the logged-out state and no OAuth code is issued. Pasting the same URL into Safari/Chrome logs in instantly.

Reading the source, this PR's approach (route OAuth/SSO URLs to the system browser) is the right fix. The failure isn't cookie persistence or dropped popups — the in-app browser correctly uses a persistent shared WKWebsiteDataStore.default() and implements createWebViewWith. It's that the auth URL loads in an isolated, app-private WKWebView with no existing claude.ai/Google session, under WebKit's default ITP / cross-site cookie partitioning, so the Google→claude.ai session-cookie handshake gets blocked. Punting auth URLs to the system browser (where the user is already signed in) sidesteps all of it, and the http://localhost:PORT/callback loopback is still reachable by any browser on the machine.

Status blockers right now: this branch is conflicting with main (the touched files, Sources/GhosttyTerminalView.swift and Sources/Panels/BrowserPanel.swift, have since been refactored — the routing seam is now the .embeddedBrowser branch around GhosttyTerminalView.swift:3143-3169 and the BrowserExternalOpenSettings in BrowserPanel.swift:461-464), and CI never ran because the Vercel deploy is still waiting on team authorization.

Would a maintainer be willing to authorize the deploy and/or greenlight a rebase? Happy to help rebase onto current main if that unblocks it. This is a fairly common paper-cut for anyone signing into Claude/GitHub/Google from a cmux terminal.

@teamleaderleo teamleaderleo added area: browser The embedded browser, web surfaces, inline VS Code S2: major A crash, hang, lost state, broken connection, or a regression on a path people use labels Sep 30, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Still reproduces on main at 14fae18; current repro and root-cause notes are tracked in #7351.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: browser The embedded browser, web surfaces, inline VS Code S2: major A crash, hang, lost state, broken connection, or a regression on a path people use

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants