Chrome fails to launch when running agent-browser from an elevated shell on Windows
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
- Open
cmd.exeor PowerShell via "Run as administrator" — has to be a UAC consent elevation, not the always-on built-in Administrator account, since Chrome only reacts toTokenElevationTypeFull. 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-dirlock collision (#1378) → "profile X is already in use"- generic crash → existing
--no-sandboxhint
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