Skip to content

fix(accounts): open WSL Claude sign-in in Windows browser - #12023

Closed
rodboev wants to merge 4 commits into
stablyai:mainfrom
rodboev:pr/orca-11823-wsl-claude-paste-output
Closed

rodboev wants to merge 4 commits into
stablyai:mainfrom
rodboev:pr/orca-11823-wsl-claude-paste-output

Conversation

@rodboev

@rodboev rodboev commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Give WSL Claude sign-in a private, temporary browser opener when the selected distro has no opener on PATH. The shim writes one atomic URL handoff into the existing temporary config directory; the main process validates and opens it through Electron. Existing distro openers keep precedence, and no renderer, IPC, preload, or mobile surface changes.

Closes #11823.

Screenshots

No visual change. The only user-visible change is a more specific existing failure-toast message when opener preparation fails.

Testing

Focused Vitest validation passed: the three touched bridge/service test files passed 52 tests. pnpm run typecheck:node, pnpm run typecheck:web, changed-file oxlint, and changed-file oxfmt --check passed.

Post-fix WSL opener validation passed: the focused bridge suite passed 17 tests, including cleanup-failure handling.

Real Windows + WSL2 and browser-owner checks remain unrun in this environment.

AI Review Report

Native host login, existing WSL openers, credential capture, persistence, rollback, IPC, preload, renderer, web, RPC, mobile, SSH, remote, macOS, Linux, and performance surfaces remain unchanged. The fallback is scoped to interactive WSL Claude account login and reauthentication.

Security Audit

The handoff uses one bounded read, atomically claims and deletes the request before validation, accepts only exact HTTPS hosts claude.com, platform.claude.com, console.anthropic.com, and anthropic.com, and rejects userinfo, fragments, ports, localhost, suffix lookalikes, and arbitrary hosts. The shim uses a quoted heredoc, an atomic same-filesystem rename, mode 0700, and no stdin or output. PATH injection is limited to the interactive WSL login command.

Notes

The live base is origin/main at 8fc892dd023850df379c07cc4e7a46cff9a3ea84. The committed head is 6c91419531233f6d93a406fe7417c8b909c56323.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds WSL browser-login support for Claude accounts. The change creates temporary login configuration directories, captures authorization URLs through WSL handoff files, validates and opens approved URLs through Electron, and stops handoff processing during cleanup. WSL command execution exports the login path when required. Tests cover script generation, URL validation, polling behavior, login integration, account creation, reauthentication, and command wiring.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes directly address issue #11823 by adding a validated Windows-browser fallback for WSL Claude login when no distro opener exists.
Out of Scope Changes check ✅ Passed The changes are limited to WSL Claude login browser handoff support and focused regression tests; no unrelated code changes are evident.
Title check ✅ Passed The title clearly and concisely describes the primary change: enabling WSL Claude sign-in through the Windows browser.
Description check ✅ Passed The description covers the summary, testing, security audit, platform scope, screenshots, and known validation limits.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/main/claude-accounts/wsl-login-browser-opener.ts (2)

135-140: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Attach a rejection handler to the onUrl result.

onUrl is typed as void | Promise<void>. void args.onUrl(url) discards a rejected promise and produces an unhandled rejection. The service callback catches its own errors today, but wsl-login-opener-flow.test.ts passes openWslLoginAuthorizationUrl directly, which can reject.

♻️ Proposed change
     const url = parseWslLoginOpenerHandoff(raw)
     if (url) {
-      void args.onUrl(url)
+      void Promise.resolve(args.onUrl(url)).catch(() => args.onInvalid())
     } else {
       args.onInvalid()
     }

152-154: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Revalidate the URL inside openWslLoginAuthorizationUrl.

The function forwards any string to shell.openExternal. Validation lives only at the call site in createWslLoginOpenerHandoff. If a later caller skips parseWslLoginOpenerHandoff, the allowlist no longer applies. Move the check into this function to keep one enforcement point.

🛡️ Proposed change
 export function openWslLoginAuthorizationUrl(url: string): Promise<void> {
-  return shell.openExternal(url)
+  const safeUrl = parseWslLoginOpenerHandoff(url)
+  if (!safeUrl) {
+    return Promise.reject(new Error('Refusing to open an unsupported authorization URL.'))
+  }
+  return shell.openExternal(safeUrl)
 }
src/main/claude-accounts/service.ts (1)

