ScreenshotNeo

BlogEngineering

How to Detect Headless Chrome

Learn which signals reveal automated Chrome, what they mean, and how to detect it without blocking legitimate users.

By the ScreenshotNeo team1 October 20267 min read

To detect Headless Chrome, start with navigator.webdriver, then evaluate User-Agent and other signals together. Treat the result as evidence of browser automation, not proof of malicious activity. Chrome’s current Headless and headful modes share the same browser implementation, so old rules that assume every headless browser is obviously different are unreliable.

For a site you operate, use detection to adapt testing, logging or risk scoring. Avoid denying access because of one browser property. Keep a supported path for authorized automation and users who are incorrectly classified.

What Headless Chrome detection actually tells you

Signal What it means How stable it is
navigator.webdriver The browser reports that it is controlled by automation. Documented standard property; useful for cooperative WebDriver automation.
User-Agent May expose a headless marker in some configurations. Configurable and affected by Chrome’s User-Agent reduction.
User-Agent Client Hints Provides structured browser information when requested. Useful for browser characterization, not an automation verdict.
Cross-signal consistency Compares headers and browser attributes for contradictions. Version-sensitive and privacy-sensitive; use cautiously.

MDN defines navigator.webdriver as indicating whether the user agent is controlled by automation. In Chrome, it is true when automation-related options such as --enable-automation, --headless, or remote debugging on port 0 are used. See the MDN documentation.

Chrome documents that current Headless and headful modes are unified. From Chrome 132, the old implementation is available only as the separate chrome-headless-shell binary; check the Chrome Headless documentation for release-specific details.

1. Check navigator.webdriver in the browser

This is the clearest browser-side check when the automation environment cooperates.

<script>
  const automated = navigator.webdriver === true;

  if (automated) {
    console.log('Automation disclosed by the browser');
    // Adapt test-only behavior or add a risk signal.
  } else {
    console.log('No automation disclosure');
  }
</script>

The property is read-only. A false value means only that this particular signal was not disclosed; it does not prove that the visitor is human or that automation is absent.

Use it for testing and conditional behavior

function isAutomatedBrowser() {
  return navigator.webdriver === true;
}

if (isAutomatedBrowser()) {
  document.documentElement.dataset.browserMode = 'automation';
}

For end-to-end tests, this lets your application expose a stable test path without pretending that automation is abuse. For access control, record the signal and combine it with authentication, request rate, account history and the action being attempted.

2. Inspect the User-Agent, but do not treat it as proof

Some Chrome configurations include a headless token in the User-Agent. That behavior is not universal, and User-Agent strings are reduced over time. A token can be useful context, but removing or changing it does not establish that automation is undetectable.

const ua = navigator.userAgent;
const mentionsHeadless = /HeadlessChrome/i.test(ua);

console.log({ ua, mentionsHeadless });

On the server, log the request User-Agent for diagnostics:

const http = require('node:http');

http.createServer((req, res) => {
  const ua = req.headers['user-agent'] || '';
  const headlessHint = /HeadlessChrome/i.test(ua);
  console.log({ ua, headlessHint });
  res.writeHead(200, { 'content-type': 'application/json' });
  res.end(JSON.stringify({ headlessHint }));
}).listen(3000);

3. Request User-Agent Client Hints when you need browser details

Chrome recommends User-Agent Client Hints for specific browser information. They characterize the browser; they do not declare that a session is automated.

const hints = {
  brands: navigator.userAgentData?.brands ?? [],
  mobile: navigator.userAgentData?.mobile ?? null,
  platform: navigator.userAgentData?.platform ?? null
};

console.log(hints);

On the server, request only the hints you need and document the purpose. Extra fingerprinting increases privacy impact and can still produce ambiguous results.

4. Combine signals instead of blocking on one field

Published crawler-detection research has examined combinations such as navigator.webdriver, Accept-Language, window.chrome, notification permissions, screen characteristics, codecs and touch support. These attributes vary across browser versions, legitimate devices and privacy settings, so use them as a measured risk model rather than a universal checklist. The NDSS study is historical and scoped to its sample; it does not provide a current accuracy rate.

function collectBrowserSignals() {
  return {
    webdriver: navigator.webdriver === true,
    userAgent: navigator.userAgent,
    language: navigator.language,
    languages: navigator.languages,
    hasWindowChrome: typeof window.chrome !== 'undefined',
    screen: {
      width: screen.width,
      height: screen.height,
      colorDepth: screen.colorDepth
    },
    touchPoints: navigator.maxTouchPoints,
    permissionsApi: typeof navigator.permissions !== 'undefined'
  };
}

const signals = collectBrowserSignals();
console.log(signals);

A simple scoring example makes the policy explicit. It is intentionally conservative: one weak clue does not trigger a denial.

function automationScore(signals) {
  let score = 0;
  if (signals.webdriver) score += 5;
  if (/HeadlessChrome/i.test(signals.userAgent)) score += 2;
  if (!signals.hasWindowChrome) score += 1;
  if (!signals.language) score += 1;
  return score;
}

