Chrome fails to launch when running agent-browser from an elevated shell on Windows

Author: mxschmittCreated Jun 1, 2026Updated Sep 14, 2026

Running agent-browser commands from a "Run as administrator" shell on Windows produces this error every time:

✗ Auto-launch failed: Chrome exited early (exit code: 0) without writing DevToolsActivePort
(also tried parsing stderr) Chrome exited before providing DevTools URL (no stderr output from Chrome)
Hint: try passing --args "--no-sandbox" if Chrome crashes silently in your environment

The --no-sandbox hint sends people down the wrong path — it doesn't help. Both bundled Chrome for Testing (installed via agent-browser install) and system Chrome reproduce.

Repro

  1. Open cmd.exe or PowerShell via "Run as administrator" — has to be a UAC consent elevation, not the always-on built-in Administrator account, since Chrome only reacts to TokenElevationTypeFull.
  2. agent-browser open https://example.com

I see this on Windows 11 26200 with agent-browser 0.27.0, Chrome for Testing 149, and system Chrome 148. Both --headed and headless reproduce. #868 looks like the same bug — the comment by @mbakhodir matches the symptom but proposes a process-group / console-detach fix that doesn't address the actual cause.

What's going on

Chrome shipped an AutoDeElevate feature in M138. The relevant Chromium code is in chrome_browser_main_win.cc and elevation_util.cc. When Chrome detects it's running with TokenElevationTypeFull, MaybeAutoDeElevate checks UserAccountIsUnnecessarilyElevated and, if true, relaunches itself unelevated through Explorer's medium-integrity token via CreateProcessWithTokenW. The original (elevated) Chrome process exits cleanly. The unelevated grandchild inherits --user-data-dir and --remote-debugging-port=0 and writes DevToolsActivePort ~500–1000 ms later.

agent-browser doesn't know any of this is happening. wait_for_devtools_active_port (in cli/src/native/cdp/chrome.rs) polls child.try_wait() first on each iteration, sees the elevated parent's exit, and bails before checking the port file. The retry loop above just spawns more Chrome processes that auto-de-elevate the same way.

There's a second problem with bundled Chrome specifically: Chrome for Testing installs into ~/.agent-browser/browsers/chrome-149.../, which by default has an ACL that grants only the owning user. Even if agent-browser adopted the de-elevated grandchild, the renderer/GPU sandbox can't read chrome.exe at low integrity and the GPU process cycle-crashes within a second:

ERROR: Sandbox cannot access executable C:\Users\<user>\.agent-browser\browsers\...\chrome.exe.
       Check filesystem permissions are valid. Access is denied. (0x5)
ERROR: GPU process launch failed: error_code=18
FATAL: GPU process isn't usable. Goodbye.

System Chrome doesn't hit this because the installer grants Users:RX system-wide on Program Files.

For reference: Playwright works around this by passing --disable-features=AutoDeElevate and runs --no-sandbox by default. Puppeteer, chromedriver, and chromedp avoid it incidentally because they always pass --enable-automation, which is in Chromium's kNoRestartSwitches list and short-circuits MaybeAutoDeElevate. agent-browser does neither.

Suggested fix

Spawn Chrome unelevated from the daemon by duplicating Explorer's primary token and using CreateProcessWithTokenW — the same Win32 sequence Chrome uses internally in base::win::RunDeElevated. Chrome ends up running with the user's standard token from the start, the renderer sandbox can read the binary because integrity matches, and MaybeAutoDeElevate no-ops because the user account isn't "unnecessarily elevated" anymore. Sandbox stays enabled, no flag hacks.

Detection mirrors Chromium's own check (OpenProcessToken + GetTokenInformation(TokenElevationType) checking for TokenElevationTypeFull). The non-elevated path stays byte-identical.

Tested against an elevated PowerShell on Windows 11:

Before After
Bundled Chrome, headless exits early works
Bundled Chrome, headed exits early works
System Chrome, headless exits early works

I have a working patch — happy to open a PR if this approach is acceptable. The diff is roughly 500 lines: a new deelevate.rs module that does the token duplication, plus a small refactor in chrome.rs so ChromeProcess can hold either a std::process::Child or a foreign HANDLE (since Rust has no public way to wrap a foreign process handle as a Child).

Diagnostics aside

Two open Chrome-launch issues (#868 and #1378) hit the same Chrome exited early (exit code: 0) message via different root causes. Worth distinguishing them in the error output rather than always pointing at --no-sandbox:

  • auto-de-elevation handoff (this issue) → "Chrome auto-de-elevated; daemon was running elevated"
  • --user-data-dir lock collision (#1378) → "profile X is already in use"
  • generic crash → existing --no-sandbox hint

Other approaches I tried

For posterity, in case this comes up again:

Disable the feature globally — Playwright's --disable-features=AutoDeElevate. Works, but Playwright pairs it with --no-sandbox, so the sandbox is always off. Also disables an upstream feature for whatever profile the user is on. Functional but worse posture than necessary.

--do-not-de-elevate switch — listed in Chromium's kNoRestartSwitches, so it suppresses MaybeAutoDeElevate cleanly. No UI side effects (unlike --enable-automation, which sets navigator.webdriver=true, shows the "automated test software" infobar, and disables the password manager). The catch: bundled Chrome still needs --no-sandbox because of the ACL/integrity issue, so this also costs the sandbox in practice.

Adopt the auto-de-elevated grandchild via GetExtendedTcpTable — let Chrome auto-de-elevate, then find the grandchild by looking up the listening TCP port's owner, hold a HANDLE for shutdown. Works for system Chrome. Bundled Chrome still dies at GPU init unless --no-sandbox is also passed. The token-duplication approach above subsumes this — by getting the integrity right at spawn, there's no grandchild to chase.

Daemon re-launches itself unelevated via the same Win32 mechanism — same idea as the chosen fix but applied one frame earlier: the daemon spawns an unelevated copy of itself, waits for it to bind, exits. Smaller diff at the Chrome-spawn site (no chrome.rs changes). Trade-off is timing — CreateProcessWithTokenW adds about 7s of process-startup overhead (Defender real-time scan and similar), so the unelevated successor takes ~13s to become reachable. The CLI's existing ensure_daemon loop polls for ~5s and gives up first. Viable if the daemon-spawn handshake is restructured, otherwise flaky.

ACL widening on the bundled Chrome dir (Users:RX, ALL APPLICATION PACKAGES, mandatory integrity label set to Low) — none of these were enough. Either the renderer still gets Sandbox cannot access executable, or you trip a follow-on persistent-cache lock failure because the user-data-dir is owned by an elevated principal that low-IL processes can't write to. The whole user-data-dir tree would need to be widened, and Chrome creates files there continuously.

Install bundled Chrome to %ProgramFiles% — sidesteps the ACL issue, but requires admin at install time and breaks the install-without-admin model agent-browser is built around. Existing installs would also need migration.

Source: vercel-labs/agent-browser