1092-1092: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Gate the opener PATH export on an explicit option.

The export is tied to keepStdinOpen, which describes stdio behavior, not the login flow. A future caller that keeps stdin open for a different WSL command would also receive the opener path, and that config directory may contain no orca-opener directory. Add a separate option, for example exportWslLoginOpenerPath, and set it next to keepStdinOpen at the auth login call site.

src/main/claude-accounts/service.test.ts (1)

1646-1649: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Also unmock ../wsl in the finally block.

The test calls vi.doMock('../wsl', ...) but only unmocks node:child_process. The ../wsl mock stays registered for the rest of the file. A later test that imports ./service after another vi.resetModules() then receives the stubbed toWindowsWslPath. Add the matching vi.doUnmock call.

♻️ Proposed change
     } finally {
       vi.doUnmock('node:child_process')
+      vi.doUnmock('../wsl')
       rmSync(windowsRoot, { recursive: true, force: true })
     }
src/main/claude-accounts/wsl-login-opener-flow.test.ts (1)

14-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move timer and directory cleanup into afterEach.

vi.useRealTimers() and rmSync run at the end of the test body. If an assertion fails, both are skipped. Fake timers then stay active for later tests in the worker, and the temporary directory leaks. The sibling file wsl-login-browser-opener.test.ts already restores timers in afterEach. Use the same pattern here.

