Browser Agent Switcher for Chrome’s UA Reduction: Deterministic User-Agent and Client Hints for Auto-Agent AI Browsers
Chrome’s User-Agent (UA) reduction fundamentally changes how servers identify browsers. Instead of stuffing ever-growing detail into a single UA string, Chromium is freezing and smoothing the UA while shifting richer details into the Sec-CH-UA family of headers and the navigator.userAgentData API. That’s a good thing for privacy and the web’s long-term stability — but it breaks naive UA spoofers and requires a smarter approach if you’re building automation, testing frameworks, or “Auto-Agent” AI browsers that need deterministic personas.
This article is a practical blueprint for building a modern, ethical, deterministic UA switcher that:
- Aligns the UA string with Client Hints (CH) so the story is coherent across network and JS layers.
- Validates the setup via “what is my browser agent” pages and raw header inspection.
- Runs per-site content negotiation tests to catch breakage and drift.
- Minimizes fingerprint drift without engaging in deceptive or abusive impersonation.
- Enables deterministic replays in CI/CD and model training pipelines.
If you maintain headless browsers, scraping frameworks, QA tooling, or autonomous browsing agents, this guide gives you the code, patterns, and guardrails.
TL;DR
- Don’t just set navigator.userAgent. In Chromium-era Client Hints, you must set UA + Sec-CH-UA* + navigator.userAgentData consistently.
- Use Chrome DevTools Protocol (CDP) Emulation.setUserAgentOverride and userAgentMetadata to keep the network and JS layers aligned.
- Treat Safari and Firefox as distinct engines. If you’re running in Chromium, do not claim Safari while emitting Sec-CH headers; that’s instantly inconsistent.
- Build a “persona” schema that drives everything: UA string, CH brands/full versions, platform and mobile flag, architecture, and navigator.* properties.
- Validate against multiple inspector sites and raw headers (what is my browser + httpbin + browserleaks + client hints demos).
- For deterministic replays, pin the browser build, persona, GREASE brand seed, viewport, GPU flags, locale, and time.
1) Background: Chrome UA Reduction and Client Hints
Chromium’s UA Reduction effort freezes most UA string variability and encourages servers to use Client Hints ( Sec-CH-UA family) instead of heuristic UA sniffing. The goals are:
- Reduce passive fingerprinting surface.
- Discourage fragile UA parsing.
- Provide a structured, feature-gated way to request high-entropy information.
Key concepts:
- Low-entropy hints (sent by default): Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform.
- High-entropy hints (require Accept-CH or permissions policy): Sec-CH-UA-Full-Version-List, Sec-CH-UA-Platform-Version, Sec-CH-UA-Arch, Sec-CH-UA-Model, Sec-CH-UA-Bitness.
- navigator.userAgentData mirrors these in JS. getHighEntropyValues returns a promise with the richer fields if the server has opted in (or if you’re injecting for testing).
- GREASE brands: Chromium injects a fake brand (for example, "Not A;Brand") to prevent hard-coding brand logic.
- Accept-CH and Critical-CH headers: Servers ask for hints and can mark some hints as critical to get them on the first request.
- Vary: Servers must include appropriate Vary headers (for example, Vary: Sec-CH-UA, Sec-CH-UA-Platform) to keep caches correct.
References for deeper reading:
- web.dev: User-Agent Client Hints
- Chromium Blog: User-Agent Reduction updates
- WICG UA-CH explainer
- MDN: User-Agent Client Hints
The important takeaway: a modern UA switcher must control both the legacy UA string and the CH surfaces coherently.
2) Designing a Persona Schema
Treat a browser identity as a declarative persona, not a single string. A good persona schema is explicit, self-contained, and deterministic. Here’s a practical JSON shape:
json{ "label": "chrome-120-windows10-desktop", "engine": "chromium", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.109 Safari/537.36", "userAgentMetadata": { "brands": [ { "brand": "Chromium", "version": "120" }, { "brand": "Google Chrome", "version": "120" }, { "brand": "Not A;Brand", "version": "8" } ], "fullVersionList": [ { "brand": "Chromium", "version": "120.0.6099.109" }, { "brand": "Google Chrome", "version": "120.0.6099.109" }, { "brand": "Not A;Brand", "version": "8.0.0.0" } ], "platform": "Windows", "platformVersion": "10.0.0", "architecture": "x86", "model": "", "mobile": false, "bitness": "64" }, "navigator": { "platform": "Win32", "vendor": "Google Inc.", "hardwareConcurrency": 8, "maxTouchPoints": 0, "languages": ["en-US", "en"], "deviceMemory": 8 }, "viewport": { "width": 1366, "height": 768, "deviceScaleFactor": 1 }, "timezone": "America/Los_Angeles", "locale": "en-US", "greaseSeed": "persona:chrome-120-windows10-desktop", "notes": "Use only in Chromium-based agents." }
Guidelines:
- Keep UA and CH in sync for brand names, major versions, and platform.
- For Edge personas, include an "Edg/" token in UA and a Microsoft Edge brand in CH brands/fullVersionList. For Chrome personas, use "Google Chrome".
- If you’re on mobile/Android, set mobile: true, adjust UA tokens (Android; Mobile), and supply a realistic model if needed.
- For Safari or Firefox personas: Do not emit Sec-CH-UA headers or userAgentData, because those are Chromium constructs. You need a WebKit or Gecko runtime for fidelity.
A persona is your single source of truth for both network and JS layers. That makes deterministic replay tractable.
3) Aligning the UA String with Sec-CH-UA Hints
Consistency rules worth codifying into your UA-switcher:
- UA brand mapping:
- UA string product (Chrome/120.0...) → CH brands include Chromium and Google Chrome with matching major and full versions.
- Include a GREASE brand. Keep its content deterministic if you want stable replays.
- Platform mapping:
- UA platform token (Windows NT 10.0; Win64; x64) ↔ Sec-CH-UA-Platform: "Windows"; Sec-CH-UA-Platform-Version: "10.0.0"; Sec-CH-UA-Bitness: "64"; Sec-CH-UA-Arch: "x86"; navigator.platform: "Win32".
- Mobile mapping:
- If UA includes Mobile tokens (Android; Mobile), set Sec-CH-UA-Mobile: ?1 and navigator.maxTouchPoints accordingly.
- Version mapping:
- UA Chrome/x.y.z → CH fullVersionList reported should match exact full version for brand entries.
- Sec-CH-UA (low entropy) uses major versions only; Sec-CH-UA-Full-Version-List adds full version.
- Vendor mapping:
- navigator.vendor should be "Google Inc." for Chromium-based (including Edge). This is a common pitfall when trying to mimic non-Chromium engines.
The drift danger: If your UA claims Chrome/120 on Windows 10, but your CH says platform "Android" or your userAgentData brands omit Chrome, detection scripts will flag it. Always pass a cohesion check before you hit production.
4) Implementing a Modern UA Switcher in Chromium via CDP
The safest way to set UAs and CHs in Chromium automation is through the Chrome DevTools Protocol (CDP), which supports setting the UA and userAgentMetadata consistently.
Puppeteer example (Node.js)
jsimport puppeteer from 'puppeteer'; const persona = { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.109 Safari/537.36', userAgentMetadata: { brands: [ { brand: 'Chromium', version: '120' }, { brand: 'Google Chrome', version: '120' }, { brand: 'Not A;Brand', version: '8' } ], fullVersionList: [ { brand: 'Chromium', version: '120.0.6099.109' }, { brand: 'Google Chrome', version: '120.0.6099.109' }, { brand: 'Not A;Brand', version: '8.0.0.0' } ], platform: 'Windows', platformVersion: '10.0.0', architecture: 'x86', model: '', mobile: false, bitness: '64' } }; (async () => { const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] }); const page = await browser.newPage(); const client = await page.target().createCDPSession(); await client.send('Emulation.setUserAgentOverride', { userAgent: persona.userAgent, userAgentMetadata: persona.userAgentMetadata }); // Optional: emulate viewport/locale/timezone for extra determinism await page.setViewport({ width: 1366, height: 768, deviceScaleFactor: 1 }); await page.emulateTimezone('America/Los_Angeles'); await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US,en;q=0.9' }); await page.goto('https://httpbin.org/headers', { waitUntil: 'domcontentloaded' }); console.log(await page.content()); await browser.close(); })();
Notes:
- Emulation.setUserAgentOverride is the key to aligning both network headers and navigator.* data in Chromium.
- If you only call page.setUserAgent, you’ll set the network UA but not CH. Always prefer CDP for modern switching.
Playwright (Node.js) using CDP
jsimport { chromium } from 'playwright'; const persona = { /* same as above */ }; (async () => { const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ userAgent: persona.userAgent, viewport: { width: 1366, height: 768 }, locale: 'en-US' }); const page = await context.newPage(); // CDP session to set userAgentMetadata const client = await context.newCDPSession(page); await client.send('Emulation.setUserAgentOverride', { userAgent: persona.userAgent, userAgentMetadata: persona.userAgentMetadata }); await page.goto('https://browserleaks.com/client-hints'); // Add assertions/extractions as needed await browser.close(); })();
Playwright (Python) + CDP
pythonfrom playwright.sync_api import sync_playwright persona = { "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.109 Safari/537.36", "userAgentMetadata": { "brands": [ {"brand": "Chromium", "version": "120"}, {"brand": "Google Chrome", "version": "120"}, {"brand": "Not A;Brand", "version": "8"} ], "fullVersionList": [ {"brand": "Chromium", "version": "120.0.6099.109"}, {"brand": "Google Chrome", "version": "120.0.6099.109"}, {"brand": "Not A;Brand", "version": "8.0.0.0"} ], "platform": "Windows", "platformVersion": "10.0.0", "architecture": "x86", "model": "", "mobile": False, "bitness": "64" } } with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context(user_agent=persona["userAgent"], viewport={"width":1366,"height":768}, locale="en-US") page = context.new_page() client = context.new_cdp_session(page) client.send('Emulation.setUserAgentOverride', { 'userAgent': persona['userAgent'], 'userAgentMetadata': persona['userAgentMetadata'] }) page.goto('https://httpbin.org/headers') print(page.text_content('pre')) browser.close()
About directly setting Sec-CH-UA headers
You can inject headers manually at the proxy or request layer (for example, curl, Node fetch). That’s useful for server-side content tests, but it does not modify the JS environment (navigator.userAgentData). If your system runs a real browser, prefer CDP to avoid drift between network and JS.
For raw request experiments:
bashcurl -s https://httpbin.org/headers \ -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.109 Safari/537.36' \ -H 'Sec-CH-UA: "Chromium";v="120", "Google Chrome";v="120", ";Not A Brand";v="8"' \ -H 'Sec-CH-UA-Mobile: ?0' \ -H 'Sec-CH-UA-Platform: "Windows"' \ -H 'Sec-CH-UA-Platform-Version: "10.0.0"' \ -H 'Sec-CH-UA-Full-Version-List: "Chromium";v="120.0.6099.109", "Google Chrome";v="120.0.6099.109", ";Not A Brand";v="8.0.0.0"' \ -H 'Sec-CH-UA-Arch: "x86"' \ -H 'Sec-CH-UA-Bitness: "64"'
Be aware: In normal operation, browsers only send high-entropy hints after Accept-CH (or Critical-CH) opt-in from the server. For testing and fuzzing, forced headers are fine as long as you understand this diverges from real client behavior.
5) Validating with “What Is My Browser Agent” and Raw Headers
Do not trust a single inspector page. Validate on at least three independent surfaces:
- Raw headers (httpbin.org/headers, your own echo server).
- Client hints inspector pages (browserleaks.com/client-hints, whatismybrowser.com/detect/client-hints).
- UA string detectors (whatismybrowser.com/detect/what-is-my-user-agent).
- JS environment dumps (navigator.userAgent, navigator.userAgentData.brands, getHighEntropyValues output, navigator.platform, vendor, languages).
Automation snippet to verify alignment in-page:
jsasync function validatePersona(page) { const jsData = await page.evaluate(async () => { const ua = navigator.userAgent; const hasUach = !!navigator.userAgentData; let lowEntropy = null, highEntropy = null; if (hasUach) { lowEntropy = { brands: navigator.userAgentData.brands, mobile: navigator.userAgentData.mobile, platform: navigator.userAgentData.platform }; try { highEntropy = await navigator.userAgentData.getHighEntropyValues([ 'architecture','bitness','model','platformVersion','uaFullVersion' ]); } catch (e) { highEntropy = { error: String(e) }; } } return { ua, platform: navigator.platform, vendor: navigator.vendor, languages: navigator.languages, hasUach, lowEntropy, highEntropy }; }); console.log('Navigator dump:', jsData); }
Checklist:
- UA major version equals CH brands major version.
- UA OS/platform tokens match Sec-CH-UA-Platform and platformVersion.
- UA Mobile tokens match Sec-CH-UA-Mobile.
- navigator.vendor aligns with engine persona.
- If you are pretending to be a non-Chromium browser, navigator.userAgentData should not exist and Sec-CH-UA* headers should not be sent.
6) Per-Site Content Negotiation Tests
Many sites still branch behavior on UA or CH. Your switcher should detect where a persona change triggers response differences and ensure everything still works.
Approach:
- Choose a corpus of sites (including your own) and make paired requests with different personas.
- Capture:
- Response status, headers (especially Vary, Accept-CH, Critical-CH, Set-Cookie).
- Body signature (hash) and optionally DOM-level diffs if using a real browser.
- Watch for:
- Accept-CH presence, indicating server expects CH on subsequent requests.
- Vary: Sec-CH-UA-* correctness to avoid cache poisoning.
- Redirects and content switches (mobile vs desktop variants).
Node.js example using Playwright to diff responses:
jsimport { chromium } from 'playwright'; import crypto from 'node:crypto'; const personas = [personaDesktop, personaAndroid]; // define two personas const targets = [ 'https://www.wikipedia.org/', 'https://httpbin.org/headers', 'https://browserleaks.com/client-hints' ]; function hash(s) { return crypto.createHash('sha256').update(s).digest('hex'); } async function fetchWithPersona(url, persona) { const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ userAgent: persona.userAgent, viewport: { width: 1366, height: 768 } }); const page = await context.newPage(); const client = await context.newCDPSession(page); await client.send('Emulation.setUserAgentOverride', { userAgent: persona.userAgent, userAgentMetadata: persona.userAgentMetadata }); await page.goto(url, { waitUntil: 'domcontentloaded' }); const body = await page.content(); const headers = await page.evaluate(() => Object.fromEntries([...performance.getEntriesByType('navigation')][0].toJSON().responseHeaders || [])); // Fallback: fetch via page.evaluate or intercepting network for full headers in real-world setup. await browser.close(); return { bodyHash: hash(body), headers }; } (async () => { for (const url of targets) { const results = []; for (const p of personas) results.push({ persona: p.label, ...(await fetchWithPersona(url, p)) }); console.log(url, results); // Example: compare body hashes if (results.length === 2 && results[0].bodyHash !== results[1].bodyHash) { console.log(`Content differs for ${url} between ${results[0].persona} and ${results[1].persona}`); } } })();
For lower-level HTTP-only tests (no JS env), use fetch/undici or curl with handcrafted headers. Just remember: CH-driven behaviors often appear after Accept-CH on the first response. To accurately simulate, you may need a two-step request: first request sees Accept-CH, second request includes high-entropy hints.
7) Minimizing Fingerprint Drift Ethically
“Fingerprint drift” occurs when surface A claims one identity while surface B claims another. Drift is the biggest red flag for bot detection and also the biggest cause of brittle automations.
Surfaces to align:
- Network headers: User-Agent, Sec-CH-UA*, Accept-Language.
- JS environment: navigator.userAgent, navigator.userAgentData, navigator.platform, navigator.vendor, navigator.languages, maxTouchPoints, hardwareConcurrency.
- Rendering/OS signals: viewport/scale, timezone, media queries (prefers-color-scheme), GPU vendor/renderer (if relevant), platform-specific APIs.
Practical guardrails:
- Prefer engine fidelity: If you must present as Safari or Firefox, use a WebKit or Gecko runtime. Do not pretend to be Safari in a Chromium engine and emit Sec-CH-UA.* That’s a dead giveaway.
- Keep CH brand lists realistic: For Chrome, include Chromium + Google Chrome + GREASE; for Edge, Chromium + Microsoft Edge + GREASE. Exact brand strings matter.
- Stabilize GREASE deterministically: Chrome rotates GREASE brand and order; if you need determinism, generate a stable GREASE token using a persona-seeded PRNG. Keep punctuation realistic (e.g., "Not A;Brand").
- Accept-CH handshake: Understand that real clients only send high-entropy hints after server opt-in. Your switcher can emulate both phases for tests. For production automation, don’t force hints that a real client wouldn’t send.
- Keep patch versions coherent: If your CH reports fullVersionList 120.0.6099.109, your UA should match that patch. Pinning the browser version in CI helps.
- Languages/timezone: For a US Windows persona, send Accept-Language: en-US,en;q=0.9 and set navigator.languages accordingly; emulate a US timezone.
- Mobile signals: Don’t forget the small stuff: viewport width, deviceScaleFactor, touch support, and Sec-CH-UA-Mobile must align.
Ethics:
- Don’t impersonate sensitive agents (for example, Googlebot, security scanners) or mislead origin servers. Many sites forbid such impersonation in ToS.
- If you run automated agents against third-party sites, respect robots.txt, rate limits, and legal constraints.
- Transparency helps: add an X-Auto-Agent header or From header with contact info where appropriate.
8) Deterministic Replay for CI/CD and Model Training
Reproducibility is life. An Auto-Agent AI browser that behaves differently on every run is impossible to debug and poisons training data. Deterministic replay means pinning the entire browsing persona and environment so that requests, responses, and JS behavior are stable enough for comparison.
Checklist for deterministic replay:
- Pin the browser build: Use a fixed Chromium/Chrome version (for example, Playwright’s docker images). Disable auto-update.
- Pin the persona: UA string, CH metadata, navigator.* fields, viewport, languages, timezone, color scheme.
- Pin the GREASE brand: Use a deterministic seed to generate the GREASE brand string and order.
- Freeze time: Set a fixed time/timezone offset if needed; beware of server-side anti-replay if you freeze too much.
- Pin GPU/CPU env: Run in a container/VM with static GPU flags (for example, --use-angle=swiftshader) to avoid renderer drift.
- Cache the Accept-CH phase: For test replays, record initial Accept-CH and Vary headers and enforce the same hint set on subsequent requests.
- Record HARs and headers: Store both request and response headers, including CH, for auditing.
Example: HAR recorder with CH awareness
- During a recording session, store:
- The persona JSON
- Every request’s headers and the final header set after redirects
- Response headers (to track Accept-CH/Vary)
- During replay:
- Load the exact persona
- Apply Emulation.setUserAgentOverride with matching metadata
- For each request, ensure that any high-entropy hints initially seen are sent again so the server follows the same path
Pseudocode for deterministic GREASE brand generation:
jsimport crypto from 'node:crypto'; function deterministicGrease(seed) { const hash = crypto.createHash('sha256').update(seed).digest(); const punctuations = [';', ':', ')', '(', '?', '/', ',']; const p = punctuations[hash[0] % punctuations.length]; // Keep it plausible and stable return { brand: `Not ${p}A${p}Brand`, version: `${8 + (hash[1] % 5)}` }; } function buildBrands(baseBrands, seed) { const grease = deterministicGrease(seed); const list = [...baseBrands, grease]; // Stable order using hash const order = (hashByte) => (a, b) => (a.brand.charCodeAt(0) ^ hashByte) - (b.brand.charCodeAt(0) ^ hashByte); return list.sort(order(base64Byte(seed))); } function base64Byte(s) { return Buffer.from(s).toString('base64').charCodeAt(0); }
You don’t have to replicate Chromium’s exact GREASE algorithm; stability is more important than mimicry for CI. Just keep a plausible GREASE entry present.
9) Multi-Engine Considerations
- Chromium personas: Full UA-CH support; use CDP for fidelity. Great for Chrome, Edge, Brave, Opera flavors.
- Firefox personas: No UA-CH (as of late 2024). UA string + navigator.* are the primary surfaces. If you run Chromium and pretend to be Firefox, servers will notice because of UA-CH presence and other engine traits.
- Safari personas: WebKit does not implement UA-CH. Correct Safari emulation requires WebKit + WebDriver (or Safari Technology Preview) and attention to details like navigator.vendor, platform tokens, and WebKit versioning.
If your product must support multiple engines, implement an adapter layer per engine rather than one-size-fits-all switching. Engine fidelity trumps superficial UA spoofing.
10) An Opinionated Minimal Contract for a UA Switcher Library
Expose a small, explicit API:
- loadPersona(persona): Validates coherence and applies it to the engine (CDP or WebDriver), including UA, CH metadata, navigator.*, viewport, and locale.
- validateCurrentPersona(): Runs the in-page and network-level checks described above and returns a signed report.
- withPersona(persona, fn): Applies persona, runs a function (test or crawl), resets on exit.
- diffResponses(url, personas): Compares response headers/body hashes and flags Accept-CH/Vary differences.
Fail fast on incoherence. For example, throw if persona.engine === 'chromium' but persona.userAgent includes 'Safari' without 'Chrome' tokens. Provide a lint mode that tells developers exactly what to fix.
11) Example Personas
Desktop Chrome 120 on Windows 10 64-bit:
json{ "label": "chrome-120-windows10-desktop", "engine": "chromium", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.109 Safari/537.36", "userAgentMetadata": { "brands": [ { "brand": "Chromium", "version": "120" }, { "brand": "Google Chrome", "version": "120" }, { "brand": "Not A;Brand", "version": "8" } ], "fullVersionList": [ { "brand": "Chromium", "version": "120.0.6099.109" }, { "brand": "Google Chrome", "version": "120.0.6099.109" }, { "brand": "Not A;Brand", "version": "8.0.0.0" } ], "platform": "Windows", "platformVersion": "10.0.0", "architecture": "x86", "model": "", "mobile": false, "bitness": "64" }, "navigator": { "platform": "Win32", "vendor": "Google Inc.", "languages": ["en-US","en"], "hardwareConcurrency": 8, "maxTouchPoints": 0 }, "viewport": { "width": 1366, "height": 768, "deviceScaleFactor": 1 }, "timezone": "America/Los_Angeles", "locale": "en-US" }
Mobile Chrome 120 on Android 13, Pixel 7 approximation:
json{ "label": "chrome-120-android13-pixel7", "engine": "chromium", "userAgent": "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.109 Mobile Safari/537.36", "userAgentMetadata": { "brands": [ { "brand": "Chromium", "version": "120" }, { "brand": "Google Chrome", "version": "120" }, { "brand": "Not A;Brand", "version": "8" } ], "fullVersionList": [ { "brand": "Chromium", "version": "120.0.6099.109" }, { "brand": "Google Chrome", "version": "120.0.6099.109" }, { "brand": "Not A;Brand", "version": "8.0.0.0" } ], "platform": "Android", "platformVersion": "13", "architecture": "arm", "model": "Pixel 7", "mobile": true, "bitness": "64" }, "navigator": { "platform": "Linux armv8l", "vendor": "Google Inc.", "languages": ["en-US","en"], "hardwareConcurrency": 8, "maxTouchPoints": 5 }, "viewport": { "width": 390, "height": 844, "deviceScaleFactor": 2.625 }, "timezone": "America/Los_Angeles", "locale": "en-US" }
Edge on Windows 11 example tweaks:
- UA includes "Edg/120.0.x" token.
- CH brands include "Microsoft Edge" instead of "Google Chrome" (still keep "Chromium" and GREASE).
- navigator.vendor remains "Google Inc." because Edge inherits this from Chromium.
12) Common Pitfalls and How to Avoid Them
- Only changing page.setUserAgent: CH remains unchanged, navigator.userAgentData still reports real values → drift.
- Omitting GREASE brand: Some servers expect a GREASE entry; lack of it can make your CH look synthetic.
- Claiming Safari in Chromium: Sec-CH-UA presence + JS engine quirks make it obvious.
- Mismatched versioning: UA shows 120.0.6099.109 while CH fullVersionList shows a different patch.
- Forgetting Accept-CH handshake: You won’t see high-entropy hints server-side on first request; tests need multi-step flows or forced headers.
- Missing Vary on server: If you also run servers, always include Vary: Sec-CH-UA-*, otherwise caches serve wrong variants.
13) CI/CD Integration
- Use pinned browser builds. Playwright’s Docker images are ideal: mcr.microsoft.com/playwright. Choose a tag that locks Chromium.
- Run validation jobs that:
- Load persona → Apply via CDP → Visit header echo + hints inspector → Assert coherence.
- Diff responses across personas for a canary set of URLs.
- Store artifacts: raw headers, navigator dumps, screenshots, and body hashes.
- Gate merges on zero drift and stable content diffs.
Example GitHub Actions pseudo-config:
yamljobs: ua-switcher-ci: runs-on: ubuntu-latest container: mcr.microsoft.com/playwright:v1.40.0-focal steps: - uses: actions/checkout@v4 - run: npm ci - run: node ./scripts/validate-personas.js
14) Training Auto-Agent Models with Stable Personas
When collecting browsing trajectories for training, each trajectory should include:
- Persona ID and full persona JSON
- Browser build fingerprint (Chromium revision)
- Request/response headers for the Accept-CH handshake
- JS environment dump at start of session
- Deterministic seeds (GREASE, viewport randomization if any, but preferably none)
For reproducible re-simulation:
- Rehydrate persona and browser build in a container
- Preload Accept-CH outcomes from the original session (or re-request the first hop and ensure identical hints)
- Fix time/timezone
- Replay with the same network conditions (latency, bandwidth) for timing-sensitive sites
This ensures your model learns behaviors that generalize, not artifacts from a drifting identity.
15) Ethical Scope and Responsible Use
A modern UA switcher is a power tool. Use it responsibly:
- Don’t impersonate crawlers or identities that grant special access.
- If you test content negotiation on third-party sites, rate-limit and cache to minimize load.
- Respect legal and contractual constraints (robots.txt, ToS, data protection).
- Prefer transparency when feasible (for example, an X-Auto-Agent header or clear User-Agent comment for your own services).
Reducing drift and aligning surfaces isn’t about evading detection; it’s about engineering accuracy and reproducibility.
16) Quick Reference: What to Align
- UA string:
- Chrome/Edge version
- OS tokens (Windows NT, Android)
- Mobile tokens
- Sec-CH-UA family:
- Sec-CH-UA (brands + major versions)
- Sec-CH-UA-Mobile
- Sec-CH-UA-Platform
- Sec-CH-UA-Full-Version-List (opt-in)
- Sec-CH-UA-Platform-Version (opt-in)
- Sec-CH-UA-Arch (opt-in)
- Sec-CH-UA-Model (opt-in)
- Sec-CH-UA-Bitness (opt-in)
- Navigator fields:
- navigator.userAgent
- navigator.userAgentData (brands, mobile, platform, high-entropy values)
- navigator.platform
- navigator.vendor
- navigator.languages
- maxTouchPoints, hardwareConcurrency
- Environment:
- Viewport + deviceScaleFactor
- Timezone + locale
- Accept-Language
- GPU flags if relevant
17) Conclusion
A UA switcher that only changes the User-Agent header is obsolete in a UA-CH world. For Auto-Agent AI browsers and serious test frameworks, you need a persona-driven approach that aligns the UA string, the Sec-CH-UA family, and the JS environment. Implement it via CDP in Chromium, validate across multiple inspectors, and run per-site negotiation tests to catch surprises. Pin everything in CI/CD for deterministic replay, and operate within ethical boundaries.
Get the basics right — and your automation will be both more robust and more respectful of the modern web’s privacy and compatibility constraints.
18) Further Reading and Tools
- web.dev: User-Agent Client Hints
- Chromium Blog: User-Agent Reduction series
- WICG/UA-CH explainer and draft spec
- MDN: User-Agent Client Hints
- Browser inspectors:
- Playwright and Puppeteer docs
- Chrome DevTools Protocol: Emulation.setUserAgentOverride
By embracing a deterministic, persona-first design and aligning UA with CH, you can build a future-proof UA switcher for Chrome’s UA Reduction era.
