All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
page.goto()now answers with the Response, instead of alwaysNone. The navigation itself was never broken - the page loaded, theresponseevents carried the right statuses andexpect_responseworked - but the return value was hardcoded toNone, sopage.goto(url).statuswas not reachable.page.reload()answers with a Response too, and so does history navigation, which shares that code path but is still held back by a separate engine defect of its own. A redirect chain answers with the FINAL response, and a same-document navigation still answersNone, as upstream does. Crawlee treats aNonehere as a failed load, so aPlaywrightCrawlerdriven by this package failed every request; it now finishes them.
- The engine is
firefox-29, throughinvisible-core 29.19.0. It carries the screencast thatpage.screencast.start(on_frame=...)needs, so the feature shipped in 0.13.0 now has an engine that answers it, and the full-window frame no longer carries the Windows resize border. Nothing else moves: the pin follows the seal.
page.screencast.start(on_frame=...)streams JPEG frames of the browser WINDOW.page.screenshot()gives the content viewport and can never show the pointer, which is drawn in the chrome document precisely so that no page can see it. The screencast captures the window through the operating system in the parent process - tab strip, address bar and pointer included, nothing injected into the page - and hands each frame toon_frameas JPEG bytes with the captured window's size. Needs an engine fromfirefox-28on;size=bounds the frame (scaled down to fit, never up, default 1280x800) andquality=the JPEG quality (default 80).path=(a video file) is refused by name: the engine has no video encoder.
- Two threads writing the Juggler pipe could splice two commands into one
line. Frames are acknowledged from the reader thread while the caller's
thread dispatches input; the pipe now has a write lock, and a
Connection.postthat sends without waiting for the reply.
- Orphaned
.tmp-<tag>-<pid>trees in the engine cache are swept. Throughinvisible-core 27.19.0: a download killed duringverifyingorextractingused to leave up to the whole extracted tree behind, because only a fresh download by the same pid ever removed it.ensure_binarynow removes the trees whose process is gone, on every call, and leaves alone this process's own tree, a live process's tree and anything not named after a pid. The MCP server 0.13.0 starts the download with the process, which made a killed download an ordinary event rather than a Ctrl-C.
- The launch counter lives on the engine's repository. Every session now
declares
invisible_firefox.usage_ping.urlthroughinvisible-core 27.18.0, pointing at theusage-counterrelease offirefox_antidetect_patch, the repository that builds and ships the engine. The engine's compiled default says the same from the next build. Engines older than firefox-21 do not read the pref and are not counted. To opt out of the ping, setinvisible_firefox.usage_ping.enabledtoFalseinextra_prefs. - The
browser launchesbadge is back. It was removed in 0.7.2 because the counter behind it lived on a repository that had been deleted, so the badge would have redrawn a frozen number every morning. The counter has lived on this repository since 2026-08-19 and is counting again, so the figure the badge shows is live. It is cumulative, and it undercounts: engines before firefox-21 still ask the old address, and they rejoin only as they upgrade.
- A persistent profile keeps its cookies and storage for the next session.
Since 0.8.0,
profile_dir=kept nothing: the first session on a profile set a cookie and a localStorage entry and read both back, the second session on the same profile read an empty jar andNone, and on diskcookies.sqliteheld zero rows. The persistent launch handed back an ordinary context, a Juggler container with its own userContextId, and Juggler deletes a container's cookies and storage together with the identity when the context is removed; everything a session wrote lived in a container that died with the session. The persistent context is now the browser's default one, the one whose state is the profile's, addressed by omittingbrowserContextIdand never removed; closing it closes the browser, as upstream does. Found by a clean-environment check of 0.12.0, on the day after the docs page promised "logging in once instead of every run".
- A test that a persistent profile keeps state, e2e: two sessions on one
profile, cookie and localStorage written in the first and read in the second,
and the cookie row read from
cookies.sqlitein between, so a regression cannot pass by keeping the data where the page sees it and the disk does not. Against 0.12.0 it fails on the on-disk read.
- The first launch on Windows works. On a machine that had never run the
engine from that path, the first
InvisiblePlaywright(...)exited with3221226505(0xC0000409) before the protocol came up, and the second one worked. It was Firefox's launcher process, not the browser: on a new pathfirefox.exeruns as the launcher first and forwards the Juggler pipe to the real browser, and it read that pipe from CRT descriptors 3 and 4, which the Node.js driver provides and this client does not, since it names the handles inPW_PIPE_READ/PW_PIPE_WRITE. The launcher died on_get_osfhandle(3); the next launch found a launcher timestamp with no browser timestamp in the registry and disabled the launcher for that path, which is why it failed exactly once per machine and why nothing about the cache directory mattered. The engine is firefox-27, where the launcher forwards the handles it is handed. Every Windows first launch since 0.8.0, the release where this client replaced the Node driver, had this; the daily Windows install check had been red since 2026-08-31. (#144)
- A test for the first launch from a path Firefox has never run from, Windows only, e2e. It removes the launcher's registry values for the binary under test, launches once, and checks both that the page answers and that the launcher recorded a start followed by the browser's, so a success with the launcher disabled, the path that always worked, does not pass. Against firefox-26 it fails with the exit code above.
- Engine pin moved to firefox-27 through
invisible-core 27.17.0.
- A screenshot photographs where the page is. The clip was
{x: 0, y: 0, width: innerWidth, height: innerHeight}, and that clip is in document coordinates: not the window, but the top corner of the page at the window's size. An agent that scrolled and looked got the same image every time, and nothing in the image says so.full_pagewas not read at all: 800x600 came back where 800x4000 was asked for. (#136) - Two manual workflows stop calling the removed
pathcommand. Both calledpython -m invisible_playwright path, a subcommand removed when the CLI becamefetchplusversion. The Windows launch matrix failed in all three cells before reaching its launch probe, and the WebRTC gate had not run since the removal. The existing parser gate scanned Python tests only, so neither call site was in its perimeter; there is now one that reads the workflows too. Thanks to @SamadhiFire for finding and fixing it. (#141)
- A check that asks the index whether the version a change proposes is still free, on pull requests. This release exists partly because that check found its own repository in the state it exists to prevent: 0.10.0 was on the index while main sat on 0.10.0 with fifteen commits on top of the tag, the screenshot fix among them. A release from there would have published nothing and reported success, because the publish workflow treats an already-present version as a deliberate no-op, which is right for a re-pushed tag and indistinguishable from somebody forgetting to bump. (#142)
- Twelve pages stop shipping code that cannot run as pasted, tool metadata is out of the shipped screenshots, and the front page names the agent layer that sits on this engine. (#137, #138, #139, #140)
-
Nine acting methods on
ElementHandle.click,hover,fill,type,press,focus,check,uncheckandselect_optionnow work on a handle, sopage.query_selector("#go").click()does what it does in Playwright. Until now the driver served fourteen handle methods, every one of them a read, and every action answeredElementHandle has no method 'click'.They go through the same humanised path as the selector versions - approach, hover, press, release, with the hit-target check and the actionability retry - rather than a second, simpler one. Two click paths would be two fingerprints. What differs is only what should: the node is not looked up again, and the handle is still usable afterwards.
-
Two pages said there is no MCP server for this project. Both were written on 2026-07-27 and the server was published on 2026-08-19: true when written, and afterwards they steered readers to Microsoft's server, which cannot carry the seeded profile, for a problem ours solves. The integration page now opens with ours and is what it actually is - how to point Microsoft's playwright-mcp at this engine if you are already on it, and what that costs.
-
The README never mentioned either sibling. A reader who would rather prompt than script had one hand-off, to the page above, which told them no server existed. There is now a short block after Install with both one-liners, and the Related projects section lists the three siblings before the third-party ones.
-
The Python floor was stated only as a badge image about 200 lines below the install block, and the first-run download size disagreed across three READMEs, all three wrong against the packaged seal. One home for it, here.
-
Four wiki pages edited in a working tree on 2026-08-30, rescued onto a branch and then lost a second time when the branch was never merged. Landed.
- The English-only gate was written for this repository on 2026-08-27 and wired nowhere: not into tests.yml, not into the pre-push hook. Both sibling packages copied the script and the CI step; this one kept only the script. Run here for the first time it found four Italian test files, now translated, and it runs in CI from now on.
-
wait_for_timeouthas never waited. A request for 2000ms returned in 1ms, in both the sync and the async API and in every caller of either, for as long as this package has had its own driver. Nothing raised, so nothing showed.The defect was in
_juggler/server.py, the driver written here to replace the Node one:op_wait_for_timeoutreadparams.get("timeout"), a key the client never sends for this call, so the sleep was always zero. The client sendswaitTimeout, which looks like a typo and is not - upstream Playwright declares this one parameter under its own name becausetimeoutis the reserved key on that channel, carrying the per-call action timeout for every other call.⛔ The first attempt patched the vendored client instead, to make it send the key the server happened to read. That is a fix at the symptom: it leaves the defect in the code this project owns, the next re-vendor of upstream would silently undo it, and it committed a false claim about the protocol into a comment, this changelog and a test. Review caught it before it shipped.
What it cost, where it was found. A corpus run against real sites reported three of them as blocked by three different anti-bot products, one with the challenge markup sitting in the document; all three serve their real page once the wait exists, so those verdicts were artefacts of the tool rather than measurements of anything. A fourth site was reported as never loading and loads in twenty seconds. In the MCP package a polling loop of sixty one-second waits ran to completion instantly, so a test with a sixty-second budget failed in fourteen for no reason anybody could see.
⛔ This changes timings for existing callers, which is why it is a minor bump and not a patch. Code that called
wait_for_timeoutand returned instantly will now take the time it asked for. That is the documented behaviour of the API and the old behaviour was silently wrong, but a script that waited in a loop will get slower rather than faster.
-
Pins
invisible-core26.17.0, which seals firefox-26. That engine moves every input handler in the visible-pointer overlay out of input delivery. Until now only themousemoveone had been deferred; the press handler was still adding a class, CREATING a DOM node, attaching a listener to it and appending it - which starts a CSS animation - synchronously in the parent process while amousedownwas in flight. That is more work than what had been removed, and on the click path rather than the move path.The move handler's cost was measured: with the overlay on, the median gap between the
mousemoveevents THE PAGE receives moved by a whole clamp step, 18 ms against 17, p = 0.0005. The press path was never measured, because the bench that found the first one injects and listens formousemoveonly - it was watching the branch that had already been fixed. The remedy here is the same rule applied to all four handlers rather than a second special case: a handler records numbers and asks for a frame, and one place draws.⛔ The timing gain is NOT measured. A press-path bench now exists and refuses to run on a loaded machine, which is all that has been available. This is a fix with a mechanism and a measured precedent, not a number.
-
The core it pins also stops the pre-push hook from naming one cause for every kind of publish-gate refusal.
-
The pinned engine moves to firefox-25, which has no updater. Until now every session showed an "Update available" badge in the toolbar shortly after start. No preference could remove it:
app.update.autostops the download rather than the notification,app.update.suppressPromptsonly delays the doorhanger because the badge is shown immediately and unconditionally, andapp.update.disabledForTestingis inert unless the browser is driven by Marionette or the remote agent, which this package is not.The engine is therefore built without the update machinery at all: the build options are gone rather than switched off, so nothing checks for an update, nothing can install one over the binary the seal pins, and the badge has no code left to draw it. The update channel is unchanged, because the application's remoting name derives from it.
One consequence is worth stating: a retail Firefox contacts Mozilla's update service at startup and this build no longer does. No page can observe that - no API exposes the updater's state - but anything watching the connection can.
-
proxy=with anhttporhttpsscheme was accepted and ignored. The page went out on the host's address while the timezone, the locale and the WebRTC candidate had all been resolved through the proxy, so the session announced one country and connected from another. That is worse than having no proxy: it is the mismatch this package exists to avoid, produced by the package. SOCKS was unaffected, which is why it went unnoticed.The cause was three ways of expressing one thing, with the scheme choosing between them: SOCKS wrote
network.proxy.*preferences, HTTP was handed to the Playwright driver to route per channel, and HTTP without a driver got different preferences again. Removing the Node driver removed the middle one and nothing said so. The other two are now gone rather than repaired: the endpoint is read once and routed by the engine command, for every scheme. A proxy that cannot be expressed refuses the launch instead of starting a browser without it. Reported from outside, with a 24-site case study behind it. -
SOCKS proxies that require a username and password could not connect at all. The credentials reached the engine's channel filter and stopped there, so the browser offered "no authentication" and the proxy closed the connection. The symptom was a connection refused, which reads like a dead proxy rather than a missing password. Needs the matching engine release.
-
profile_dir=raisedKeyError: 'browser'. A persistent context never opened: the reply carried the context and not the browser, and the client reads both.
- Pins
invisible-core24.16.0, which seals thefirefox-24engine. The proxy credentials fix lives in the engine, so on an older binary that one is inert.
- The Node driver.
invisible_playwright._driver(6 MB of vendored JavaScript) and thenode.exedownloader are gone, and the browser is now driven from Python over the Juggler protocol directly. The wheel goes from 1.6 MB to 0.7 MB and the installed package from 11.2 MB to 6.5 MB, and a first install no longer downloads a 92 MB Node runtime - it fetches the browser and nothing else. invisible-playwright show-trace. The trace viewer is a Node application and left with the runtime that ran it. Traces themselves are unaffected: they are still recorded, andplaywright show-tracefrom an ordinary Playwright install opens them.
- Pins
invisible-core23.16.0, which seals thefirefox-23engine. That is the engine carrying the two fixes below: both need it, and on an older binary the Python half of each is inert. - The visible pointer overlay is ON by default. It draws the Windows arrow,
with the package logo's green halo around it, in the browser's own chrome
window - which the page cannot reach, so no site sees a difference either
way. What it changes is what a person watching the screen sees. Pass
show_cursor=Falsefor the previous behaviour.
- A teardown that was cancelled left the browser running.
asyncio.CancelledErrorinherits fromBaseException, not fromException, so the guard around each close step never caught it: cancel the task and teardown stopped wherever it had reached, skipping every later step including the one that reaps whatever the close did not. A cancellation is now caught, kept, and re-raised once every step has run, so it still propagates and nothing is left behind. Reported by DatGuy1 in #104. - A browser that had opened a few hundred pages stopped delivering events
entirely while still answering commands, so the next
new_page()timed out waiting for a session that had already been announced. Event delivery was a chain of nested calls that grew by three per page and never shrank; past roughly 330 pages it crossed Python's recursion limit, and the failure was swallowed by the handler that keeps a bad callback from killing the connection. Delivery is now flat and subscribers are removed when their page closes. - A closed page is now disposed, so neither the client's nor the server's object registry grows for the life of the browser.
- A CSS query on a page returned the browser's own form widgets. Firefox builds
the controls inside
<input type=date|time>in a shadow root it marks closed, and the engine handed those to automation along with the closed roots a page had authored, so the selector engine collected them as if the page had written them:page.locator("button")answered 2 on a document with one button, and the extra match was invisible. The damage was not the count. A click on the invisible match reported success and sent nothing, so a site that had not blocked anything looked like it had. Needs the matching engine release; the guard isElement::GetShadowRootForBindingsrefusing UA widgets, which is what the sibling API has done upstream since bug 2035665. Reported from outside, with a 24-site case study behind it. Response.text()andResponse.body()read the body again. The command they rest on had been removed from Juggler while trimming it, and two more callers used it without saying so: traces and HARs recorded with embedded content were being written with every response body empty, and nothing raised. Bodies cost memory again, bounded as upstream bounds them: 100 MB per tab, 10 MB per response, oldest evicted first. Reading one from inside apage.on("response")handler now waits for the request to finish instead of racing it, which is why the main document used to fail where subresources did not.
- Pins
invisible-core21.16.0, which pins thefirefox-21engine. - The CI font gate writes its preferences into the profile instead of sending them
over the protocol. From
firefox-21the engine refusesBrowser.enablewith auserPrefsfield rather than applying it late: preferences that arrive after startup mean the first launch initialises graphics and fonts with the defaults and the second with the stealth values, two different code paths. Nothing changes for users of this package - the vendored driver already writes them into the profile before startup.
- The Playwright client is now vendored inside the package.
invisible_playwright._pw(client) and_driver(Node driver) ship with the wheel, andplaywrightis no longer a dependency: the code that runs is the code we ship. The vendored copy carries four changes over upstream 1.61.1 -set_contentwaits for load the way a driven page needs, ~643 KB of unused subsystems removed (android, electron, bidi, recorder, chromium, webkit),_exposeConsoleApineutralised, andconsole.debugdropped from injected code. It is Apache-2.0 inside an otherwise MIT package;pyproject.tomldeclaresMIT AND Apache-2.0andTHIRD_PARTY_FORK.mdrecords the provenance. - Pins
invisible-core20.16.0.
- macOS is no longer supported. Releases stopped at
firefox-20; the CI builds Windows and Linux only. On a Mac the package now refuses at launch with a message that says why, instead of trying to download a binary that will not exist. Already published macOS assets are untouched.
- Without a proxy the egress IP was discovered twice before the browser started - two identical requests to an external service, from the real address, where a real user makes none. It is discovered once now.
pyeeandgreenletare declared explicitly. They were transitive dependencies ofplaywright; removing that dependency removed them too, while the vendored client still imports them, so the wheel installed and then failed to import.
-
Pins
invisible-core20.15.0, which carries a fix for a defect that lived in 17 published core versions. Anhttp://orhttps://proxy handed tobuild_launch_planproduced NO proxy preference at all:configure_proxyreturns a non-SOCKS endpoint to its caller, because only Playwright can answer a proxy's 407, and that path launches the binary withsubprocessand had nowhere to put it. The browser then went out on the machine's own address while the geo layer had already resolved timezone, locale and egress THROUGH the proxy, so the session announced one country and connected from another.This wrapper was never affected and needs no change on your side. It hands the endpoint to Playwright, and that was measured: the same exit IP as curl through the same proxy, in http and in socks5. The fix matters if you also use
invisible_core.launch.build_launch_plandirectly.
- The
browser launchesbadge. The counter behind it was a release asset hosted on a repository that was deleted on 2026-08-18, so the series is frozen on its last real value and the renderer would have redrawn that number every morning. A frozen figure presented as current is worse than no figure. The history is untouched and the SVG on thebadgesbranch stays, because already-published PyPI pages are serving it.
- Pins
invisible-core20.14.0, which seals firefox-20. The engine release is the memory one: the same fingerprint at the same seed, the same detector verdicts, less RAM and less CPU. The font faces stopped being copied twice on their way into the shadow list, the bundle is opened by path instead of being held in the heap, and the.ttcaccounting now counts the faces we DECLARE rather than every face the file happens to carry.
- This entry itself was missing until 2026-08-18. 0.7.1 shipped to the index on
2026-08-17 and the changelog stopped at 0.7.0, so for a day the released
version was undocumented. It was not caught by the release: the guard that
compares the index against this file,
test_the_changelog_documents_every_ version_it_claims_to_coverintests/test_release_e2e.py, is markede2eand the default selection deselects it. It fired on the first full e2e run after the release, which is the run that happens for a reason unrelated to releasing.
- Pins
invisible-core19.14.0, which seals firefox-19. The engine release carries aseal.jsonasset for the first time, so the seal a client verifies and the seal the build produced are the same bytes rather than two things that agree. session_kwargsin the test bench no longer pins the proxy exit. It used a literal session id, and the providers are sticky on it, so every realness measurement this project made left through the same address: one distinct IP in twenty-four hours, eleven events on it in one hour, and the detector had classified it as a datacenter. The same binary on fresh exits scores 0, 3 and 0 where it scored 14. Consistency runs still pin one exit for their pair, because that comparison is about our own determinism and a rotating exit puts the network inside it.
scripts/ci_font_gate.pyasked a question whose answer could not mean anything. It launched the engine raw, which since the generic-family map became a declaration means launching it without one - and the engine does not invent declarations. And it inferred "did this face load" from a line height that we declare: it worked while the fallback measured 91 and the families 95, and stopped the day the metrics became declared and the fallback landed on 95. It now hands in the one declaration it needs and asserts which families SHARE a face, measured from the ink box. Green on this build, on the previous release, and on both platforms.
- Two e2e tests for failures the suite could not see: a session that dies under heavy text shaping, and a frame reporting its parent's screen origin instead of its own. Both validated by reintroducing the real defect in the engine and rebuilding, not by reasoning about them.
- Pins
invisible-core18.13.0, which puts an upper bound in wall-clock time on the engine download. Arequeststimeout is per socket operation, so a connection delivering a byte every 59 seconds satisfied it forever and the transfer had no total limit;INVISIBLE_DOWNLOAD_DEADLINE(default 1800s) bounds it, and the refusal names the deadline, the elapsed time and the bytes received. Set it to 0 on a genuinely slow link. - README: the Telegram invitation moved to the top and the status badges to the bottom.
browser.new_page()now gives the page the same contextbrowser.new_context()does: the profile's viewport, screen, DPR, colour scheme, locale and timezone. It did not, because Playwright'sBrowser.new_pageforwards to the IMPLEMENTATION object, whose ownnew_pagecallsnew_contexton itself, so a wrapper installed on the api object was never consulted. Measured in a real browser with the seed the e2e uses:new_pagegaveinnerWidth1280, Playwright's stock viewport, against a fingerprint reportingscreenWidth1920, whilenew_contextgave 1906; a dark-pinned profile came back light on the same path.new_pageis the call in this package's README, in the class docstring and in every example it ships, which is why this is a minor bump and not a patch.- The engine-mismatch message told you to run
fetch --force, three days after that flag was removed with four of the six subcommands. A test pinned the old string, so both were wrong together. - Four tests marked
unitdrove__enter__with Playwright mocked and made two real network calls each, then sat out the lifetime guard's full 10s deadline waiting for a browser tree a mock never produces. 45.6s to 0.34s for that file. CI-only.
- Requires
invisible-core==18.12.0, which carries one prefs composition for all three entry points (compose_session_prefs), aproxy=argument onget_default_stealth_prefsthat had no way to reachconfigure_proxybefore, a proxy endpoint with no port refused rather than dropped silently, and a process scan that no longer asks psutil for the parent of every process on the machine. build_prefsdelegates to that composition, so the three entry points that used to stack layers on top oftranslate_profile_to_prefsin their own order now agree, with the difference asserted as an exact set. What stays here is this path's delivery and its two decisions: the cloak, which only Windows and macOS need, and which generator draws the pointer path.max_seconds_foris applied here rather than passinghumanizethrough, becausehumanize=0with the binary engine selected is a cap of nothing, not a request to disable motion, and passing it straight to the core would make it falsy and switch the generator off.- Eight imports in
launcher.pyandasync_api.pywere unused and are gone. The ninth,IANA_TO_POSIX_TZin_session.py, is not dead: the import IS the probe that turns an old core into a message about a version instead of a message about a symbol, so it keeps anoqacarrying the reason. datetime.utcfromtimestampis replaced in_recaptcha_seed.py. It returns a naive datetime and has been deprecated since 3.12, which matters now that the matrix reaches 3.14.- CI runs 3.11, 3.12, 3.13 and 3.14 on Ubuntu and Windows, every version
requires-pythonpromises; two of them had never run here. ruff selects F601 beside F821 and F811, because a dict literal with a repeated key silently keeps only the last one and Python does not warn. - ruff is declared in the dev extra instead of being pip-installed unpinned on every run, so the tool that gates every push cannot change under us between two pushes.
pytest-mockandresponseswere declared and used by no test in this package, and are dropped. - Publishing is a workflow on a tag push using PyPI trusted publishing (OIDC, environment
pypi), so there is no long-lived token on a machine or in a secret. The upload sits behind a gate that checks three things a person had to remember: the tag names the version being built, theinvisible-core==pin is already on the index, and the suite passes, since a tag push does not trigger the ordinary test workflow. It needs a GitHub publisher registered on PyPI before its first run; until that exists the upload fails withinvalid-publisher, which is the correct failure.
- The CLI is two commands,
fetchandversion.path,clear-cache,doctorandfetch --forceare gone, and so is the tag argument. Breaking for anybody scripting the four that were removed - they fail loudly rather than being ignored, and a test asserts that. None of the behaviour is gone. doctorruns insidefetchnow, on every run and before the download rather than after. That ordering is the point: a cached tree that no longer matches the seal is the case worth catching, and it is invisible to a "download if missing" that only looks at whether a file exists. It was the thing most worth doing and the thing least likely to be typed.--forceis unnecessary once every run verifies: a tree is replaced because it does not match the seal, not because a flag was passed.pathis the last line offetch's stdout, so$(invisible-playwright fetch)is the scripting form, and unlikepathit guarantees the thing it names exists and matches. The mismatch report goes to stderr so it can never end up inside a captured path.clear-cacheis deliberately NOT folded in. The cache root is shared withinvisible_firefox, so pruning trees no seal points at would delete the other product's engine on a machine running both.versionprints the location instead.- The tag argument went with them: the seal decides which engine a build runs and
verify_enginerefuses anything else, so a tag on the command line could only ever name something that would then be rejected. - The surface is asserted as an EXACT set, not a subset. A subset check passes while the CLI grows back one convenience at a time, which is how it reached six.
- Requires
invisible-core==18.11.0. 18.10.0 classifies a 404 on the engine archive instead of raising a bareHTTPError, which is issue #51, and 18.11.0 removed the part of it that ranpip install --upgradeon the caller's environment: a library that installs things while it is running mutates an environment nobody asked it to touch, ignores whatever lockfile chose that version, and inside a container rewrites an image layer at runtime. What reaches a user of this package is that a 404 on the engine now says whether the release was retired on purpose, in which case no retry will find it and the message carries the upgrade command, or whether the tag is current and the fault is ours. Nothing in this package's behaviour changed.
- The README said the sampler draws "~400 fields". Measured against a real profile: 197 leaves on the most generous count, which treats every bundled font name as a field, and 155 prefs emitted. Corrected to ~200.
tests/test_readme_claims.pynow gates the engine version, the platform list, the download size, the documented subcommands in both directions, and every fenced Python example on the page - four claims that had nothing checking them. - A publish was racing its own verification: the install e2e runs on
release:, which fires seconds after the upload, and the index does not serve a new version to pip immediately, so the job reported a forgotten publish that had happened two minutes earlier. A bounded five-minute poll on the per-version endpoint, scoped to the release event. CI-only.
- Requires
invisible-core==18.9.0, which carries a clearer engine refusal (it used to locate a missing juggler inside anomni.jathe tree does not have) and a publish gate that no longer reports a mistyped command with the same exit code as a broken gate. An exact pin means those reach a user only when this package's pin moves, so this release is what delivers them. - CI runs ruff with F821 and F811 selected, those two only: they catch a name that cannot resolve, which is not a style question and not findable by running tests. Nothing here had ever run a linter, and there are zero violations once the three below are fixed.
Rendererwas used as an annotation six times in_behaviour.pyand defined in no module in the repository. It survives only becausefrom __future__ import annotationsnever evaluates an annotation, so the six were strings that looked like a type and a reader had no way to learn what the parameter accepts. It is a real alias now.prof: Anyin_motion.py, withAnyitself unimported. Importing it would have widened that module's deliberately tiny allowlist, which one of its own tests enforces, and there was no need:profis the cumulative distance table_profile_tablereturns and_profile_atconsumes, both annotatedlist[float]a few lines away.- The install e2e imported the venv helpers from
invisible_core.testing, which the runner does not have on purpose, so the job went red at collection having tested nothing. The helpers are local to those two files again, with the reason above them, and the core's suite now parses every file the user-install workflows name and refuses the import. CI-only. - CI arms the hooks, because the suite asserts they are armed. The assertion moved into
invisible_core.testingand all three repos started making it, but only the core's workflow raninstall_hooks.py, so this repo went red on every push with a message about an unset config that reads like a developer's mistake rather than a missing CI step. CI-only.
- Requires
invisible-core==18.8.0, which carries the work of the last two days: the prefs builder split into fourteen named steps with its output hash-identical across 400 profiles, the GeoIP fetch moved out ofdownload,invisible_core.pinas a public module, and a publish gate whose index cross-check finally asks about the package it is gating. - Nothing in this package's behaviour changed. The imports moved: it asks
invisible_corefor what that package exports instead of reaching into its private modules, and its own back-compat shims are for users again rather than for its own source.
- The suite could not tell a collapsed selection from a pass in one place and the install e2e imported the package it tests the install of in another; both are CI-only and neither reached a user.
0.4.6 - 2026-07-27
- This package can no longer be published without a gate. A release used to be a bare
twine uploadof whatever happened to be in a directory - which is how 0.4.4 reached the index built from a tree that predated the fix it was meant to carry. The gate isinvisible_core.release, one implementation for all three packages, and the pre-push hook runs it on a release tag. What makes it work is thatpublishbuilds and uploads what it just built, so a stale directory cannot be what ships. PUBLISHED.jsonrecords what actually reached the index, back-filled from the artifacts themselves.- Requires
invisible-core==18.6.0.
0.4.5 - 2026-07-27
- An environment holding an older
invisible-corefailed on the browser launch path withImportError: cannot import name 'IANA_TO_POSIX_TZ' from 'invisible_core'- a symbol name, from a package whose version the reader did not choose. It now states which core version this build needs, keeps the original error inside the message for whoever is debugging, and ends with the command that fixes it. 0.4.4 has this bug and cannot be corrected in place; a PyPI filename is never re-uploaded. Use 0.4.5.
0.4.4 - 2026-07-27
- The process-lifetime machinery moved into
invisible-core, shared with the profile manager. Both packages launch the same browser and both had the same leak; the manager had it a day longer because the fix lived here.invisible_playwright._reaperstays as a re-export, so nothing you import changes. - The IANA-to-POSIX timezone table is the core's now. This package carried a byte-identical copy of all ten entries, including the Arizona row that exists because mapping it to
MST7MDTmade libc apply DST and an identification service deduce a Denver origin - and the core's own copy carried a comment admitting it had been copied from here. Two copies with a documented keep-in-sync obligation and nothing enforcing it.
- The lifetime guard counted an attempted job assignment as a successful one, so a process it had failed to adopt was never retried and the count it reported was a number of tries. Measured on the sibling package, that reported eight processes held while eight survived the kill. This package shipped the same accounting.
0.4.3 - 2026-07-27
async with InvisiblePlaywright(...)no longer leaves browsers behind when the runner is killed. The lifetime guard added in 0.4.0 reached the sync entry point only, so every async user kept the entire leak while this file said it was closed. If you drive this package withasync withon Windows, upgrading is the fix.- The guard itself stopped adopting the process tree after the first process it found, so part of the tree stayed outside the kernel job even on the sync path. Found by killing the runner mid-session and counting the browsers still carrying that session's token: sync 0/0/0/0, async 2/0/0/2 - the same code on both, differing only in timing, which is what made an intermittent race look like a working feature. After the fix: ten sessions, zero survivors on both.
INVPW_TRUE_HEADLESSnow works on both entry points. It was read in the async class alone, so a documented environment variable applied or did not depending on whether you wrotewithorasync with.
- The two entry points share their session logic instead of restating it: prefs, the launch environment and the IANA-to-POSIX timezone table live in one module both call. 80.6% of the async class was the sync class retyped, which is how a fix reaches one of them and a release note describes both.
- Requires
invisible-core==18.3.0.
- The lifetime guard is a strategy object instead of a module of flags.
SessionToken(a value object),find_processes(the only place psutil appears) andLifetimeGuardwith aJobObjectGuard/NullGuardpair:os.nameis tested in exactly one place, so no launcher code branches on the platform, and the Null implementation reports that it guarantees nothing rather than doing nothing while looking successful. Behaviour unchanged. - The stroke planner is six named stages instead of one 203-line function, and the idle planner is a table of episodes instead of an if/elif chain with the weights written as bare literals in the branch conditions. Both verified byte-identical against recorded output, which caught two float-associativity regressions that no other test could see -
a * b * canda * (b * c)differ in the last bit, and that is enough to change a rounded pixel and every draw after it. A permanent fingerprint test now covers 576 cases and 16135 waypoints.
- Three long functions became named stages, with their output pinned before and after so the refactor could not change it in silence.
- The lifetime guard became a strategy rather than a module of flags.
- The forbidden-name scan no longer blocks a clone that has no word list.
- Pointer movement is generated in this package, from the session seed, instead of by the engine. Existing code gets it with no edit: the six internal funnels every pointer action already goes through are wrapped, so a
page.clickwritten a year ago moves through the new generator. A plainsync_playwright()browser in the same process is untouched. - Movement between actions: drift while reading, motion while the wheel turns, overshoot and correction, and movements that end on nothing. If every movement ends on something clickable, the set of endpoints is itself a signature however good each path is.
INVPW_CURSOR_ENGINEselects the generator:python(default),binary(the previous behaviour) oroff.- The browser process tree is tied to this process's lifetime on Windows, so it cannot outlive a runner that was killed. Measured: eight survivors on the first attempt and twelve on the second before, zero after.
psutilbecomes a dependency for this - an optional reaper is absent exactly on the machines that need it, and silently. This reached the SYNC entry point only.async withkept the whole leak through 0.4.2 - see 0.4.3.
- Install is now
pip install invisible-playwright, from the index. The git URL is gone from the README, the CLI docs and the generated release notes. Nothing about the package changes for someone who was already installing it from git, except that pip can now see what it is holding. invisible-coreis declared as an exact version specifier (invisible-core== an exact version) instead of agit+https://...direct reference. A direct reference carries no version, so there is nothing forpip checkto compare and a broken environment reports clean; a real specifier is reported, exit 1, and a plain reinstall of this package repairs it. The cost is that a binary bump is now a release of every package that pins the core, which is deliberate.- Requires
invisible-core>=18.2.0, which bounds the timezone lookup as a step rather than only per request.
hover()no longer fails intermittently on Windows. The approach now completes before the automation layer's hit-target check is installed, so the only event inside its window is that layer's own move, on target. Measured on a page whose target needs a scroll: 3 failures out of 3 before, 0 out of 3 after.
- The browser tree now dies with this process even when the process is KILLED. An exception out of the
withblock was never the leak -__exit__runs and Playwright cleans up, measured over an interleaved A/B with zero survivors. The leak is the killed-runner path, where__exit__never executes at all: launch, kill the runner, and eight processes were still alive, twelve on the second attempt.
- Declared as
invisible_playwrighton the index, pinninginvisible-core==18.1.0.
- Playwright pin is back to
>=1.55,<=1.61.0: the floor drop to1.40shipped in 0.3.4 is reverted. The conservative CI-tested floor (1.55) is kept together with the tested upper cap (1.61.0).
- Playwright floor lowered to
>=1.40,<=1.61.0. The1.55floor was never a real compatibility bound - it was only the single minor that 0.3.1 had pinned. Thefirefox-18binary was smoke-tested (launch,new_context, navigate, evaluate, click) against 1.40, 1.45, 1.50 and 1.54 and passed on all four, so anyone on an older client keeps working; the upper cap stays at the tested 1.61.0. Reverted in 0.3.5.
- Playwright pin is now
>=1.55,<=1.61.0(was>=1.55,<1.62). The open upper bound would let an untested version below 1.62 install and break a fresh setup the way 1.61 first did; the cap is now the exact version validated against this binary, 1.61.0. It moves forward on purpose, once a newer Playwright is tested against the binary. - The expected Firefox version in the tests is derived from the constant instead of a hardcoded literal, so a binary bump no longer needs the test edited alongside it.
- Playwright pin moved to
>=1.55,<1.62(was>=1.55,<1.56). Thefirefox-18binary rebases onto Firefox 151, which is what the latest Playwright (1.61) pairs with, so 1.61 now drives the binary natively - no more pinning to an older client. Both ends of the range were tested against the new binary (full browser suite on 1.61; drift-free protocol on 1.55).scripts/playwright_pin.txt-> 1.61.0. - README badges are served as SVGs from the repo instead of a third-party badge service, so the header no longer depends on an external endpoint; the dynamic Firefox-version badge, which was the flaky one, is now static.
- The CI font gate checks that the whitelisted faces actually LOAD, not only that the family enumerates. Enumerating a font is not loading it, so the previous check could pass on a build that could name a family it could not render.
- #48: Playwright 1.61 adds an
isMobilefield to theBrowser.setDefaultViewportJuggler command that the FF150 binary does not accept, which kills the session. The range narrows from>=1.40,<1.61to the CI-tested 1.55.x (>=1.55,<1.56, single-sourced fromscripts/playwright_pin.txt) so a fresh install always gets a compatible client. Widened again in 0.3.2, oncefirefox-18rebased onto Firefox 151.
- The patched-Firefox source repo is now
feder-cr/firefox_antidetect_patch(it wasfeder-cr/invisible_firefox, a name the profile manager took over). Release notes, CI references and the release URLs the download tests mock all point at the new name; the binaries themselves are unchanged. - The wrapper no longer injects the font-list / system-UI environment variables at launch: the binary ships its own font bundle and is self-contained on that front. The integration tests were adapted and the font-sampling tests dropped.
scripts/ci_font_gate.py: asserts the Windows font persona on every OS, so a Linux or macOS runner catches a font regression that would otherwise only show up on Windows.
- Pure config (seed -> fingerprint -> prefs, binary download, proxy, geo) is split out into a standalone
invisible-corepackage with zero Playwright, so a profile manager can reuse it without pulling Playwright in. The wrapper depends on it and replaces the moved modules with full-alias shims: existing imports (public API, submodules, private names) andisinstancechecks keep working unchanged.tests/test_backcompat.pylocks that contract with 6 guards. BINARY_VERSIONwalksfirefox-7->firefox-13across this cycle:firefox-8, thenfirefox-9(firefox-8was found broken and is refused outright, not merely superseded),firefox-10,firefox-11,firefox-12(the cross-OS render-parity build) andfirefox-13(geo-aware locale/Intl + the Windows font bundle + the audio gate).- Playwright is capped at
<1.61(>=1.40,<1.61) and the pin is single-sourced fromscripts/playwright_pin.txtinstead of being written in two places. - WebGL personas: only the GPU buckets that survive the tampering checks are shipped, and the render-noise seed is decoupled from the persona seed.
- The dead
zoom.stealth.normalize_date_nowbaseline pref is dropped. - New runtime dependencies:
requests[socks](SOCKS egress lookup),maxminddb(mmdb reader),tzdata(IANA database forzoneinfo, which Windows lacks). After the split they arrive transitively throughinvisible-core.
timezone="auto": the browser timezone is auto-derived from the egress IP. By default (no explicit timezone) it ALWAYS resolves - from the proxy egress when a proxy is set, otherwise from the host's own public IP - so the zone can never disagree with the IP (the classictimezone_mismatchsignal). An explicit"Area/City"is the only way to force a specific zone. On failure: with a proxy the launch raises (no silent host-TZ fallback behind a foreign proxy); without a proxy it falls back to the host TZ so a transient lookup can't break the launch.- The egress IP is mapped to its IANA zone with an offline mmdb (
daijro/geoip-all-in-one). It always tracks the upstream weekly rebuild: on every launch the current latest release tag is resolved from thereleases/latest/downloadpermalink (no GitHub API → no rate limit) and pulled only if newer than the cache, older copies pruned. Offline → the cached copy is reused; never a pinned tag (daijro prunes old releases, so a pin eventually 404s).STEALTHFOX_GEOIP_MMDBpoints at your own.mmdbto skip the download. resolve_session_timezone(timezone, proxy)andensure_geoip_mmdb()re-exported at the package root (plusGeoTimezoneError) so integrations that own their launch can reproduce the resolution.tests/test_geo.py(37) +tests/test_geoip_update.py(freshness / auto-update / offline fallback) unit tests.- Cross-OS render parity (needs
firefox-12): the same font/canvas/WebGL fingerprint now renders consistently on Windows, Linux and macOS, so a Windows persona looks identical regardless of the host the binary runs on. Each whitelisted font renders a distinct canvas image (font-detection probes that dedup by rendered image keep every name), the standard Windows fonts (Calibri, Franklin Gothic, Gadugi, Javanese Text, Myanmar Text) are always present so the detected font set matches a real Windows install, and the per-seed render-noise leaves a solid-colour reference render byte-exact while still varying real fingerprint renders. - GPU persona applied on every platform: Linux/macOS hosts now present a coherent Windows GPU (renderer + WebGL parameters) instead of the host's real adapter; pool re-rooted on a real-device GPU mix.
tests/test_canvas_render_stealth.py,tests/test_webgl_noise_active.pyand newtests/test_sampler.pycases: regression guards for per-font canvas distinctness, solid-readback purity under render-noise, and the always-present standard-font invariant.- macOS support in the wrapper (x86_64 + arm64): the release pipeline builds five targets and the wrapper resolves the archive for the host it runs on.
- Headless is cloaked on Windows and macOS and runs under Xvfb on Linux, instead of the flagged headless mode; CI carries a cloak guard and a WebGL-masking guard. The cloak applies on the async path too.
- The e2e suite runs the real detectors offline (BotD, FingerprintJS, fpscanner, CreepJS, vendored under
tests/vendor/) and a hermetic SOCKS5 auth + routing e2e, all on CI on every push. The 15 hand-rolled BotD imitations are dropped now that the real one runs. fetch --forceon the CLI, to re-download an archive over a cached one.
- WebRTC behind a proxy ships the validated realness configuration, with CI guards: a fully blocked WebRTC is itself a tell, so the guard asserts the positive form and not merely the absence of a leak.
- The TLS ClientHello matches stock Firefox again: cipher
0xC009was being offered and stock Firefox does not offer it. - The humanize prefs were written under the wrong namespace.
- The font pool uses the real Windows 11 family name
franklin gothic medium; the previous name does not exist on a real install.
- Public config helpers in
invisible_playwright.config:get_default_stealth_prefs(seed, *, pin, locale, timezone, extra_prefs, humanize, virtual_display)returns a completefirefox_user_prefsdict;get_default_args()returns the baseline CLI args list (currently empty). Both also re-exported at the package root. invisible_playwright.ensure_binaryre-exported at the package root for parity with thecloakbrowser.download.ensure_binaryintegration pattern that downstream projects (Skyvern, Crawlee, agno) already expect.- These helpers let third-party fetchers (changedetection.io plugins, Crawlee
BrowserPoolsubclasses, agno toolkits) driveplaywright.firefox.launch(executable_path=..., firefox_user_prefs=...)themselves without depending on theInvisiblePlaywrightcontext manager owning the lifecycle. tests/unit/test_config_public.py: 14 unit tests covering deterministic seed, locale / timezone / pin / extra_prefs / humanize variations, and round-trip via the public namespace.
InvisiblePlaywrightcontext manager surface is identical (backwards compatible).BINARY_VERSIONstays atfirefox-7. Python-only release; no new Firefox build.
- #20: cross-origin iframes were unreachable from Playwright.
element_handle.content_frame()returnedNone,frame.evaluate()threw cross-origin SOP errors, andframe_locator(...).click()timed out even withforce=True. Root cause: FF150 defaultsfission.webContentIsolationStrategy=1(IsolateEverything), which site-isolates every cross-origin iframe into a separatewebIsolatedcontent process even whenfission.autostart=False. The parent's Juggler FrameTree then has a Frame placeholder with no docShell and no URL - every protocol op that needs to enter the iframe fails. Fix: pinfission.webContentIsolationStrategy=0(IsolateNothing) in the baseline prefs. The setting can be flipped back per session viaextra_prefs={"fission.webContentIsolationStrategy": 1}.
tests/test_cross_origin_iframe.py: 4 unit + 5 e2e regression sentinels for cross-origin iframe interaction. The e2e layer runs entirely offline against two local HTTP servers on127.0.0.1(two ports = two SOP origins) and coverspage.framesURL tracking,content_frame(),frame.evaluate(),frame_locator(...).locator(...), and end-to-enddispatch_event("click")for plain, sandboxed and titled iframes. A future FF upgrade or fingerprint A/B that flips the pref back to1will fail the suite before shipping.
BINARY_VERSIONstays atfirefox-7. Python-only release; no new Firefox build was needed.
- #18: Tab crash when running with
headless=Trueon Windows on pages that trigger cross-process navigation. Two separate bugs that only manifested together: (1) the Chromium content sandbox at default level 6 puts content processes onkAlternateWinstation, but the wrapper hides the browser window on its own alt-desktop (CreateDesktopfor headless on Windows). Mismatched desktops → cross-process navigations couldn't reparent windows → content process exits cleanly and Playwright firespage.on('crash'). (2) The canvas2dgetImageDatastealth spoof wrote to a read-only mappedDataSourceSurface. On GPU-backed canvases that memory is write-protected → segfault during the finalgetImageDataat page unload. Wrapper now setssecurity.sandbox.content.level=4in the alt-desktop workaround set, andfirefox-7ships the source fix that moves the noise to the JS array's writable backing buffer.
BINARY_VERSIONbumped fromfirefox-5tofirefox-7.firefox-6was rolled back when its partial fix turned out to be wrong (the iframe-burst hypothesis was a dead end; bisection in the evening found the real two-bug cause documented above).
profile_dir=kwarg onInvisiblePlaywright(sync + async). When set, the session usesfirefox.launch_persistent_context()so cookies, localStorage, sessionStorage, extensions, cache and prefs are kept on disk between runs.__enter__returns aBrowserContextdirectly:with InvisiblePlaywright(profile_dir=p) as ctx: ctx.new_page(). Pair with a stableseed=to also pin the fingerprint identity across runs. First run creates the dir; subsequent runs reuse it.
launch_persistent_context(timezone_id="…")no longer times out at 180s. Root cause:juggler/content/main.jscallsdocShell.overrideTimezone(...)on every navigation; the patched Firefox up to firefox-4 didn't expose that IDL method onnsIDocShell, so the call threwTypeError: docShell.overrideTimezone is not a function. On the non-persistent path the error fired after launch and was harmless; on the persistent path it blocked the launch handshake.firefox-5ships the C++ method (seepatch.mdsection 19); this release removes the firefox-4 era Python workaround that was filteringlocale/timezone_idout of the persistent context kwargs.
BINARY_VERSIONbumped fromfirefox-4tofirefox-5. The Python source delta is JS/Python only; the new Firefox build adds 50 lines of C++ indocshell/base/nsIDocShell.idl+nsDocShell.cpp.
- #15:
python -m invisible_playwright fetchraisedRuntimeError: no SHA256 for firefox-150.0.1-stealth-linux-x86_64.tar.gz in checksums.txtfor every user because the parser kept the*binary-mode prefix thatsha256sumwrites in front of filenames. Now.lstrip("*")is applied to the key. Reporter + patch: @LostBoxArt. Unrelated to thefirefox-Nbinary; existing caches still work, only first-time fetches were broken.
- #13: every page that threw an uncaught JS error (e.g. bunny.net) crashed the Playwright client with
TypeError: Cannot read properties of undefined (reading 'url'). Root cause: upstream Playwright Juggler added a requiredlocationfield to thePage.uncaughtErrorevent in the 2026-05-07 roll (microsoft/playwright@c8604ec); our fork was carrying the pre-roll schema in everyfirefox-Nbuild. Fix matches upstream - Runtime.js builds theerrorLocation, PageAgent.js forwards it on both worker and runtime error paths, Protocol.js declares the schema field. Reporter: @dionorgua.
BINARY_VERSIONbumped fromfirefox-3tofirefox-4. JS-only change insidechrome/juggler/;xul.dllandfirefox.exeare byte-identical tofirefox-3.
BINARY_VERSIONbumped fromfirefox-2tofirefox-3. The new archives on both Windows and Linux are built from a clean clone of feder-cr/firefox_antidetect_patch#stealth/150 - the consolidated source-of-truth fork (renamed fromfeder-cr/firefox; the companionfeder-cr/firefox-stealthpatches repo was deleted, all patches now live as commits on top ofmozilla-firefox/firefox).- The patched Firefox archive now ships the proper C++ implementation of
windowUtils.jugglerSendMouseEvent, replacing the JS shim from 0.1.2.
- C1+C2:
setDownloadInterceptorIDL + cpp (re-landed for FF150). - C4: 5
nsIDocShellstealth attributes (fileInputInterceptionEnabled,overrideHasFocus,bypassCSPEnabled,forceActiveState,disallowBFCache). - C5:
LauncherProcessWin.cpp+nsWindowsWMain.cppjuggler-pipe handle inheritance - without this, the Playwright pipe disconnects immediately on launch. - C6:
juggler-navigation-started-renderer/-browserobserver notifications innsDocShell.cppandCanonicalBrowsingContext.cpp- without these,Page.readynever fires andctx.new_page()hangs. - C7 (partial): storage stub for
nsIDocShell.languageOverride. WorkaroundInvisiblePlaywright(locale="")recommended until full BC FIELD port lands.
- Both archives built from same source: feder-cr/firefox_antidetect_patch commit
68906f1f9c55. - Windows + Linux smoke suite green: launch,
ctx.new_page(),page.mouse.{move,down,up,click,wheel},navigator.webdriver=false, sannysoft 32/33 PASS. - SHA256 published in
checksums.txton thefirefox-3release.
- This is the first release with a native Linux build of the patched binary (previous
firefox-3draft mentioned shipping the Linux firefox-2 archive byte-for-byte; that no longer applies - Linux now has the full C++ patch series).
BINARY_VERSIONbumped fromfirefox-1tofirefox-2. The patched Firefox archive on GitHub Releases now contains the JS fix from 0.1.1 (everypage.mouse.*/page.click()/locator.click()/mouse.wheel()failure on the FF150 binary). Users on 0.1.1 must runpython -m invisible_playwright clear-cache && python -m invisible_playwright fetchto pick up the new archive.
- Archive integrity tests on both platforms: Windows zip extracted + booted via Playwright (
mouse.move + click + page.click(selector)all succeed end-to-end), Linux tarball file-level checks (firefox/libxul.so sizes, byte-identity of patched JS files against Windows source). 21/21 assertions pass. - SHA256 published in
checksums.txton thefirefox-2release.
0.1.1 - 2026-05-18
- Critical: every
page.mouse.*,page.click(selector),locator.click(),page.hover(),mouse.wheel()failed on the patched Firefox 150 binary withwin.windowUtils.jugglerSendMouseEvent is not a function. The Juggler JS was porting calls to a Playwright-specific C++ method that was never landed in the FF146→FF150 port; replaced with the Mozilla chrome-scopewin.synthesizeMouseEventhelper which is present in FF150. Six call sites patched acrossjuggler/protocol/PageHandler.jsandjuggler/content/PageAgent.js. Reporter: @trob9 - #9. _linkedBrowser.scrollRectIntoViewIfNeeded()is now guarded at both call sites inPageHandler.js(dispatchMouseEventanddispatchWheelEvent) - the method is not present on the shipped FF150<browser>element, so the unguarded call threw before the mouse event was dispatched.
tests/test_mouse.py: 12-case regression suite covering every patched code path (mouse.move/click/dblclick/right-click, modifiers, locator.click/hover, wheel, manual mousedown+up, off-viewport move, humanize intermediate moves, scroll-and-click on offscreen element). Test cases inspired bymicrosoft/playwright-python/tests/async/test_click.py.- Community standards:
CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,.github/ISSUE_TEMPLATE/*,.github/PULL_REQUEST_TEMPLATE.md.
- The Stealthfox humanize Bezier expansion continues to fire intermediate
mousemoveevents; the swap tosynthesizeMouseEventdoes not change the human-trajectory behavior (verified by test). - The reCAPTCHA v3 score (0.90) and FingerprintPro / CreepJS results documented in the README are unaffected -
synthesizeMouseEventis a legitimate Mozilla helper that does not increase the anti-detect surface. - A binary refresh of the patched Firefox archive on GitHub Releases is required for users to receive this fix (the Juggler JS is shipped inside the archive). The
BINARY_VERSIONwill be bumped tofirefox-2in that release.
0.1.0 - 2026-05-13
- Initial public release.
InvisiblePlaywrightsync and async context managers - drop-in replacement forplaywright.sync_api.Browser/async_api.Browser.- StealthFox humanize hook: Bezier-curve mouse trajectories enabled by default.
_fpforgeBayesian fingerprint sampler with ~400 fields per session.- CLI:
invisible-playwright fetch | path | version | clear-cache. - Pinnable fingerprint fields via
pin={...}(seedocs/pinning.md). - SOCKS5 / SOCKS4 / HTTP / HTTPS proxy support with auth.
- Linux x86_64 and Windows x86_64 binary support.
The two oldest entries point at a COMMIT, not a tag: 0.1.0 and 0.1.1 predate arriving on PyPI (the index starts at 0.3.5) and never had a tag. Creating one today is not harmless:
publish.ymltriggers onv*, and for a version the index does not serve,already-publisheddoes not short-circuit, so a new tag could kick off a publish attempt. The commit is the same information without that risk.