const score = automationScore(signals);
const action = score >= 5 ? 'review-or-step-up' : 'allow';
console.log({ score, action });

Before changing a user-visible flow, measure this model against known legitimate browsers and authorized automation. Keep the automated decision separate from the abuse decision: an automated build, accessibility tool or internal monitor may be completely authorized.

5. Server-side collection with a complete example

Client-side JavaScript can report browser properties, while the server sees HTTP headers and can attach the result to a request or session. Do not trust a client-provided verdict as an authentication factor.

import express from 'express';

const app = express();
app.use(express.json());

app.post('/browser-signals', (req, res) => {
  const ua = req.get('user-agent') || '';
  const client = req.body || {};
  const webdriver = client.webdriver === true;
  const uaHeadlessHint = /HeadlessChrome/i.test(ua);

  const score = (webdriver ? 5 : 0) + (uaHeadlessHint ? 2 : 0);
  res.json({
    automatedSignal: score > 0,
    score,
    reasons: {
      webdriver,
      uaHeadlessHint
    }
  });
});

app.listen(3000, () => console.log('Listening on http://localhost:3000'));

Send the browser result after page load:

await fetch('/browser-signals', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ webdriver: navigator.webdriver === true })
});

6. Test the signal with Chrome, Puppeteer and Selenium

Chrome command line

google-chrome --headless --remote-debugging-port=0 https://example.com

Puppeteer

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const result = await page.evaluate(() => ({
  webdriver: navigator.webdriver,
  userAgent: navigator.userAgent
}));
console.log(result);
await browser.close();

Selenium with Python

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
driver.get('https://example.com')
print(driver.execute_script("return {webdriver: navigator.webdriver, userAgent: navigator.userAgent}"))
driver.quit()

7. Common errors and fixes

Problem Cause Fix
navigator.webdriver is undefined The code ran outside a browser or an older/nonstandard environment is involved. Guard access with navigator && 'webdriver' in navigator and test in the target browsers.
Every automated request is blocked A single signal was treated as a verdict. Use scoring or review thresholds and allow authorized automation.
No HeadlessChrome token appears User-Agent reduction, configuration differences or a unified Headless build. Use webdriver and contextual signals; never require the token.
Results differ between Chrome versions Headless architecture and exposed attributes change over time. Pin supported browser versions in tests and revalidate after upgrades.
Client and server disagree Headers, JavaScript properties and proxies can differ. Log both observations with timestamps and request IDs before changing policy.
Privacy review rejects the detector Too many fingerprint attributes are collected without a clear purpose. Start with the minimum signals, document retention and provide an appeal path.

8. Performance, reliability and cost considerations

  • Performance: Reading a few local navigator properties is inexpensive. Avoid repeated collection on every interaction.
  • Reliability: Browser attributes are version and configuration dependent. Maintain a compatibility test matrix for supported Chrome releases.
  • False positives: Accessibility tools, CI jobs, monitoring and internal QA can be automated and legitimate.
  • Operations: Prefer passive logging or a step-up challenge before a hard block. Monitor conversion and support reports after policy changes.
  • Privacy: Collect only signals tied to a documented decision, limit retention and avoid building a broader fingerprint than necessary.
  • Cost: Local checks add negligible compute; expensive work usually comes from downstream challenges, browser rendering or manual review. Apply stronger checks only to higher-risk actions.

9. Historical evidence and its limits

An NDSS 2020 study reported that 93 of 291 sites that blocked crawlers (31.96%) used fingerprinting for crawler detection. That statistic describes the study’s sample and period; it is not a current prevalence estimate and is not a Headless-Chrome-only detection rate. The researchers tested several crawler variants and noted ground-truth and scope limitations. Use the paper as evidence that detection is layered and context dependent, not as a benchmark for a production detector.

Or skip the browser setup

If your goal is to capture a page for documentation, monitoring or an AI workflow, ScreenshotNeo provides a screenshot API and MCP server. The DIY browser route above remains useful when you need to inspect a visitor session; ScreenshotNeo handles rendering and capture with one request.

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}`);

See the ScreenshotNeo documentation for options. It removes cookie banners, newsletter popups and chat widgets before the shot; bot checks, blank pages and failed loads are never billed; and its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Does navigator.webdriver detect every headless browser?

No. It detects automation disclosed through that property. A false value is not proof that a browser is human-controlled.

Is Headless Chrome the same as automated Chrome?

Headless describes the lack of visible UI. Automation describes external control. They often occur together, but they are different concepts.

Can I detect Headless Chrome only from HTTP?

You can inspect headers such as User-Agent, but HTTP alone does not provide a universal, reliable headless verdict.

Should a site block users with automation signals?

Only when the action and evidence justify it. Prefer risk scoring, authentication and step-up controls, with an accessible path for authorized automation.

What changed in modern Chrome?

Chrome unified current Headless and headful modes; the old implementation is now distributed separately as chrome-headless-shell from Chrome 132 onward.