How to Configure the Operating System User Agent in Headless Chrome
Set a custom OS identity in Headless Chrome with Puppeteer, Client Hints, DevTools, troubleshooting, and reliable validation steps.
Direct answer: In Puppeteer, set the page user agent with page.setUserAgent(). If your test depends on User-Agent Client Hints or JavaScript-visible platform data, provide matching platform and userAgentMetadata values too. This changes the identity reported to websites; it does not change Chrome’s internal behavior or the operating system running Chrome.
Current Chrome uses unified Headless mode, selected with headless: true. Chrome 132 and later keep the older implementation as a separate chrome-headless-shell binary; Puppeteer selects that with headless: 'shell'. State the Chrome and Puppeteer versions and the headless mode in reproducible tests.
Set the user agent with Puppeteer
Install Puppeteer, launch current unified Headless Chrome, set the identity before navigation, and verify both the request header and page-visible values.
npm install puppeteer
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: true
});
const page = await browser.newPage();
await page.setUserAgent({
userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
platform: 'Windows',
userAgentMetadata: {
brands: [
{ brand: 'Chromium', version: '145' },
{ brand: 'Google Chrome', version: '145' },
{ brand: 'Not A(Brand', version: '99' }
],
fullVersion: '145.0.0.0',
platform: 'Windows',
platformVersion: '10.0.0',
architecture: 'x86',
model: '',
mobile: false
}
});
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
const identity = await page.evaluate(() => ({
userAgent: navigator.userAgent,
platform: navigator.platform,
userAgentData: navigator.userAgentData
? {
brands: navigator.userAgentData.brands,
platform: navigator.userAgentData.platform,
mobile: navigator.userAgentData.mobile
}
: null
}));
console.log(identity);
await browser.close();
})();
See the current Puppeteer Page.setUserAgent API reference for the installed version. Some older releases use the positional form, such as page.setUserAgent(userAgent, userAgentMetadata); check your version before copying an example between projects.
Choose the identity surfaces your test needs
| Surface | What it affects | How to validate |
|---|---|---|
| Legacy User-Agent string | The User-Agent request header and navigator.userAgent |
Inspect server logs, DevTools Network, and navigator.userAgent |
platform |
Puppeteer’s platform value and related JavaScript-visible identity | Check navigator.platform and the API result |
| User-Agent Client Hints | Sec-CH-UA headers and navigator.userAgentData |
Inspect request headers and call navigator.userAgentData |
| Actual operating system | Native fonts, graphics, input, filesystem, and OS behavior | Run the browser on the target OS or device |
A UA string alone does not synchronize every identity surface. Chrome recommends Client Hints for modern browser identification, so set and validate the metadata required by the application under test.
Use a minimal override when Client Hints do not matter
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setUserAgent(
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36'
);
await page.goto('https://example.com');
This is sufficient for sites that only branch on the legacy string. It is not sufficient when the site reads Client Hints, navigator.userAgentData, or platform-specific APIs.
Override the user agent manually in DevTools
- Open Chrome DevTools and select the Network conditions panel.
- Clear Use browser default under User agent.
- Enter the complete UA string required by the test.
- Edit the related User-Agent Client Hints when the test depends on them.
- Reload the page and inspect the request headers and JavaScript-visible values.
DevTools changes are useful for one-off compatibility checks. They are not a substitute for a versioned automation script when the result must be reproducible.
Headless mode and Chrome version details
Use headless: true for current unified Headless Chrome. Puppeteer’s headless: 'shell' selects Headless Shell when that separate binary is installed. Chrome 112 introduced the updated implementation, and Chrome 132 made the old implementation available only as chrome-headless-shell. The Chrome documentation describes the result as: “Chrome now has unified Headless and headful modes.”
// Unified Headless Chrome
const browser = await puppeteer.launch({ headless: true });
// Separate Headless Shell, when your installation provides it
const shellBrowser = await puppeteer.launch({ headless: 'shell' });
Do not assume a launch flag is a portable operating-system override. Configure the page with setUserAgent, then verify the resulting request and page values.
Validate the override end to end
- Record the Chrome version, Puppeteer version, headless mode, UA string, platform, and metadata used.
- Capture the outgoing
User-AgentandSec-CH-UA*headers with DevTools Protocol logging or a test endpoint you control. - Evaluate
navigator.userAgent,navigator.platform, andnavigator.userAgentDatain the page. - Test redirects and subresources because a redirect target can apply different Client Hint policies.
- Compare behavior with a real browser on the target OS when native behavior matters.
page.on('request', request => {
if (request.isNavigationRequest() && request.frame() === page.mainFrame()) {
console.log(request.url(), request.headers());
}
});
Common errors and fixes
| Symptom | Cause | Fix |
|---|---|---|
| The server still sees HeadlessChrome | The override ran after navigation or on a different page | Create the page, call setUserAgent, then navigate. Apply it to every new page. |
navigator.userAgent changed but feature detection did not |
The site uses Client Hints or platform APIs | Set consistent platform and userAgentMetadata, then verify navigator.userAgentData. |
| Client Hint headers are missing | The origin has not requested hints, or the browser did not receive an Accept-CH response |
Check the origin’s hint policy and inspect a subsequent request. Do not infer header behavior from the legacy UA string. |
| Rendering still looks Linux or container-based | A UA override does not change fonts, graphics, sandboxing, or OS APIs | Run on the target OS or use an environment that provides the required native behavior. |
setUserAgent options are rejected |
Puppeteer API differences between versions | Read the API reference matching the installed version and use its current options form. |
| Headless and headed results differ | Different Chrome modes, versions, flags, fonts, or extensions | Pin the same Chrome version and record the selected headless mode; validate both identity and rendering. |
Performance and reliability considerations
- Set the UA once per page before navigation instead of changing it repeatedly during a flow.
- Reuse a browser process for multiple pages, while applying the override to each newly created page.
- Keep the UA, Client Hint versions, and platform internally consistent. Contradictory values can trigger compatibility branches or bot defenses.
- Use explicit waits for the page behavior you are testing. A UA override does not make a slow page complete sooner.
- Pin Chrome and Puppeteer versions in CI. Headless implementation and API behavior can change across releases.
- Do not use this technique as proof that an application works on a real operating system. Native fonts, graphics, input, and platform APIs still require the actual target environment.
Automated screenshot alternative
If your goal is a consistent rendered image rather than browser identity testing, ScreenshotNeo accepts the URL and capture settings through one API request. Its options include custom user agent, headers, cookies, timezone, geolocation, viewport, device presets, JavaScript, CSS, waits, blocking rules, element capture, full-page capture, and PDF output. See the ScreenshotNeo API documentation for the complete parameter list.
Or skip the browser setup
Use this request when you need a screenshot and do not want to maintain Chrome installation, launch flags, or page orchestration:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with 1,000 screenshots per month at no charge.
FAQ
Does changing the UA change the operating system?
No. It changes the browser identity reported to servers and scripts. Chrome still runs with the host OS’s fonts, graphics, APIs, and other internal behavior.
Should I set Client Hints every time?
Set them when the application reads Client Hints or navigator.userAgentData. A legacy UA-only test can use the string form.
Which headless mode should new tests use?
Use headless: true for current unified Headless Chrome. Use headless: 'shell' only when you intentionally run the separate Headless Shell binary.
Can a UA override replace cross-OS testing?
No. Run on the target OS when native platform behavior is part of the requirement.
Why do two sites receive different identity data?
Sites can request different Client Hints and apply different detection logic. Inspect the actual headers and JavaScript values for each origin.


