Client Hints Fingerprinting: How HTTP Headers Reveal Your Browser Identity
Client Hints headers like sec-ch-ua expose browser brand, version, platform, and device details with every HTTP request. Learn how inconsistencies in these headers create trackable signals and how to maintain consistency.
Introduction
Every HTTP request your browser sends carries identity information in the form of Client Hints headers. Before any JavaScript runs, before any page content loads, headers like Sec-CH-UA, Sec-CH-UA-Platform, and Sec-CH-UA-Mobile have already transmitted browser brand, version, and operating system details to the server in a structured, machine-readable format. Additional high-entropy values covering CPU architecture, OS version, device model, and full version strings are available when the server requests them or when JavaScript calls navigator.userAgentData.getHighEntropyValues().
These headers were designed as a privacy improvement over the monolithic User-Agent string, providing structured data through an opt-in mechanism. In practice, the default headers sent on every request already contribute meaningful entropy to browser fingerprinting. More importantly, the internal consistency of Client Hints values across all request types within a session, and the alignment between HTTP headers and JavaScript APIs, has become a privacy-critical surface. Any inconsistency between what the network layer reports and what the JavaScript layer exposes creates a trackable signal that is trivially observable server-side.
BotBrowser addresses this at the engine level, ensuring that every Client Hints value is profile-driven, consistent across all request types and execution contexts, and aligned between HTTP headers and JavaScript APIs.
BotBrowser's Solution
Profile-Driven Client Hints
BotBrowser generates all Client Hints values from profiles captured on real browser instances. When a profile is loaded, the engine constructs the complete Client Hints configuration from the profile data, including:
- The full
Sec-CH-UAbrand list with the correct GREASE brand for the profiled Chrome version - Deterministic brand ordering matching real Chrome behavior
- Platform, platform version, architecture, bitness, and model values
- Full version list with all brand entries and their complete version strings
This configuration is set once at browser startup and applies uniformly to every request throughout the session. There is no secondary code path, no per-request regeneration, and no context-dependent variation.
Cross-Request Consistency
All request types use the same brand list. Navigation requests, subresource requests (scripts, stylesheets, images, fetch calls), worker requests, prefetch requests, and service worker requests all receive identical Sec-CH-UA headers because they all draw from the same profile-derived configuration. A single page load can generate dozens of HTTP requests, and every one of them carries the same Client Hints values.
JavaScript and HTTP Alignment
BotBrowser ensures that navigator.userAgentData.brands in JavaScript returns the same brands that appear in the Sec-CH-UA HTTP header. Similarly, navigator.userAgentData.getHighEntropyValues() returns values consistent with the high-entropy Client Hints headers. This alignment extends to every execution context in the browser:
- Main thread (
windowcontext) - Dedicated web workers
- Shared workers
- Service workers
- Worklets
Every context reports the same Client Hints values because they all derive from the same profile.
Cross-Session Stability
When using the same profile across multiple browser sessions, BotBrowser produces the same Client Hints configuration every time. The GREASE brand, version, ordering, and all metadata values are deterministic given the same profile. This is important for scenarios where a persistent browser identity must be maintained across restarts.
Brand Override
When you need to present a different browser brand (e.g., Edge instead of Chrome), BotBrowser automatically adjusts all Client Hints surfaces:
chrome --bot-profile="/path/to/profile.enc" \
--bot-config-browser-brand=edge \
--bot-config-brand-full-version=136.0.3240.76
This updates Sec-CH-UA brands, Sec-CH-UA-Full-Version-List, navigator.userAgentData.brands, and the User-Agent string in one coherent operation. All surfaces reflect the same brand identity.
Configuration and Usage
Basic Profile Usage
For most use cases, loading a profile is sufficient:
chrome --bot-profile="/path/to/profile.enc"
The profile contains all Client Hints configuration. No additional flags are needed for consistent Client Hints behavior.
Playwright Integration
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');
// Read low-entropy Client Hints from JavaScript
const brands = await page.evaluate(() =>
navigator.userAgentData.brands.map(b => `${b.brand};v="${b.version}"`)
);
console.log('Brands:', brands);
// Read high-entropy Client Hints
const hints = await page.evaluate(async () => {
const data = await navigator.userAgentData.getHighEntropyValues([
'platformVersion', 'architecture', 'bitness',
'fullVersionList', 'model'
]);
return {
platform: data.platform,
platformVersion: data.platformVersion,
architecture: data.architecture,
bitness: data.bitness,
fullVersionList: data.fullVersionList,
model: data.model,
};
});
console.log('High-entropy hints:', hints);
await browser.close();
})();
Puppeteer Integration
const puppeteer = require('puppeteer-core');
(async () => {
const browser = await puppeteer.launch({
executablePath: '/path/to/botbrowser/chrome',
args: [
'--bot-profile=/path/to/profile.enc',
],
headless: true,
defaultViewport: null,
});
const page = await browser.newPage();
await page.goto('https://example.com');
// Verify Client Hints from the loaded profile
const jsHints = await page.evaluate(() => ({
brands: navigator.userAgentData.brands,
mobile: navigator.userAgentData.mobile,
platform: navigator.userAgentData.platform,
}));
console.log('Client Hints:', jsHints);
await browser.close();
})();
Verification
Checking JavaScript Client Hints
Open a page and verify that navigator.userAgentData returns expected values:
// In browser console or via automation
const brands = navigator.userAgentData.brands;
console.log('Brands:', JSON.stringify(brands, null, 2));
console.log('Platform:', navigator.userAgentData.platform);
console.log('Mobile:', navigator.userAgentData.mobile);
const high = await navigator.userAgentData.getHighEntropyValues([
'platformVersion', 'architecture', 'bitness',
'fullVersionList', 'model', 'wow64'
]);
console.log('High-entropy:', JSON.stringify(high, null, 2));
Using Online Verification Tools
Visit BrowserLeaks, CreepJS, or Cover Your Tracks to view your Client Hints values alongside other fingerprinting data. Confirm that the reported brands, platform, and version match what you expect from your profile. See also BotBrowser's fingerprint verification guide for comprehensive verification guidance.
Best Practices
Always Use Profiles
Do not manually construct Client Hints values. The relationship between Chrome version, GREASE brand, and brand ordering is version-dependent and changes with each release. Profiles capture these relationships from real browser instances and reproduce them exactly.
Match Client Hints with User-Agent
If you override the User-Agent string with --user-agent, ensure that the --bot-config-browser-brand and --bot-config-ua-full-version flags align. BotBrowser will generate matching Client Hints automatically, but the User-Agent string override is applied separately and must be consistent.
Keep Profiles Updated
Client Hints change with each Chrome release. New GREASE brands are introduced, and version numbers advance. Using an outdated profile with old version numbers can itself become a distinguishing signal. Regularly update your profiles from the BotBrowser profiles repository to match current browser versions.
Do Not Mix Client Hints Sources
Avoid combining BotBrowser's profile-based Client Hints with CDP overrides or framework-level User-Agent changes. Each of these operates at a different layer, and combining them creates inconsistencies. Let BotBrowser handle all identity signals through its profile system.
Monitor Server-Requested Hints
Some servers request additional high-entropy hints via Accept-CH. BotBrowser responds to these requests with values from the profile. If you are testing against a specific service, check which hints it requests and verify that BotBrowser's responses are complete and consistent.
Frequently Asked Questions
What is the difference between User-Agent and Client Hints?
The User-Agent header is a single string containing browser, version, and OS information in a free-form format. Client Hints (Sec-CH-UA-* headers) provide the same information as structured key-value pairs. Modern Chromium browsers send both, and servers compare them for consistency. See Custom User Agent for a detailed comparison.
Can I set Client Hints manually with Playwright or Puppeteer?
Playwright and Puppeteer allow overriding the User-Agent string and, through CDP, some Client Hints metadata. However, these overrides do not cover all surfaces (workers, service workers, high-entropy values), and they require manual GREASE brand management. BotBrowser's profile-based approach covers every surface and avoids the risk of partial overrides creating inconsistencies.
Do Client Hints work in headless mode?
Yes. BotBrowser sends identical Client Hints in both headless and headed modes. The Sec-CH-UA headers and navigator.userAgentData values are the same regardless of the display mode.
How do Client Hints relate to navigator properties?
navigator.userAgentData is the JavaScript interface to Client Hints. The brands, platform, and mobile flag exposed through navigator.userAgentData correspond directly to the Sec-CH-UA, Sec-CH-UA-Platform, and Sec-CH-UA-Mobile HTTP headers. BotBrowser ensures these are always aligned. For more on navigator properties, see Navigator Properties Fingerprinting.
What happens if the server does not request high-entropy hints?
BotBrowser only sends high-entropy Client Hints headers when the server requests them via Accept-CH, following the standard Chromium behavior. However, the JavaScript getHighEntropyValues() API is always available. BotBrowser ensures that both the HTTP headers (when sent) and the JavaScript API return consistent values from the profile.
Can Client Hints headers be used for tracking without JavaScript?
Yes. The default Sec-CH-UA, Sec-CH-UA-Mobile, and Sec-CH-UA-Platform headers are sent with every HTTP request, even when JavaScript is disabled. This is why BotBrowser's protection operates at the engine level rather than relying on JavaScript-layer interventions.
How often should I update my profiles for Client Hints accuracy?
Each Chrome release can introduce changes to Client Hints values. It is recommended to update profiles with each major Chrome release (approximately every four weeks) to ensure that values match current browser versions.
Does BotBrowser support brand overrides for Edge, Brave, and Opera?
Yes. The --bot-config-browser-brand flag supports chrome, edge, brave, opera, and webview. When a brand is specified, BotBrowser updates all Client Hints headers, JavaScript APIs, and the User-Agent string to reflect the chosen brand consistently.
Summary
Client Hints headers carry browser identity information on every HTTP request, creating a privacy-significant surface that operates before any page content loads. The consistency of these values across request types, execution contexts, and between HTTP headers and JavaScript APIs is critical for privacy protection.
BotBrowser handles this at the engine level. Profiles captured from real browser instances define all Client Hints values. These values are generated once from the profile and applied uniformly to every request type, every execution context, and every API surface. The result is Client Hints behavior that matches real browser output, because it is produced by the same Chromium engine with profile-driven configuration.
For more on related topics, see:
- Custom User Agent for User-Agent string and Client Hints management
- Navigator Properties Fingerprinting for JavaScript-level browser identity
- What is Browser Fingerprinting for a comprehensive overview of fingerprinting techniques
Related Articles
Ready to protect your browser fingerprint?
BotBrowser provides engine-level fingerprint control with real device profiles. Start with a free tier or explore all features.