Do websites know you are using a script?
Often, yes, and usually not for the reason people assume. The fingerprint gets most of the attention, but a site can tell a script is driving the browser through a separate set of tells that have nothing to do with the GPU, the fonts or the screen. These are automation-layer tells, and they answer a narrower question than “who are you”: they answer “is a program at the wheel”.
The honest version of the answer is that these tells are fixable, and that fixing them does not make you invisible. It removes the announcement that a script is present. It does not remove a datacenter IP, a per-account quota, or the fact that you filled a form in eighty milliseconds. This page covers the announcement, and is clear about where the announcement ends.
The four tells that say “a script is here”
None of these depend on your fingerprint. A browser with a flawless machine identity still trips every one of them if the automation layer is careless.
navigator.webdriver. A boolean the automation standard requires the browser to set totruewhen a driver is attached. It is the single most checked automation flag on the web, it is one line to read, and setting it tofalseis not the fix a real browser reportsundefined.- CDP or BiDi artifacts. The remote control protocols that most drivers speak leave observable traces: extra objects on the page, timing side effects, a debugger that is attached. A page can probe for the protocol itself rather than for any property it sets, which is why CDP and BiDi have their own detection surface.
- Synthetic events with
isTrustedfalse. Every DOM event carries anisTrustedflag. The browser sets it totruefor events it generated from real hardware input andfalsefor events created in JavaScript. A driver that clicks by dispatching a synthetic event producesisTrusted: false, and that flag cannot be forged from page script. - Unnaturally regular timing. Keystrokes at a perfectly uniform interval, a click the same number of milliseconds after every load, requests on a fixed period. This one is different from the other three: it is not something the browser leaks, it is something your code produces.
The first three are properties of the automation plumbing. The fourth is a property of how you drive. That distinction is the whole point of this page.
Why invisible_playwright does not expose the first three
invisible_playwright is a Firefox patched at the C++ level, driven by stock Playwright over a patched protocol. The design goal is that the browser looks like a real Firefox driven by a real person, and the automation layer is where “driven by a real person” is won or lost.
The control channel does not present the driver tells that a normal automation setup exposes: navigator.webdriver reads the way a clean browser reads, and the protocol the wrapper speaks to the engine is not the one the common protocol probes are looking for. More importantly, input is not synthesized in page script. Clicks and keystrokes are routed through the browser’s real input pipeline, so the events a page sees are generated by the engine the same way a physical mouse or keyboard would generate them. That means isTrusted is genuinely true, because the event genuinely came through the trusted path, not because a flag was patched.
Switching from plain Playwright to this is two lines, and every Playwright method works unchanged afterwards:
from invisible_playwright import InvisiblePlaywright
with InvisiblePlaywright(seed=42) as browser:
page = browser.new_page()
page.goto("https://example.com")
page.fill("#name", "Ada Lovelace") # keystrokes go through the real input path
page.click("#submit") # a genuinely trusted click, isTrusted true
The browser object is a real Playwright Browser, so there is no reduced API to learn. The seed=42 argument makes the machine identity reproducible, which matters for debugging: the same seed gives the same browser every run, so a failing run can be replayed instead of guessed at.
The tell you still own: timing and behaviour
Here is the caveat, stated plainly. Removing the driver tells hides that a script is present. It does nothing about the fourth tell, because the fourth tell is yours.
A trusted click that lands at the exact same offset after every page load is still a robot. A form filled faster than a human can read it is still a robot. A request loop on a metronome is still a robot. Some sites do not fingerprint the browser at all, they watch the session, and against those sites the quality of your pacing is the entire game. This is especially true for AI agents, whose pauses can be shaped like model latency rather than human thought.
You control this, which is good news, because it means the fix is in your code rather than in the engine. Vary the intervals. Do not act faster than a person could. Let the page settle before the next action. The engine gives you trusted events and human-like mouse arcs by default; the rhythm of when you fire them is on you.
import random
import time
with InvisiblePlaywright(seed=42) as browser:
page = browser.new_page()
page.goto("https://example.com")
for field, value in [("#first", "Ada"), ("#last", "Lovelace")]:
page.click(field)
page.type(field, value, delay=random.randint(80, 220)) # per-keystroke jitter
time.sleep(random.uniform(0.4, 1.1)) # think between fields
page.click("#submit")
What this does not fix, so you supply it
The driver layer is one of several independent things a site checks, and being clean on one says nothing about the others. invisible_playwright reads as a genuine Firefox at the fingerprint, TLS and driver layers, which is why it passes most detection checks. It does not, on its own, address:
- IP reputation. A perfect browser on a known datacenter or already-blocked address still loses. You supply a clean exit.
- Per-account quotas and rate limits. These are counted server-side against your account or address, and no browser property changes the count.
- Behaviour and timing, covered above, which is yours to shape.
- The country and timezone agreeing with the exit. The engine auto-derives the timezone from the egress IP so these do not drift apart, but the exit itself is a choice you make.
The rule from the rest of these notes applies here too: a suppressed signal is itself a signal. Hiding that a script is present is worth doing precisely because it is not the same as hiding everything, and treating it as a complete solution is how a clean driver layer ends up flagged for a metronomic click.
How to confirm it on your own setup
Do not take a vendor’s word for any of this, including ours. Open a page that reads these values in both invisible_playwright and a stock browser on the same machine, and compare the fields. Read navigator.webdriver. Attach a listener and read event.isTrusted on a real click versus a scripted one. The method for testing this without fooling yourself with a green verdict is its own page, and the short version is: assert the signal you expect is present, do not just confirm a bad one is absent.
with InvisiblePlaywright(seed=42) as browser:
page = browser.new_page()
page.goto("https://example.com")
print("webdriver:", page.evaluate("() => navigator.webdriver"))
trusted = page.evaluate("""() => new Promise(resolve => {
document.addEventListener('click', e => resolve(e.isTrusted), { once: true });
})""")
page.click("body")
print("isTrusted:", trusted)
Run it more than once. This domain is not deterministic, and a single green run is not a pass.
Conclusion
Websites can tell a script is driving a browser, and they do it mostly through the automation layer rather than the fingerprint: navigator.webdriver, the control protocol’s artifacts, and untrusted synthetic events. invisible_playwright is built so those three do not announce themselves, because its input goes through the real pipeline and its protocol does not expose the driver tells. That is genuinely useful and it is genuinely bounded. The fourth tell, timing, is not the engine’s to fix and not something any browser patch can hide. Get the plumbing clean with the tool, get the rhythm human with your code, and put both behind an exit that does not give you away on its own.
Short answers to the questions that lead here
Can a website tell I am using a script? Often yes, through automation-layer tells like navigator.webdriver, control-protocol artifacts and untrusted synthetic events, which are independent of your fingerprint.
Does hiding navigator.webdriver make me undetectable? No. It removes one specific announcement. Timing, IP reputation and rate limits are separate and remain yours to handle.
What is isTrusted and why does it matter? It is a per-event flag the browser sets true only for events from real input. Events synthesized in page script come back false, which flags automation. Input through the real pipeline is trusted for real.
Will invisible_playwright pass every bot check? It reads as a genuine Firefox at the fingerprint, TLS and driver layers, which is why it passes most. It does not fix your IP, your quotas, or superhuman pacing.
How do I make my timing look human? Vary intervals, add per-keystroke jitter, pause between actions, and never act faster than a person could read the page.
Is running a script against a site illegal? That is a question about a site’s terms and your jurisdiction, not about detection. This page is about how detection works, not about permission to automate.
Sources
- The WebDriver specification for the
navigator.webdriverrequirement, the WebDriver BiDi specification for the remote-protocol surface, and the DOM standard for theisTrustedevent flag, each read from its own text rather than from a summary. - This project’s own input path and protocol behaviour, and its release gates, from which the trusted-event and driver-tell claims above are measured rather than asserted.
See also: navigator.webdriver, explained for the one flag every site reads, why isTrusted separates a real click from a scripted one, and testing detection without fooling yourself with a verdict.
Written while maintaining invisible_playwright, a Firefox patched at the C++ level driven by stock Playwright. It hides that a script is present; the pacing is still yours to get right.