# Headless Chrome's user agent gives it away

> Headless Chrome names itself HeadlessChrome in its user agent and drops the real GPU. Why our background work runs headed, off-screen, instead.

Source: https://mosaicbrowser.com/blog/headless-is-a-signal

## Headless is not a way to hide, it is a way to be seen

Headless Chrome names itself HeadlessChrome in its user agent and drops the real GPU. Why our background work runs headed, off-screen, instead.

![Headless is a way to be seen, beside a screenshot: a Mosaic leak test: every surface a site reads, checked from inside the profile page](https://mosaicbrowser.com/covers/headless-is-a-signal.webp)

Headless Chrome tells sites it is headless in its own user agent. On stock Chrome 152 we measured the two side by side: the headed browser sends `Chrome/152.0.0.0`, and the headless one sends `HeadlessChrome/152.0.0.0`. It also drops the real GPU. So `--headless=new`, which looks like the obvious way to run background work out of the user's sight, is the one setting that names the browser as automated to every site it touches.

## Headless Chrome user agent detection: the two strings

These are the two user agents, from the same build, on the same Mac:

```
headed    Mozilla/5.0 (…) Chrome/152.0.0.0 Safari/537.36
headless  Mozilla/5.0 (…) HeadlessChrome/152.0.0.0 Safari/537.36

```

A server sees the second one in the `User-Agent` header of the very first request, before any script runs. A page sees it again in `navigator.userAgent`. Detecting headless Chrome by user agent needs no fingerprinting at all, only a substring match.

The GPU is the quieter half. Headless rendering does not use the machine's real graphics stack, so WebGL's renderer string and everything drawn through it come out different from the same profile run headed. A profile whose fingerprint was drawn around a particular GPU, as ours are, stops matching itself.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Its own Chromium](https://mosaicbrowser.com/features/engine)

## The trap: our best profiles leaked it most

Mosaic passes `--user-agent` to the engine only when a profile's user agent differs from the one the engine would send on its own, because the flag itself empties some of the client hints a real Chrome sends. Usually it does not differ, because a profile is meant to claim the Chrome it really runs. That was the common case and the intended one.

Combine that with headless, as it was before the fix, and the logic inverts. The engine's own user agent, in headless mode, was the `HeadlessChrome` one. The best-configured profiles, whose claimed user agent equalled the engine's, were exactly the ones that got no override, so they were the ones that announced `HeadlessChrome` to every site.

And the work that ran headless was warm-up. The cookie robot defaulted to headless, so it collected each profile's cookies from a browser that had identified itself as automated, on a GPU the profile would never use again. That is worse than never warming up. It writes a bot's visit into the history and calls it a reputation. (The same robot had a second problem, in how it scrolled and clicked, written up in [the warm-up robot that left a bot signature](https://mosaicbrowser.com/blog/warm-up-bot-signature).)

## Headed, parked off-screen

What replaced headless is an ordinary headed browser with its window at `--window-position=-32000,-32000`. Nobody sees it, the same way nobody sees a headless run. But it is the same engine, the same renderer, the same GPU and the same user agent the profile uses when you open it yourself.

The rule we took from it: background work uses the same browser the profile uses. Any shortcut that changes how the engine describes itself changes who the profile is, and the sites it visited remember the wrong one. It applies to more than warm-up. The leak test and the detection checks in Mosaic measure what a site sees, and a headless browser grades a browser we do not ship, so a measurement never runs headless either.

![A Mosaic leak test, run from inside the profile's own page](https://mosaicbrowser.com/shots/leak-test.png)
*The leak test runs headed, in the profile's own browser*

Headless is still there for a caller who asks for it, for example from the [REST API](https://mosaicbrowser.com/features/rest-api). When it is on, the launcher now always forces a user-agent override, so the token cannot reach a site. The client hints that come with an override are then thinner than a real Chrome's, which is the lesser tell, and the GPU difference remains. We would not use it for anything that touches an account.

## What "off-screen" cost on macOS

Replacing one launch mode turned up a fault in code that tested for the old one. Visible profile windows run from a small app bundle with its own identifier, which is what gives them a Dock tile. That step was skipped for headless launches, which have no Dock tile. When background work moved from headless to an off-screen headed window, nothing updated the condition, so every warm-up, leak test and check script started minting a new app identifier, and macOS greets every new identifier with a notifications banner once.

The bug report had two banners named `Stock comparison 21:46:58` and `Stock comparison 21:47:30`, two runs of a check script thirty-two seconds apart. The condition is a function with a full truth table now, and a check pins all sixteen combinations.

## If you automate Chrome yourself

Three things carry over to any setup, with or without Mosaic.

1. Read your own user agent from inside the page you automate, not from your launch flags. If it contains `HeadlessChrome`, every site can see it.
2. Check the WebGL renderer in headless and headed runs of the same profile. If they differ, the profile has two fingerprints.
3. If a window in the way is the problem, park the window instead of removing the browser's head.

Attaching Playwright or Puppeteer to a headed Mosaic profile, rather than launching a headless one, is covered in [using Playwright with an anti-detect browser](https://mosaicbrowser.com/guides/use-playwright-with-an-anti-detect-browser). How the engine itself is built, from stock Chromium with our patches, is on [the engine page](https://mosaicbrowser.com/features/engine).

## Questions

### Does headless Chrome reveal itself in the user agent?

Yes. Measured on stock Chrome 152, the headless user agent read HeadlessChrome/152.0.0.0 where the headed one read Chrome/152.0.0.0. Any site that reads the header or navigator.userAgent sees it.

### Is changing the user agent enough to hide headless Chrome?

No. Headless also drops the real GPU, so the graphics values no longer match the machine the profile claims, and an overridden user agent leaves other traces such as client hints that do not agree with it.

### How does Mosaic run background work without a window in the way?

It starts a normal headed browser and parks its window off every display, at -32000,-32000. Same renderer, same GPU and the same user agent the profile always uses.

### Can I still start a Mosaic profile headless?

Yes, if you ask for it. When you do, the launcher forces a user-agent override so the HeadlessChrome token does not reach sites, but the GPU still differs, so we do not use it for anything that measures or warms a profile.

## Related

[An anti-detect browser with its own Chromium engine](https://mosaicbrowser.com/features/engine): Mosaic is an anti-detect browser with its own Chromium engine, Tesseract Chrome: stock source plus 64 patches that change fingerprint values in C++.

[The warm-up robot that left a bot signature](https://mosaicbrowser.com/blog/warm-up-bot-signature): Our warm-up robot scrolled with window.scrollBy, so pages saw isTrusted false and a scroll with no wheel. How a page detects that, and what we changed.

[How to use Playwright with an anti-detect browser](https://mosaicbrowser.com/guides/use-playwright-with-an-anti-detect-browser): Use Playwright with an anti-detect browser by starting the profile first and attaching with connectOverCDP. Keep its context; leave the fingerprint alone.

[Anti-detect browser REST API: profiles from your scripts](https://mosaicbrowser.com/features/rest-api): Mosaic's anti-detect browser REST API runs on 127.0.0.1 with API keys. Create and start profiles, then attach Puppeteer or Playwright over CDP.



[Is Camoufox still maintained? Reading the repository](https://mosaicbrowser.com/blog/is-camoufox-maintained): Is Camoufox still maintained? Its repository answers better than forums do: compare the latest release's date and Firefox base with today's Firefox.

[Junglefox and Chroma, Kameleo's two engines](https://mosaicbrowser.com/blog/kameleo-junglefox): Junglefox is Kameleo's Firefox-based engine, beside Chroma, its Chromium one. A Firefox profile needs an engine that really is Firefox, and keeping one is work.



## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
