The webdriver flag and cdc_ variables are ChromeDriver's fingerprint, not Selenium's
If a site can tell you are running Selenium, the tell is probably not in your test code. It is coming from ChromeDriver (or GeckoDriver), the separate process that sits between your script and the browser and translates WebDriver protocol commands into browser control. That process leaves marks a script-level patch cannot reach, because your script never set them.
navigator.webdriver is a protocol requirement, not a library quirk
navigator.webdriver is part of the WebDriver specification itself. A browser implementing the protocol is expected to expose this flag as true while under remote control, regardless of which client library is sending the commands. Selenium's Python, Java or JS bindings do not set this property; ChromeDriver and GeckoDriver do, as part of being a spec-compliant WebDriver server talking to the browser they launched.
That distinction matters for how you try to fix it. A stealth pattern that overrides navigator.webdriver with a JS snippet injected from your own script is racing a value the browser already set before your script ran, and it does nothing about whatever else in the runtime reads the same underlying automation state. Patching the getter hides one read of the flag. It does not undo the condition the flag was reporting.
ChromeDriver's own footprint
Independent of that flag, ChromeDriver has habits of its own. Launching Chrome under its default automation switch used to show, and on some setups still shows, a distinct infobar telling the user the browser is controlled by automated test software. You can suppress it via a capability like excludeSwitches: ['enable-automation'], but that itself produces a Chrome instance configured differently from a stock session, its own tell to anything checking flags against runtime state.
The cdc_ variables
To implement the WebDriver spec's script-execution atoms, ChromeDriver has historically injected global variables into the page's window object, using a recognizable name pattern beginning with cdc_. These are not something your test writes. They are stamped in by the driver process itself on document load, so a detection script can simply enumerate window for that prefix, and no patch written from the Selenium side removes them, because the driver reinjects them on the next navigation regardless of what your script did on the last one.
Different ChromeDriver builds have handled this differently over time, so do not assume any single variable name still holds for the build you run. The mechanism is the constant: the driver stamps recognizable markers into the page, and those markers live at the driver layer, not the library layer.
Why this is a protocol problem, not a library problem
Selenium's architecture routes commands through a standalone driver binary that speaks the WebDriver wire protocol to the browser. Playwright and Puppeteer connect directly over the Chrome DevTools Protocol without an intermediate driver process implementing that spec. That is the real difference: Selenium's model requires a separate process whose entire job is to comply with a specification that includes announcing itself. A wrapper library patching your script's environment is arguing with a spec-compliant server process, not a JS quirk.
Checks to run against your current setup
- Read
navigator.webdriverin the console during a normal session. Note the driver version and capabilities in use. - Enumerate
windowfor any global matching acdc_-style prefix, on the exact ChromeDriver build you deploy, not a build from memory. - Confirm whether your capabilities suppress the automation infobar, and if they do, note that the suppression itself is a non-default configuration.
- Diff the full
windowandnavigatorsurface between a ChromeDriver session and a manual Chrome session on the same machine, rather than checking the one flag you already know about. - Check when your patches run. Anything applied through
execute_scriptafter the page has loaded is too late for whatever a detection script reads on first paint.
When a rewrite genuinely is not worth it
Be honest about the cost before acting on any of this. AntiBrow's launch() returns a standard Playwright Page and BrowserContext, not a Selenium WebDriver instance. If your suite is built on Selenium locators, waits, a page-object layer, and Selenium Grid for parallel runs, moving means rewriting that surface, not swapping an import. For a short script that is an afternoon. For a mature codebase with years of page objects and CI wiring, it is a project.
If the specific gap you are chasing really is just the flag and the cdc_ variables, a maintained community wrapper for your existing driver, undetected-chromedriver being the best known one for Python, closes that narrower hole for less cost than a framework migration. Do that arithmetic first, and remember that neither option changes what your proxy or your account history say about you; the driver layer is one signal among several.
If the fingerprint underneath is the actual problem
A driver-layer patch cannot touch the device identity underneath: canvas, WebGL, fonts, audio and hardware properties that come from the machine, not the driver. AntiBrow generates that fingerprint once per profile, applies it in the browser engine below the JS layer, and persists it with the profile, with unlimited local profiles on every plan. That only matters if the flag and the cdc_ variables are not your whole problem. Read the Playwright migration notes or the Puppeteer ones for the same move from those libraries, and check the SDK reference before committing to a rewrite either way.
Try it on the free tier.
Unlimited local profiles, no credit card. Check it against the detectors yourself.