♻️ Proposed change
-import { describe, expect, it, vi } from 'vitest'
+import { afterEach, describe, expect, it, vi } from 'vitest'
@@
 describe('WSL Claude login opener flow', () => {
+  afterEach(() => {
+    vi.useRealTimers()
+  })
+
   it('delivers one URL to the Windows-side browser owner and consumes the request', () => {
@@
     watcher.stop()
     rmSync(root, { recursive: true, force: true })
-    vi.useRealTimers()
   })

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 22771039-4174-4ac7-aab8-08f90c051bfd

📥 Commits

Reviewing files that changed from the base of the PR and between 8fc892d and 5433133.

📒 Files selected for processing (5)
  • src/main/claude-accounts/service.test.ts
  • src/main/claude-accounts/service.ts
  • src/main/claude-accounts/wsl-login-browser-opener.test.ts
  • src/main/claude-accounts/wsl-login-browser-opener.ts
  • src/main/claude-accounts/wsl-login-opener-flow.test.ts

Comment thread src/main/claude-accounts/service.ts Resolved
@greptile-apps

greptile-apps Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR adds a fallback browser opener for WSL Claude sign-in on Windows: when the selected WSL distro has no xdg-open or wslview on PATH, a lightweight shell shim is installed into a temporary orca-opener directory and appended to PATH before launching claude auth login. When the shim fires, it atomically writes an open-url.request handoff file; the main process polls for that file, claims it with a rename, reads and validates the URL, then opens it in Windows via shell.openExternal.

  • wsl-login-browser-opener.ts (new): Shim script generation, parseWslLoginOpenerHandoff with strict HTTPS + exact-host allowlist validation, atomic claim-before-read polling loop, and three distinct error callbacks (onUrl, onInvalid, onReadError).
  • service.ts: createTemporaryClaudeConfigDir now runs a provisioning script instead of a bare mktemp; runClaudeLoginAndCapture starts the handoff watcher for WSL sessions and prioritises openerError over the raw login error in the catch block.

Confidence Score: 5/5

Safe to merge. The new WSL browser opener shim is narrowly scoped to interactive WSL Claude login, leaves all existing code paths unchanged, and the URL handoff is guarded by an atomic rename, a byte-bounded read, and an exact-host HTTPS allowlist before anything reaches shell.openExternal.

The handoff mechanism is carefully constructed: atomic claim-before-read prevents double-dispatch; three distinct error callbacks give the caller precise failure context; openerError is correctly prioritised over the raw login error in the catch block; PATH injection is append-only so existing distro openers retain precedence; and the provisioning script self-tests the shim before printing the directory marker. No functional defects were identified in the changed code.

Files Needing Attention: No files require special attention. The service.ts integration and all new files reviewed cleanly.

Important Files Changed

Filename Overview
src/main/claude-accounts/wsl-login-browser-opener.ts New module implementing WSL browser opener shim: atomic claim-before-read handoff, strict HTTPS URL allowlist validation, controlled cleanup ordering, and Electron shell opener. Well-structured with no issues found.
src/main/claude-accounts/service.ts Wires the new opener handoff into the WSL login flow: creates the handoff watcher only when linuxPath is set, forwards distinct error messages for URL-open vs invalid-URL vs read-error cases, and always stops the watcher in finally. PATH export is correctly scoped to the keepStdinOpen (login) command only.
src/main/claude-accounts/wsl-login-browser-opener.test.ts Comprehensive unit tests covering: self-test and script content, output parsing, URL validation allowlist (11 rejection cases and 4 acceptance cases), byte-length bounding, atomic claim-and-delete, and cleanup failure handling.
src/main/claude-accounts/wsl-login-opener-flow.test.ts Integration-style test verifying end-to-end delivery of a URL to the Windows browser via openExternal, one-shot consumption, and no second delivery on a duplicate write.
src/main/claude-accounts/service.test.ts Adds a comprehensive end-to-end test exercising the WSL bridge through both addAccount and reauthenticateAccount paths, including mktemp failure, PATH export injection, and openExternal call counts.

Sequence Diagram

sequenceDiagram
    participant S as ClaudeAccountService
    participant H as HandoffWatcher
    participant W as wsl.exe bash
    participant F as open-url.request
    participant E as shell.openExternal

    S->>W: execFileSync buildWslLoginConfigDirScript
    W-->>S: ORCA_CLAUDE_LOGIN_DIR path

    S->>H: createWslLoginOpenerHandoff poll every 25ms
    S->>W: "spawn claude auth login with PATH+=orca-opener"

    W->>F: xdg-open shim writes URL atomically via mv

    H->>F: renameSync to .consumed
    H->>F: readSync bounded read
    H->>F: rmSync .consumed
    H->>H: parseWslLoginOpenerHandoff HTTPS allowlist check
    H->>E: onUrl shell.openExternal

    E-->>H: Windows browser opens OAuth URL

    W-->>S: claude auth login exits 0
    S->>S: auth status and captureAuthFromConfigDir

    S->>H: handoff.stop in finally
    S->>W: rm -rf temp dir
Loading

Reviews (3): Last reviewed commit: "Prevent WSL handoff cleanup failures fro..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/claude-accounts/wsl-login-browser-opener.test.ts (1)

81-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the new onReadError branch.

These tests cover oversized readable input only. They do not execute the catch at src/main/claude-accounts/wsl-login-browser-opener.ts Line 132. Add a deterministic read failure and assert exactly one onReadError call, no URL or invalid callback, and cleanup of the claimed file.

src/main/claude-accounts/service.ts Lines 591-616 maps onReadError to a distinct sign-in failure.

Also applies to: 107-113


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8eb24e20-fe42-4cd5-a5fc-ec8564c12f4d

📥 Commits

Reviewing files that changed from the base of the PR and between 5433133 and 8899922.

📒 Files selected for processing (5)
  • src/main/claude-accounts/service.test.ts
  • src/main/claude-accounts/service.ts
  • src/main/claude-accounts/wsl-login-browser-opener.test.ts
  • src/main/claude-accounts/wsl-login-browser-opener.ts
  • src/main/claude-accounts/wsl-login-opener-flow.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/claude-accounts/wsl-login-opener-flow.test.ts
  • src/main/claude-accounts/service.test.ts
  • src/main/claude-accounts/service.ts

@click6067-ship-it

click6067-ship-it commented Aug 27, 2026 •

Copy link
Copy Markdown

Field report from Orca 1.4.190 on Windows + WSL2: /usr/bin/xdg-open was present, but invoking it did not open the Windows browser. Prepending a temporary xdg-open/wslview shim to PATH allowed the Claude OAuth flow and WSL account registration to complete.

I have not built or tested this PR, so this is field evidence only. Since this patch appends its shim to PATH to preserve existing openers, could you also validate the installed-but-nonfunctional opener case before merge?

@nwparker

Copy link
Copy Markdown
Contributor

Thanks @rodboev for the work here. Closing as superseded: WSL Claude sign-in now creates its own BROWSER helper and opens the callback URL in the desktop browser (#29566, which also closed #11823). Credit for the original approach; appreciate the contribution, and feel free to open a fresh PR against current main if you see anything still missing.

@nwparker nwparker closed this Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: WSL "Add Account" hangs 180s when the distro has no xdg-open — Claude CLI falls back to the paste-code flow and its output is never surfaced

5 participants