What Does the navigator.webdriver Flag Actually Mean?
The navigator webdriver property is a read-only boolean on Navigator.prototype that reports whether the browser session runs under automation control. It reads true when a session is launched with switches such as --enable-automation or --headless, and false in an ordinary hand-driven session. The boolean rarely gets you blocked on its own; the fingerprint values that contradict it do.
📌 TL;DR Executive Summary
- Core Takeaway: navigator.webdriver is a boolean on Navigator.prototype — true when the user agent is controlled by automation, false otherwise in every current Chrome, Firefox and Safari build. It has worked that way since May 2018.
- Key Risk/Challenge: Suppressing it with a script patch is easy and easy to catch. The patch usually lands on the navigator instance instead of the prototype, and the replacement getter leaks its own source through Function.prototype.toString.
- Recommended Solution: Correct values before page scripts run, keep the descriptor shape clean, and confirm the result on sannysoft and CreepJS instead of trusting one boolean to decide the outcome.
The Property Itself: One Boolean, Standardized for WebDriver
The navigator webdriver property lives on Navigator.prototype, not on the navigator object you inspect in the console. The WebIDL declares it as a plain boolean: true when the user agent is controlled by automation, false otherwise. It is read-only, so a page cannot set it — but any script running in the same JavaScript realm can redefine it.
MDN’s documentation for navigator.webdriver describes it as a standard part of the WebDriver contract, exposed in browsers since May 2018. That date matters, because a lot of what circulates about this property was written in the gap before browsers settled on consistent behaviour.
Why Older Tutorials Say “undefined”
Before Chrome 89 in 2021 and Firefox 75 in 2020, the attribute was exposed only when automation was actually active. Reading it in a clean, hand-driven session returned undefined instead of false. Chromium’s Intent to Ship was explicitly titled navigator.webdriver === false when automation is not active, aligning Chromium with what Gecko and WebKit already did.
That change broke a generation of copy-pasted advice. If a tutorial tells you the correct stealth value is undefined, it is describing a browser from 2019, and the patch it recommends makes your session look stranger, not cleaner, on any current build.
Reading the Descriptor Instead of the Value
The value is the least interesting part of the property. Whether it sits on the prototype and whether its getter looks native is where detection bites. You can check both from a page in your session:
const audit = () => {
const own = Object.getOwnPropertyDescriptor(navigator, 'webdriver');
const proto = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver');
return {
ownPropertyOnInstance: own ? 'present (wrong)' : 'absent (good)',
value: navigator.webdriver,
getterLooksNative: proto && proto.get
? /native code/.test(proto.get.toString())
: false,
};
};
A genuine browser getter stringifies to a string containing [native code]. A user-written function stringifies to its own source text. That difference is one line of detection code, and it is the reason keyword-level fixes belong below the JavaScript layer rather than on top of it.
Which Launch Conditions Flip navigator webdriver to True
You do not have to opt into automation signalling deliberately. Several independent switches produce the same true value, and some of them appear in ordinary debugging setups where nobody intended to look automated.
| Condition | Where it comes from | navigator.webdriver |
|---|---|---|
--enable-automation |
The switch drivers pass to announce control | true |
--headless |
Any headless launch in current Chrome builds | true |
--remote-debugging-port set to 0 |
A debugging channel on an auto-assigned port | true |
Firefox marionette.enabled |
The preference, or the --marionette flag |
true |
| No automation switches | Manual browsing, or automation that connects without them | false |
| Prototype init script | Patch on Navigator.prototype before page scripts | false (patched) |
Selenium keeps moving on this front. The project shipped 4.50 in September 2026, with current bindings requiring JDK 11+, Python 3.10+ and .NET 8+, and it removed ChromeDevTools support for Firefox in May 2026, pointing to WebDriver BiDi instead. BiDi is a living standard under active development rather than a frozen specification, so the automation layer underneath your driver is being rebuilt while you read this.
navigator.webdriver plus Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver') in your live session. Two lines tell you whether you have an untouched true value or a patch that already leaked its origin.
Why One Boolean Rarely Decides the Outcome
Detectors do not check navigator webdriver in isolation. They build a picture from dozens of correlated signals and score how consistent it is. Test runs of the kind covered in the fingerprint.com detection tests show the same pattern every time: the profile that fails is the one contradicting itself, not the one with a known flag set.
Common tells that travel alongside an automation switch include ChromeDriver’s $cdc_ properties on the document, a HeadlessChrome token in the user-agent string, an empty plugin list, a WebGL renderer reporting SwiftShader, and canvas output that never varies between sessions. Public suites such as CreepJS, BotD, sannysoft and fpscanner collect those signals and compare them against each other rather than reading any one value.
| Signal | What a detector sees | The real fix |
|---|---|---|
| navigator.webdriver | true on an untouched automated session | Correct the value before page scripts, on the prototype |
| Getter source | A patched getter stringifies to its own code | Avoid script patches; spoof at the engine level |
| Property owner | webdriver becomes an own property of navigator | Define on Navigator.prototype if you must patch |
| User-agent string | A HeadlessChrome token, or a version that contradicts client hints | Keep UA, client hints and platform in one coherent set |
| Plugins array | Empty where a normal desktop build reports entries | Populate it the way a desktop build does |
| WebGL renderer | SwiftShader or a software rasterizer in a “desktop” profile | Report a GPU string that matches the OS |
That last row is why a copied patch list stops working. Every individual fix is checkable against every other one, and detectors are written to do exactly that comparison. A profile claiming Windows 11 with an Apple GPU, an empty plugin array and a webdriver getter that stringifies to JavaScript is easier to flag than an honest automated session.
A Practical Checklist for Auditing and Fixing Your Setup
Work through this in order. Skipping steps is why people patch, retest, see one green result, and then fail on a different site a week later.
- Record a baseline per profile. Evaluate
navigator.webdriver, both descriptors, the user-agent string, plugins, languages, timezone and the WebGL renderer, and save the output before you change anything. - Check the numbers that never move. A static hardwareConcurrency count or a device-memory value no real machine ships survives a webdriver patch — how hardwareConcurrency leaks a profile is worth understanding before you pick numbers.
- Find your injection point. Your patch has to run after the document is created but before any of its scripts execute. Puppeteer exposes Page.evaluateOnNewDocument() for that window, and Playwright’s context init scripts do the same job.
- Patch the prototype if you patch at all. Define on
Navigator.prototype, keep the descriptor configurable, and never leave webdriver as an own property ofnavigator. - Verify the getter source. Confirm the replacement stringifies with
[native code]and that the descriptor shape still matches the original. - Retest on two suites and in both modes. sannysoft gives a fast pass/fail table; CreepJS looks at descriptor hygiene and cross-surface consistency. Run each once headed and once headless, because the injection race resolves differently between them.
- Repeat per profile. A patch applied in one context does not follow a profile with its own user-agent, locale and proxy.
Attaching the patch to a locally running browser over CDP looks like this:
from playwright.sync_api import sync_playwright
CDP_URL = "http://127.0.0.1:PORT" # copy it from the profile's automation settings
PATCH = """
Object.defineProperty(Navigator.prototype, 'webdriver', {
configurable: true,
get: () => false,
});
"""
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(CDP_URL)
context = browser.contexts[0]
context.add_init_script(PATCH)
page = context.new_page()
page.goto("https://bot.sannysoft.com")
print("value:", page.evaluate("navigator.webdriver"))
getter = page.evaluate(
"Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver').get.toString()"
)
print("getter source:", getter[:60])
When Patches Fail: What Detectors Actually Notice
Once navigator webdriver reads false, the remaining risk sits in how you changed it. These are the failure modes that show up most often in test suites.
Patching the Instance Instead of the Prototype
Object.defineProperty(navigator, 'webdriver', {...}) changes the value you see in the console, which feels like success. It also turns webdriver into an own property of the navigator instance — something a clean browser never has. A single Object.getOwnPropertyNames(navigator) call exposes the difference, so define on the prototype or leave the property alone.
Leaving the Getter Source Readable
Function.prototype.toString returns native code for a genuine getter and full source text for a user-written one. If your replacement stores the original descriptor and returns a JavaScript function, you have traded one tell for a sharper one. Hand-rolled patches tend to survive a casual check and fail a serious one for exactly this reason.
Reporting undefined Because a Blog Said So
Current browsers declare the property as a plain boolean in normal sessions, so undefined is a state that no shipping Chrome, Firefox or Safari produces. Set false if you must set anything at all. The undefined value belongs to browsers older than Chrome 89 and Firefox 75.
Injecting After Page Scripts Have Already Run
Page-level patches must land before the page’s own scripts read the value. If a script captured the descriptor or stored a reference early, later redefinition changes nothing for that reference. This is also the usual cause of flaky results: in a headed session your patch may win the race, in headless it may lose, so the same build passes locally and fails in CI.
Fixing the Flag but Not Its Neighbours
A clean webdriver value next to an empty plugin array is still a contradiction. Desktop Chrome builds report plugins, so an empty list in a profile claiming to be desktop Chrome is a mismatch — the navigator plugins fingerprinting breakdown shows how detectors read that array. The same applies to client hints, language lists and timezone offsets, which all have to agree with each other and with the proxy’s exit IP.
Treating Suppression as Proof of Humanity
Removing the webdriver signal does not make traffic human. Detection also weighs behaviour, request timing, IP reputation and fingerprint coherence. Hiding one boolean changes nothing about how you click, type or navigate, and any vendor implying otherwise is selling something.
Where Send.win Fits in a Multi-Profile Workflow
Script patching exists because most automation stacks launch a stock browser and then try to hide it. Send.win works from the other direction: the Sendwin Stealth engine spoofs canvas, WebGL, audio, fonts and hardware at the engine level inside a patched-Chromium build, so those fingerprint values come from the browser rather than from a script dropped into the page. There is no patched getter to stringify and no stray own property to find.
That matters at scale. Each profile reads as a separate machine — no two profiles share a fingerprint — and the network layer does the same job for geography. Built-in residential proxies on every plan feed timezone, locale, WebRTC and geolocation from the proxy’s exit IP, so a session does not report one region in JavaScript and another at the socket. Bring-your-own HTTP or SOCKS5 proxies follow the same rule.
Sendwin Browser installs locally on Windows 10/11 64-bit, macOS 12+ and Linux, while the cloud browser runs the same profiles on EU and US nodes with nothing to install — the free preview gives you 10 minutes a day and Pro and Team remove the limit. On paid plans, cloud sync carries logins across devices, and sharing a profile with a teammate opens it already signed in without a password changing hands. The local Automation API for Selenium, Puppeteer and Playwright sits on the Team plan and attaches to a running profile, which is where the CDP placeholder above comes from.
If you are still deciding how profiles, logins and proxy groups should be organised before you point automation at them, settle the criteria first — the questions in choosing an antidetect browser are cheaper to answer now than after you have 200 profiles.
🏆 Send.win Verdict
If navigator.webdriver keeps showing up true in your sessions, you are looking at a symptom of running an automation-controlled browser. Send.win gives you isolated profiles with engine-level fingerprint spoofing and matching residential proxy geography, so your automation connects to a profile that already presents as its own ordinary machine instead of a stock build with a patch sitting on top of it.
Try Send.win free today — 30 days at $0 with 10 isolated profiles, 10 built-in residential proxies and the Stealth engine included; your profiles stay on your machine and you can cancel in two clicks.
Frequently Asked Questions
What does navigator.webdriver actually mean?
It is a read-only boolean on Navigator.prototype that reports whether the user agent is controlled by automation, standardized as part of the WebDriver contract. Current Chrome, Firefox and Safari builds return true when automation is active and false otherwise. It has been available since May 2018 and is well established on many devices.
Why is navigator.webdriver true in my Selenium session?
The browser was started with automation signalling enabled. Common triggers are the –enable-automation flag, –headless, a remote debugging port set to 0, or Firefox’s marionette.enabled preference. Any one of those is enough, and several appear in ordinary debugging setups where nobody intended to look automated.
Is patching navigator.webdriver enough to avoid detection?
No. Detectors read dozens of signals and score internal consistency rather than one boolean. ChromeDriver’s $cdc_ document properties, a HeadlessChrome token, an empty plugin list, a SwiftShader WebGL renderer and a static canvas fingerprint all feed the same decision. A correct webdriver value next to a contradictory user-agent string still fails.
How do detectors catch a patched webdriver getter?
Two ways, both cheap. If you redefine the property on the navigator instance, it becomes an own property that a clean browser does not have. If you replace the getter with a JavaScript function, Function.prototype.toString returns your source instead of native code. Either check runs in a single expression.
Should I set navigator.webdriver to false or undefined?
False. Every current browser declares navigator webdriver as a plain boolean, so a normal session reads false. Reporting undefined reproduces behaviour that existed only before Chrome 89 in 2021 and Firefox 75 in 2020, when the attribute was exposed only during automation. Tutorials recommending undefined describe browsers that no longer ship.
Can a page read navigator.webdriver before my patch runs?
It can, which is why injection order decides everything. Your patch must execute after the document is created but before any of its scripts run — Puppeteer’s Page.evaluateOnNewDocument() and Playwright’s context init scripts exist for that window. If a page script captured the descriptor early, redefining the property later changes nothing for that reference.
What other signals do bot detectors check besides webdriver?
User-agent strings and client hints, the plugins array, languages, timezone against IP geolocation, WebGL vendor and renderer strings, canvas and audio output, hardware concurrency and device memory. Public suites like CreepJS, BotD, sannysoft and fpscanner aggregate these and test whether they agree with each other.
Does headless mode always set navigator.webdriver to true?
Headless launches are listed among the conditions that produce a true value, alongside the automation and remote debugging flags. Even where a patch corrects the boolean, headless sessions often leak elsewhere — a HeadlessChrome token, a software WebGL renderer, or timing patterns that differ from a headed run. Test both modes separately.
How Send.win Helps With Navigator Webdriver
Send.win is an antidetect browser built for exactly this kind of work — every profile is a clean, isolated identity:
- Isolated profiles – unique fingerprint, separate cookies and storage per profile
- Stealth engine – canvas, WebGL, fonts, and audio spoofed at the engine level
- Desktop app + cloud sessions – native app for Windows, macOS, and Linux, or run profiles in the cloud with no install
- Built-in residential proxies – with automatic timezone, locale, and WebRTC matching
- Team features – share logged-in profiles with teammates without sharing passwords
Try the instant cloud browser demo — no install, no signup — or download the desktop app. The 30-day free trial needs no credit card, and paid plans start at $6.99/month billed annually (see pricing).