Back to Blog
Identity

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-Agent header with Sec-CH-UA headers on the same request. A mismatch indicates modification.
  • JavaScript-to-header comparison: Pages can compare navigator.userAgent with navigator.userAgentData.brands and 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: ?0
  • Sec-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 brands
  • Sec-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:

  1. User-Agent header matches navigator.userAgent
  2. Brand tokens in Sec-CH-UA match navigator.userAgentData.brands
  3. Platform in Sec-CH-UA-Platform matches navigator.userAgentData.platform
  4. Major version in UA string matches the version in Sec-CH-UA brand tokens
  5. Full versions in Sec-CH-UA-Full-Version-List match getHighEntropyValues().fullVersionList
  6. Platform version is realistic for the claimed OS (e.g., Windows 11 reports "15.0.0" or higher)
  7. Architecture and bitness match the platform (e.g., x86/64 for Windows, arm/64 for M-series Mac)
  8. 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-UA headers (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-Agent and Sec-CH-UA-* headers on every request
  • Main thread JavaScript: navigator.userAgent, navigator.userAgentData
  • Web Workers: navigator.userAgent and navigator.userAgentData in 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:

  1. Check that navigator.userAgent contains the correct browser name and version
  2. Verify navigator.userAgentData.brands contains the correct brand tokens
  3. Confirm Sec-CH-UA headers match the JavaScript-reported brands
  4. Test getHighEntropyValues() for correct platform, architecture, and version info
  5. Verify consistency in a Web Worker context

Common pitfalls to check for:

  • Windows 11 vs. Windows 10: Sec-CH-UA-Platform-Version starting 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-UA brand tokens.

Best Practices

  1. Prefer profiles over manual configuration. Profiles capture complete, consistent UA/Client Hints configurations from real browsers. Manual configuration requires careful version alignment.

  2. 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.

  3. Keep versions realistic. Use recent, current browser versions. Outdated versions are unusual and draw attention.

  4. Align brand-specific versions. When using --bot-config-browser-brand=edge, set --bot-config-brand-full-version to a real Edge version that corresponds to the Chromium major version.

  5. Test with fingerprint verification tools. Verify that all UA and Client Hints signals align after every configuration change.

  6. 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.

#user-agent#client-hints#identity#ua-ch#privacy