From 7674f8babac0243bf48d2bea235c146c1f65ce3f Mon Sep 17 00:00:00 2001 From: Brennan Benson Date: Thu, 9 Jul 2026 16:19:05 -0700 Subject: [PATCH] Suppress Git Credential Manager OAuth popup on git clone (fixes #7652) Orca's git runner disables the interactive credential prompt on every git call that goes through gitExecFileAsync/gitStreamStdout, but the two raw 'git clone' spawns (desktop repos:clone and the runtime clone path) passed no env, so they inherited process.env with no guard. On Windows a clone that needs GitHub auth then makes Git Credential Manager pop its 'Connect to GitHub' OAuth window, and in a network-restricted intranet the browser/device flow never completes while git's credential retry re-pops it. Apply nonInteractiveGitEnv() to both clone spawns so the prompt is suppressed (GCM_INTERACTIVE=never, credential.interactive=false, GIT_TERMINAL_PROMPT=0). The credential *helper* is kept, so cached-token clones for private repos still work; only the interactive fallback popup is disabled and the clone fails fast with a clear error instead. --- src/main/ipc/repos-remote.test.ts | 31 ++++++++++++++++++++++++++++++- src/main/ipc/repos.ts | 9 ++++++++- src/main/runtime/orca-runtime.ts | 8 +++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/main/ipc/repos-remote.test.ts b/src/main/ipc/repos-remote.test.ts index bd2541c7ebd..7d73f28c436 100644 --- a/src/main/ipc/repos-remote.test.ts +++ b/src/main/ipc/repos-remote.test.ts @@ -103,7 +103,14 @@ vi.mock('../git/repo', async () => { vi.mock('../git/runner', () => ({ gitExecFileAsync: vi.fn(), - gitSpawn: gitSpawnMock + gitSpawn: gitSpawnMock, + // Mirrors the real guard's credential-prompt markers so the clone regression + // test (#7652) can assert Git Credential Manager's OAuth popup is disabled. + nonInteractiveGitEnv: (env: NodeJS.ProcessEnv = process.env) => ({ + ...env, + GIT_TERMINAL_PROMPT: '0', + GCM_INTERACTIVE: 'never' + }) })) vi.mock('../git/worktree', () => ({ @@ -2067,6 +2074,28 @@ describe('repos:add + repos:clone', () => { expect(result).toHaveProperty('path', join(destination, 'orca')) }) + it('clones with the non-interactive credential guard so Git Credential Manager cannot pop its OAuth window (#7652)', async () => { + const destination = await createTempRoot() + + await handlers.get('repos:clone')!(null, { + url: 'https://example.com/orca.git', + destination + }) + + // Without this env, a clone that needs GitHub auth makes Git Credential + // Manager pop its "Connect to GitHub" OAuth window on Windows and loop it + // when the network cannot complete the flow. + expect(gitSpawnMock).toHaveBeenCalledWith( + ['clone', '--progress', '--', 'https://example.com/orca.git', join(destination, 'orca')], + expect.objectContaining({ + env: expect.objectContaining({ + GIT_TERMINAL_PROMPT: '0', + GCM_INTERACTIVE: 'never' + }) + }) + ) + }) + it('treats cloneAbort with no active clone as a no-op', async () => { await expect(handlers.get('repos:cloneAbort')!(null, undefined)).resolves.toBeUndefined() }) diff --git a/src/main/ipc/repos.ts b/src/main/ipc/repos.ts index c5d1c8f2a9c..8dac54f6670 100644 --- a/src/main/ipc/repos.ts +++ b/src/main/ipc/repos.ts @@ -42,7 +42,7 @@ import { isTuiAgent } from '../../shared/tui-agent-config' import { invalidateAuthorizedRootsCache } from './filesystem-auth' import type { ChildProcess } from 'node:child_process' import { access, mkdir, readdir, rm } from 'node:fs/promises' -import { gitExecFileAsync, gitSpawn } from '../git/runner' +import { gitExecFileAsync, gitSpawn, nonInteractiveGitEnv } from '../git/runner' import { isAbsolute, join, posix } from 'node:path' import { cleanupClaimedCloneTarget, @@ -2253,6 +2253,13 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v try { proc = gitSpawn(['clone', '--progress', '--', args.url, clonePath], { cwd: args.destination, + // Why: without the non-interactive guard, a clone that needs + // GitHub auth makes Git Credential Manager pop its "Connect to + // GitHub" OAuth window on Windows; in a network-restricted env the + // browser/device flow can never complete and git's credential + // retry re-pops it (issue #7652). Fail fast with a clear error and + // let Orca's non-intrusive GitHub state stand instead. + env: nonInteractiveGitEnv(), stdio: ['ignore', 'ignore', 'pipe'] }) } catch (err) { diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 9681ea33e14..d600720ae63 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -41,7 +41,7 @@ import { AGENT_PROMPT_SUBMIT_DELAY_MS, buildAgentPromptPasteBytes } from '../../shared/agent-prompt-injection' -import { gitExecFileAsync, wslAwareSpawn } from '../git/runner' +import { gitExecFileAsync, wslAwareSpawn, nonInteractiveGitEnv } from '../git/runner' import { cleanupClaimedCloneTarget, claimCloneTarget, @@ -10666,6 +10666,12 @@ export class OrcaRuntimeService { try { proc = wslAwareSpawn('git', ['clone', '--progress', '--', trimmedUrl, clonePath], { cwd: trimmedDestination, + // Why: without the non-interactive guard, a clone that needs GitHub + // auth makes Git Credential Manager pop its "Connect to GitHub" OAuth + // window on Windows; in a network-restricted env the browser/device + // flow can never complete and git's credential retry re-pops it + // (issue #7652). Fail fast with a clear error instead. + env: nonInteractiveGitEnv(), stdio: ['ignore', 'ignore', 'pipe'] }) } catch (err) {