User-Agent and Client Hints (UA-CH): Complete Control Guide
How User-Agent strings, Client Hints headers, and navigator.userAgentData work together, and how to manage them consistently at the engine level.
Introduction
The User-Agent string has been a cornerstone of browser identification for decades. But the modern browser identity system is more complex. Client Hints (UA-CH) provide structured data about the browser through both HTTP headers and JavaScript APIs. The Sec-CH-UA header, navigator.userAgentData, and getHighEntropyValues() all expose brand, platform, version, and device information in a standardized format. When these values do not align with the traditional User-Agent string, the inconsistency creates a distinct tracking signal.
BotBrowser manages all User-Agent and Client Hints signals through profiles and CLI overrides. Profiles set the baseline automatically, and CLI flags provide runtime control for custom identities. This article explains how UA and Client Hints work together, what signals matter, and how to configure them correctly.
Privacy Impact
User-Agent and Client Hints are among the first signals a server receives. They arrive with the initial HTTP request, before any JavaScript runs. This makes them a primary input for server-side fingerprinting and tracking.
The privacy risks of mismatched UA/Client Hints include:
- Server-side inconsistency detection: Servers compare the
User-Agentheader withSec-CH-UAheaders on the same request. A mismatch indicates modification. - JavaScript-to-header comparison: Pages can compare
navigator.userAgentwithnavigator.userAgentData.brandsand check for alignment. Any discrepancy is flagged. - High-entropy value correlation:
getHighEntropyValues()returns detailed platform, architecture, and version information. These must match the broader identity. - Cross-context consistency: UA values must be consistent across the main thread, web workers, service workers, and HTTP headers. Partial overrides that only affect one context create detectable gaps.
BotBrowser ensures consistency across all of these surfaces because it controls UA and Client Hints at the engine level, before any request is sent or any JavaScript executes.
Technical Background
The User-Agent String
The traditional User-Agent header is a long string that identifies the browser, version, OS, and rendering engine:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.7444.60 Safari/537.36
This string is sent with every HTTP request and is available through navigator.userAgent in JavaScript. While browsers are gradually reducing the information in the UA string (Chrome's UA Reduction), it remains a significant identity signal.
Client Hints (UA-CH)
Client Hints provide the same information in a structured format through HTTP headers and JavaScript APIs:
Default headers (sent with every request):
Sec-CH-UA: Brand tokens with major versions. Example:"Chromium";v="142", "Google Chrome";v="142", "Not:A-Brand";v="99"Sec-CH-UA-Mobile: Whether the device is mobile. Example:?0Sec-CH-UA-Platform: Operating system. Example:"Windows"
High-entropy headers (sent only when the server requests them via Accept-CH):
Sec-CH-UA-Full-Version-List: Full version strings for all brandsSec-CH-UA-Platform-Version: OS version (e.g.,"15.0.0"for Windows 11)Sec-CH-UA-Arch: CPU architecture (e.g.,"x86")Sec-CH-UA-Bitness: System bitness (e.g.,"64")Sec-CH-UA-Model: Device model (primarily for mobile)
JavaScript API (navigator.userAgentData):
// Low-entropy (always available, no permission needed)
navigator.userAgentData.brands // [{brand: "Chromium", version: "142"}, ...]
navigator.userAgentData.mobile // false
navigator.userAgentData.platform // "Windows"
// High-entropy (returns a Promise)
const data = await navigator.userAgentData.getHighEntropyValues([
'platformVersion', 'architecture', 'bitness',
'fullVersionList', 'model'
]);
GREASE Tokens
Chromium adds randomized "GREASE" (Generate Random Extensions And Sustain Extensibility) tokens to Client Hints. These are deliberately invalid brand names inserted to prevent servers from hard-coding assumptions about token formats. A typical Sec-CH-UA value includes one GREASE token:
"Not:A-Brand";v="99", "Chromium";v="142", "Google Chrome";v="142"
The GREASE token format, version, and position change between browser versions and brands. BotBrowser generates correct GREASE tokens for each profile and brand configuration.
Consistency Requirements
For a credible browser identity, the following must all align:
User-Agentheader matchesnavigator.userAgent- Brand tokens in
Sec-CH-UAmatchnavigator.userAgentData.brands - Platform in
Sec-CH-UA-Platformmatchesnavigator.userAgentData.platform - Major version in UA string matches the version in
Sec-CH-UAbrand tokens - Full versions in
Sec-CH-UA-Full-Version-ListmatchgetHighEntropyValues().fullVersionList - Platform version is realistic for the claimed OS (e.g., Windows 11 reports
"15.0.0"or higher) - Architecture and bitness match the platform (e.g.,
x86/64for Windows,arm/64for M-series Mac) - All values are the same in the main thread, workers, and HTTP headers
Common Approaches and Their Limitations
Framework-Level UA Override
Playwright and Puppeteer provide UA override options:
// Playwright
const context = await browser.newContext({
userAgent: 'Custom UA String'
});
// Puppeteer
await page.setUserAgent('Custom UA String');
These change navigator.userAgent and the User-Agent header but do not update:
Sec-CH-UAheaders (still reflect the real browser identity)navigator.userAgentData.brands(unchanged)getHighEntropyValues()(returns real values)- Worker-level UA (may not be consistent)
The mismatch between the overridden UA string and the unchanged Client Hints is easily detectable.
CDP Network.setUserAgentOverride
CDP provides Network.setUserAgentOverride with partial Client Hints support:
await cdpSession.send('Network.setUserAgentOverride', {
userAgent: 'Custom UA',
userAgentMetadata: {
brands: [...],
platform: 'Windows',
// ...
}
});
This is more comprehensive than framework-level overrides but still has gaps:
- Requires
Network.enable, which can affect fingerprinting - Must be set per target (page, worker)
- GREASE token generation is not automatic
- Version alignment is manual and error-prone
Manual Header Injection
Setting custom headers with page.setExtraHTTPHeaders() can override Sec-CH-UA headers, but the JavaScript APIs (navigator.userAgentData) still return the original values. This creates a mismatch between HTTP-level and JavaScript-level identity.
BotBrowser's Approach
Profile-Based Automatic Configuration
BotBrowser profiles contain complete UA and Client Hints configurations captured from real browser environments. When you load a profile, all signals align automatically:
chrome --bot-profile="/path/to/profile.enc"
The profile sets:
- User-Agent string (header and JavaScript)
- All default Client Hints headers
- All high-entropy Client Hints values
- navigator.userAgentData (brands, platform, mobile)
- GREASE tokens appropriate for the browser version
- Consistent values across main thread, workers, and HTTP headers
No additional flags are needed for standard UA/Client Hints configuration.
CLI Overrides for Custom Identities
When you need to build a custom identity beyond what the profile provides, BotBrowser offers granular CLI flags:
Core identity flags:
--bot-config-browser-brand=chrome|edge|brave|opera|webview(ENT Tier2): Switch the browser brand--bot-config-ua-full-version=142.0.7444.60(ENT Tier2): Set the Chromium full version--bot-config-brand-full-version=142.0.3595.65(ENT Tier2): Set the brand-specific version (for Edge, Opera, etc.)
Platform and device flags (ENT Tier3):
--bot-config-platform=Windows|Android|macOS|Linux: Platform name--bot-config-platform-version=13: OS version string--bot-config-model=SM-G991B: Device model (mobile)--bot-config-architecture=x86|arm|arm64: CPU architecture--bot-config-bitness=32|64: System bitness--bot-config-mobile=true|false: Mobile device flag
Custom User-Agent with placeholders:
The --user-agent flag supports placeholders that get replaced at runtime:
chrome --bot-profile="/path/to/profile.enc" \
--user-agent="Mozilla/5.0 (Linux; Android {platform-version}; {model}) AppleWebKit/537.36 Chrome/{ua-full-version} Mobile Safari/537.36" \
--bot-config-platform=Android \
--bot-config-platform-version=13 \
--bot-config-model=SM-G991B \
--bot-config-ua-full-version=142.0.7444.60
BotBrowser replaces {platform-version}, {model}, and {ua-full-version} with the corresponding flag values, then auto-generates matching navigator.userAgentData and all Client Hints headers.
Consistency Across All Surfaces
BotBrowser ensures UA/Client Hints consistency across:
- HTTP headers: All
User-AgentandSec-CH-UA-*headers on every request - Main thread JavaScript:
navigator.userAgent,navigator.userAgentData - Web Workers:
navigator.userAgentandnavigator.userAgentDatain worker contexts - Service Workers: Same values available in service worker scope
- High-entropy values:
getHighEntropyValues()returns values consistent with all other signals
Configuration and Usage
Standard Profile Usage
For most use cases, the profile handles everything:
chrome --bot-profile="/path/to/profile.enc"
Brand Override with Version Alignment
When switching brands, align versions:
chrome --bot-profile="/path/to/profile.enc" \
--bot-config-browser-brand=edge \
--bot-config-brand-full-version=142.0.3595.65
Playwright Verification Example
const { chromium } = require('playwright-core');
(async () => {
const browser = await chromium.launch({
executablePath: '/path/to/botbrowser/chrome',
args: [
'--bot-profile=/path/to/profile.enc',
],
headless: true,
});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
// Verify UA string
const ua = await page.evaluate(() => navigator.userAgent);
console.log('User-Agent:', ua);
// Verify low-entropy Client Hints
const brands = await page.evaluate(() =>
navigator.userAgentData.brands.map(b => `${b.brand} v${b.version}`)
);
console.log('Brands:', brands);
// Verify high-entropy Client Hints
const highEntropy = await page.evaluate(async () => {
return await navigator.userAgentData.getHighEntropyValues([
'platformVersion', 'architecture', 'bitness',
'fullVersionList', 'model'
]);
});
console.log('Platform:', highEntropy.platform);
console.log('Platform Version:', highEntropy.platformVersion);
console.log('Architecture:', highEntropy.architecture);
console.log('Bitness:', highEntropy.bitness);
console.log('Full Version List:', highEntropy.fullVersionList);
await browser.close();
})();
Puppeteer Verification Example
const puppeteer = require('puppeteer-core');
(async () => {
const browser = await puppeteer.launch({
executablePath: '/path/to/botbrowser/chrome',
args: [
'--bot-profile=/path/to/profile.enc',
'--bot-config-browser-brand=edge',
],
headless: true,
defaultViewport: null,
});
const page = await browser.newPage();
await page.goto('https://example.com');
const ua = await page.evaluate(() => navigator.userAgent);
const brands = await page.evaluate(() =>
navigator.userAgentData.brands.map(b => b.brand)
);
console.log('User-Agent:', ua);
console.log('Brands:', brands);
await browser.close();
})();
Verification
After configuring UA and Client Hints, verify alignment:
- Check that
navigator.userAgentcontains the correct browser name and version - Verify
navigator.userAgentData.brandscontains the correct brand tokens - Confirm
Sec-CH-UAheaders match the JavaScript-reported brands - Test
getHighEntropyValues()for correct platform, architecture, and version info - Verify consistency in a Web Worker context
Common pitfalls to check for:
- Windows 11 vs. Windows 10:
Sec-CH-UA-Platform-Versionstarting from"15.0.0"indicates Windows 11. Lower values indicate Windows 10. The value must match the User-Agent's Windows version string. - Brand token ordering: Each brand has a specific order. Check that the GREASE token, Chromium token, and brand token appear in the correct sequence.
- Version number alignment: The major version in the UA string should match the major version in
Sec-CH-UAbrand tokens.
Best Practices
-
Prefer profiles over manual configuration. Profiles capture complete, consistent UA/Client Hints configurations from real browsers. Manual configuration requires careful version alignment.
-
Do not use Playwright/Puppeteer UA overrides. These framework-level overrides only change the UA string, not Client Hints. With BotBrowser profiles, you do not need them.
-
Keep versions realistic. Use recent, current browser versions. Outdated versions are unusual and draw attention.
-
Align brand-specific versions. When using
--bot-config-browser-brand=edge, set--bot-config-brand-full-versionto a real Edge version that corresponds to the Chromium major version. -
Test with fingerprint verification tools. Verify that all UA and Client Hints signals align after every configuration change.
-
Update profiles regularly. Browser versions change frequently. Use current profiles to match the versions that most users have.
Frequently Asked Questions
Does BotBrowser handle UA Reduction (Chrome's reduced User-Agent string)? Yes. BotBrowser profiles reflect the actual UA string format of the browser version they represent, including any UA Reduction changes.
Can I set different UA values per browser context?
Per-context UA customization is available through Playwright's userAgent option, but this only changes the UA string. For full Client Hints consistency, use separate browser instances with different profiles or brand settings.
What happens if I set --user-agent without Client Hints flags?
BotBrowser will use the custom UA string but derive Client Hints from the profile. If the custom UA string is inconsistent with the profile's Client Hints, a mismatch may occur. It is best to use the placeholder syntax with corresponding --bot-config-* flags.
Does navigator.userAgentData work in workers?
Yes. BotBrowser ensures that navigator.userAgentData returns consistent values in the main thread, web workers, and service workers.
How does BotBrowser handle GREASE tokens? BotBrowser generates GREASE tokens matching the patterns observed in real browser instances for each brand and version. The GREASE format, version number, and position are all correct.
Can I use custom UA strings with special characters?
Yes. The --user-agent flag accepts arbitrary strings. When using placeholders like {platform-version}, they are replaced with values from the corresponding flags.
Is getHighEntropyValues() affected by user consent?
In standard Chromium, getHighEntropyValues() returns a Promise that resolves with the requested values. BotBrowser profiles include all high-entropy values from the source browser, so the Promise resolves with consistent, profile-defined data.
What about the Sec-CH-UA-WoW64 header? BotBrowser includes all standard Client Hints headers for the platform. WoW64-related hints are set correctly for Windows profiles where applicable.
Summary
User-Agent strings and Client Hints are the network-level foundation of browser identity. BotBrowser manages both through profiles that capture real browser configurations and CLI flags that provide runtime customization. All signals, from HTTP headers to JavaScript APIs to worker contexts, are consistent and aligned automatically.
For brand identity control, see Browser Brand Switching. For geographic identity, see Timezone, Locale, and Language Configuration. For complete identity isolation, see Multi-Account Browser Isolation.