Python async launchers ran the binary/license check and the GeoIP and
WebRTC exit-IP lookups on the caller's event loop, freezing every other
coroutine for up to 20s per launch. Run them via asyncio.to_thread.
.NET: license validation and Pro version lookups inside async download
methods now run via Task.Run instead of holding the caller's thread.
A persistent context arrives with a page already open. The license guard
stores its wrappers on that page object, and an object attribute shadows the
class methods humanize patches, so guarding first pinned Playwright's own
methods and silently disabled humanize on that page. Python (sync and async)
and JS now humanize first, then guard. The .NET wrapper already composes in
this order; new tests lock it in.
- tests: Python call-order tests, JS real-browser guard-on-humanized-page
test, .NET LicenseGuardCompositionTests
- remove internal support references from code comments and test docstrings
page.input_value / text_content / inner_text / inner_html / get_attribute and
dispatch_event now resolve and read through the humanize isolated world, like
every other humanized call, for page, frame, locator and element handle (sync
and async). The raw Playwright path stays reachable through page._original /
Original / Humanize.Unwrap.
- shared world helper library gains the inputValue / textContent / innerText /
innerHTML / getAttribute / dispatchEvent ops (byte-identical across the
Python, JS and .NET ports)
- engine read() / dispatch() (Python), read / dispatch (JS),
ReadAsync / DispatchAsync (.NET): deadline, resolve-until-match, one world
call, {value} / {error} mapping, Playwright error labels
- wrappers expose the six APIs on Page, Frame, Locator and ElementHandle;
generator delegation guards updated so they stay hand-written
- an ElementHandle read / dispatch on a hidden element, or one sharing its box
with another element, and a dispatch whose event_init holds a JSHandle, run
Playwright's own call, as before
- tests: Python unit and real-browser tests, JS real-browser engine tests,
.NET HumanReadsTests (values, late selector, null / world error / timeout
mapping, dispatch event shape, frame reads, hidden / same-box handles,
JSHandle event_init)
A free user whose browser crashed sees "close another running session"
with nothing left to close. The message now says how to close cleanly
and how long a crashed session holds its seat per plan.
Puppeteer reports a failed launch as 'Code: N' and .NET Playwright carries
no exit code at all, so an over-cap launch surfaced as a raw crash instead
of CloakBrowserLicenseError. Match Puppeteer's real wording and fall back
to the launch's denial file in the launch catch.
The dropdown search reads one UTF-16 unit per key and drops a surrogate, so
a typed emoji never moves it. The type-ahead plan no longer types one: Linux
and Windows reach such options with arrows. A macOS dropdown ignores arrows
too, so there the option raises a clear error before the popup opens.
The long-dropdown test now allows one input/change per letter on macOS.
SOCKS5 and credentialed HTTP proxies are set on the browser, so
context.request / page.request / route.fetch() send from the real IP.
Warn once on first use (Python + JS) and document in-page fetch.
Tests marked slow or real_browser skip the per-test temp cache and use
~/.cloakbrowser like a real install: the developer's key and latest
binary at a stable path. The temp cache re-downloaded the keyless
binary for every test under a new path, which path-keyed firewalls
re-prompt for. Unit tests stay isolated.
launch_persistent_context() now stores its seed in <user_data_dir>/.cloakbrowser-seed
on first launch and reuses it, so a profile keeps one identity across sessions.
Same file format in Python, JS and .NET. An explicit --fingerprint in args wins
and leaves the file alone; stealth_args=False skips it; non-persistent launches
stay random. Concurrent first launches publish without overwriting (link), so
they agree on one seed. Filesystems without hard links (FAT/exFAT, some network
mounts) fall back to an exclusive create, and an empty file is re-read briefly
as in-progress. A corrupt file is logged and regenerated.
Exports used only "import", so require('cloakbrowser') failed with ERR_PACKAGE_PATH_NOT_EXPORTED. A "default" condition lets Node's built-in require(esm) load all three subpaths. engines raised to ^20.19.0 || >=22.12.0, where require(esm) is on by default.
Stdout status lines corrupted MCP stdio servers and any caller parsing stdout on first run. Matches the banner, Python, and .NET. Tests assert nothing reaches stdout.
A letter typo on a digit is rejected by <input type=number>, so the
correcting Backspace deleted the previous real digit (1999 -> 999).
Digit neighbors are now digit-only in the Python, JS (Playwright +
Puppeteer) and .NET wrappers.
Layout can report a top edge of -1/64 px on an unscrolled page, which skipped
the pinned-at-top check and sent a pointless upward scroll burst before the
click. Allow 1px slack in Python, JS and .NET, with regression tests.
Also credit @Shub3am in README contributors (#554).
Under humanize, page.GetByText(...) returns our HumanizedLocator decorator. Playwright
APIs that internally down-cast an ILocator arg to the concrete Locator (as Locator)
threw NullReferenceException on it. Unwrap to the raw locator before delegating, matching
the existing DragTo/Or/And/SelectOption pattern.
Covers AddLocatorHandlerAsync (both overloads, callback re-wrapped), RemoveLocatorHandlerAsync,
Locator.Filter (Has/HasNot), and Page/ElementHandle ScreenshotAsync (Mask). Also adds
HumanizedLocator to LicenseGuard.Unwrap and a public Humanize.Unwrap(ILocator) escape hatch.
Python/JS unaffected (they monkey-patch in place, no wrapper type).
- Pro Stable to 151.0.7922.108.6 (Linux + Windows); macOS on 151.0.7922.108.3
- Patch count to 87; platform tables to Chromium 151 across all rows
- Document macOS persona and --fingerprint-transparent-proxy / --fingerprint-portable-cookies
- Font Setup: add Mac fonts block and Docker (/usr/local/share/fonts) paths
The download is signature/checksum-verified before extraction, so the
per-member path/symlink sanitization was redundant. Extract archives as-is
across all three wrappers (Python, JS, .NET), tar and zip.
- Python: filter="fully_trusted" (pins legacy behavior; 3.14 defaults to
"data"), plain zip extractall
- JS: drop the tar filter callback; inline the one-line extractTar
- .NET: TarFile/ZipFile.ExtractToDirectory; remove orphan ResolveSafeEntryPath
- Drop the obsolete traversal/symlink tests
Add a Mac fonts count to `cloakbrowser info` next to the existing Windows
font check, in the Python, JS, and .NET wrappers. Counts the macOS core
font set via fc-list on Linux and prints Mac fonts: n/20 with a conditional
nudge when incomplete. Tests added in all three wrappers.
The banner wrote '->' and em dashes straight to stderr. On a cp1252/strict
console (UK/EU-locale Windows) that raised UnicodeEncodeError and aborted the
binary-download path, blocking license apply and profile launch in the Manager.
Swap the banner glyphs to ASCII and route the writes through a swallow-all
helper so a cosmetic banner can never abort a launch. Same treatment in the JS
and .NET wrappers plus the Windows-font warning. Regression tests added across
all three (ASCII-only + cp1252-no-crash).
Increase the default GeoIP resolution timeout from 5 to 20 seconds across the Python, JavaScript, and .NET wrappers.
Fail the launch when requested GeoIP resolution times out, cannot load its database, cannot complete a lookup, or leaves timezone or locale unresolved. Explicit timezone and locale overrides remain valid without an egress-IP result.
Add parity coverage for the new timeout and failure behavior.
A supplied license key the server rejects, or one that cannot be validated
(server unreachable, no cache), now raises instead of silently downloading the
free binary. No-key (keyless) and valid-key (Pro) paths unchanged. Python, JS,
and .NET, with mirror tests in each.
The test inlines the resolver once per builder call, so adding the selector
engines took the script past 110 KB. Linux caps a single argv entry at
MAX_ARG_STRLEN (128 KB), so "node -e <script>" failed with E2BIG on CI while
passing on macOS, which allows a larger argument.
Write the script to a temp file and pass the path instead, which has no size
ceiling.
Removing the Playwright fallback closed a main-world read leak but left every
get_by_* locator raising UnsupportedHumanizeSelectorError. Reimplement the four
cheap engines in the isolated-world resolver so those locators work again.
Selector engines (shared resolver, byte-identical in all three wrappers):
- internal:testid and internal:attr (placeholder/alt/title). The i flag is a
case-insensitive substring on the raw attribute value; test ids stay strict.
- internal:text. Exact compares the full normalized subtree text, unlike the
public text= engine which compares immediate fragments.
- internal:label, resolving aria-labelledby, then aria-label, then .labels.
- internal:role and >> chaining stay unsupported; the error now names what is
supported and the workaround.
Text parity fixes in __elementText, checked against playwright-core:
- drop a <br> to newline branch Playwright does not have
- concatenate shadow-root text into the host, which Playwright does
- give the public text= engine Playwright's self/selfAndChildren rule, since
contains() cannot see into a shadow root and kept the host as a match
Target identity: payloads carry a per-world generation (protocol v2). The world
is rebuilt on navigation and its id counter restarts, so an id alone could name
a different element. Generation is bounded to Int32, which the .NET wrapper
requires.
force=True now skips the pointer check entirely, matching Playwright, instead of
suppressing only the coverage rejection.
Also: drop dead _BOX_OP, _ACTIONABLE_OP and readSnapshot, and add a test
asserting the three hand-copied resolver literals stay identical and free of
characters that would terminate their host string literals.
Verified against Playwright's own locator resolution in a real browser:
20/20 engine parity, 7/7 text parity.
Derive actionable geometry from rendered text and visible descendants when an element has no box of its own. Preserve existing geometry behavior for normal elements and exclude hidden descendant content. Add Python, JavaScript, and .NET unit and browser regressions covering text, nested content, visibility, and humanized clicks.
Port the proven selector resolution and actionability behavior to the .NET wrapper.
- preserve exact targets across delayed interactions
- align supported locator semantics and legacy fallbacks
- add regression coverage for ordering, mutation races, and scrolling
Port the proven Python interaction behavior to the JavaScript wrapper.
- align selector resolution and actionability semantics
- preserve exact targets across delayed interactions
- improve text, state, and open shadow tree consistency
- add regression coverage for ordering, mutation races, and force behavior
Collect light-DOM matches before recursively merging open Shadow Root
matches, preserving Playwright order for broad selectors using first or nth.
Add resolver and real-browser coverage for mixed and nested Shadow Roots.
Resolve#512 by ignoring non-rendered document content during text
matching and selecting the same visible target as Playwright.
Improve text, structural, selector-list, and open shadow-root handling.
Align element state checks and forced-click behavior, and prevent pointer
dispatch when the original target is detached or replaced during mouse
movement.
Add sync, async, and real-browser regression coverage.
The image already ships these fonts, but only as a transitive dependency of
openbox (openbox -> libimlib2 -> libspectre -> libgs10 -> fonts-urw-base35).
They are what Verdana, Georgia, Trebuchet MS and Tahoma fall back to; without
them fontconfig resolves those families to the CJK font instead of a Latin one.
Listing the package directly means a future change to the window manager or
X stack cannot silently drop the Latin fallback. No runtime change.
`cloakbrowser info` printed a bare seat count with no denominator, so a
customer could not tell whether they were at capacity (#513). It also
collapsed six distinct outcomes into one "unavailable": unreachable,
timeout, invalid key, inactive licence, rate limited, and the server
reporting the count as unknown while degraded.
Adds SessionSeats (active, limit, state, reason) and getSessionSeats to
all three wrappers; get_active_session_count keeps its signature and
delegates. The limit is read from the server, never derived from the plan
name, and falls back to the old bare count when absent.
Python, JS and .NET renderers verified byte-identical.
Persist _cloak_denial_path on the context object in
launch_persistent_context_async so the license guard has access to the
denial path after installation.
Humanized actions could fail with an element-not-attached error after a
navigation driven by a click or form submit, since the checks only refreshed
on goto. Now refresh on any main-frame navigation. Python, JS
Playwright/Puppeteer, and .NET. Regression from 0.5.6.
A fully-visible element above/below the target zone on a page pinned at
that boundary can never reach the zone, so the scroll loop ran the full
budget (~7s/click) doing nothing. Bail when it's fully visible and the
page can't scroll toward the zone. Ported to JS, Python (sync+async),
and .NET with regression tests.
page.selectOption re-dispatches to the main frame's selectOption, which
patchSingleFrame routes back to page.selectOption. Binding the saved
original to the page looped forever; bind it to the main frame's native
method instead. Adds a regression test.
_iter_frames / iterFrames only walked the main frame and its direct
children, so a frame nested 2+ levels deep that already existed when
patching ran was never humanized. The frameattached listener covers
frames added after patching, so this only affected pre-existing deep
frames (e.g. manual patch_page/patch_context on a loaded page).
Recurse the full frame tree; the _human_patched guard keeps it
idempotent. Python + JS; .NET wraps frames lazily and is unaffected.
Forward caller-provided press delays through humanized page, frame, locator, element handle, and keyboard paths across Python, JavaScript, and .NET.
Use each automation library's native press operation for shortcut chords while retaining the existing humanized pre-press timing and focus behavior.
Ports the Python/JS stealth_dom fix to the .NET wrapper. Add StealthDom.cs:
reimplements the common Playwright selector grammar (css, :has-text, text=,
xpath=, trailing >> nth=N) for direct DOM resolution in the isolated execution
context, with a fallback to the regular Playwright read for unsupported grammar.
World reads are wrapped so a world/CDP failure falls back rather than propagating.
The pre-click actionability, scroll geometry, viewport, and pointer-events reads
now go through it. .NET hooks the humanized click at the locator layer with no
selector string, so page.Locator(selector) now threads the selector into
LocatorHumanizer, whose reads use the isolated world when the selector is known
(else Playwright). HumanPage's selector path is wired the same way. GetBy*/chained
locators fall back to Playwright; ElementHandle and sub-frame paths unchanged.
Tests: StealthDom builders + a Node-driven check of the shipped resolver JS
selector semantics (byte-identical to the other wrappers), and a guard that
page.Locator(selector) threads the selector into the humanized locator.
Python + JS wrappers. Add a stealth_dom / stealthDom module: reimplements the
common Playwright selector grammar (css, :has-text, text=, xpath=, trailing
>> nth=N) for direct DOM resolution in the isolated execution context, with a
fallback to the regular Playwright read for grammar it can't resolve. World
reads are wrapped so a world/CDP failure falls back rather than propagating out
of the humanized action.
ensure_actionable, ensure_stable, scroll geometry (_get_element_box + the
no_viewport window-size read), and check_pointer_events now read through it,
sync and async. Selector-based main-page actions (click/dblclick/hover/type/
fill/focus/press) are covered; ElementHandle and sub-frame paths unchanged.
JS routes Locator actions through frame methods, so main-frame locator clicks
now delegate to the humanized page methods (which use the shared helpers)
instead of the frame-scoped Playwright reads; sub-frames unchanged.
Tests: builders + parse, the rewired helpers' branching via a mock isolated
world, a Node-driven check of the shipped resolver JS selector semantics, and a
guard that main-frame locator clicks delegate to the humanized page path.
- humanize frames created after page load across Python and JavaScript\n- keep sync, async, Playwright, and Puppeteer paths idempotent\n- wrap .NET frame lifecycle event payloads without raw frame leaks\n- add cross-wrapper unit and browser regression coverage
context.NewCDPSessionAsync(page) threw NullReferenceException for any licensed
.NET user: Playwright down-casts the IPage/IFrame argument to its concrete
Page/Frame to read .Guid, which fails on the CloakBrowser proxy the wrapper
returns. The license-guard proxy (all licensed launches) and the humanize
decorator both produced a handle that fails that cast.
- guard proxy unwraps page/frame/handle arguments before forwarding, keeps
page.Context guarded, and Wrap() is idempotent
- HumanizedBrowserContext.NewCDPSessionAsync unwraps its argument (covers the
keyless+humanize path with no guard proxy)
- HumanizedPage.Context re-wraps so page.Context stays humanized
- public Humanize.Unwrap(page/frame) escape hatch
- regression tests build the guard proxy and assert the inner receives raw
Wire the existing launch()-time geoip resolver into the info/doctor command
across Python, JS, and .NET. With --proxy, info resolves the exit IP and the
timezone/locale a launch would apply (caching the GeoIP DB if absent) and prints
them in text and --json. Plain info is unchanged (no network) and now hints at
the flag. Adds diagnostics tests in all three suites.
The license guard stored each wrapped method as a functools.partial,
relying on it not being a descriptor. Python 3.14 made partial a method
descriptor, so when humanize reads the wrapped methods off a holder
instance the partial re-binds and injects a spurious positional arg —
every humanized page call raised TypeError on first use. Replace the
partial with a non-descriptor callable class. Fixes#488.
An element scrolled into view can be pushed off screen again while we wait
for a reflowing page to settle. Nothing scrolled a second time, so the click
was dispatched outside the viewport and hit nothing, with no error raised.
Also stop the pointer-events check from turning a confirmed miss back into
'undetermined' when a late probe times out, which let that click through.
Measured on a page reflowing 10-25s: silent miss at ~32s before, clean click
now. Static pages and pages reflowing past the timeout are unchanged.
A concurrent-session denial resolves after the CDP handshake, so the browser
exits with a live connection already established. The launch-failure path never
sees it and the user gets a bare TargetClosedError on their first call (#477).
The binary records the license exit code (76-79) to a per-launch file whose path
the wrapper passes via CLOAKBROWSER_LICENSE_STATUS_FILE. The wrapper reads it
when a guarded call throws and re-raises the correct CloakBrowserLicenseError.
Discrimination is on the file, not the error type, so a genuine crash is never
mislabelled. Fail-safe (no key or unwritable dir -> feature off) and
backward-compatible (old binaries never write the file).
Guarded surfaces: new_page/new_context, a persistent context's already-open
pages (goto + the wait family), and Puppeteer user-created contexts. Orphaned
denial files are swept at mint time; an observed code is cached in-process so a
concurrent second call can't miss it; the .NET reader parses as tolerantly as
Python/JS.
Implemented across Python, JavaScript (Playwright + Puppeteer), and .NET, with
unit tests. Reproduced end-to-end with a real over-cap denial.
The Windows OS fonts were labeled optional/CreepJS-only, but Linux
defaults to the Windows persona, so their absence is a font-fingerprint
tell. List the exact fonts the launch warning checks and add a verify
step (issue #480).
Playwright's launch defaults switch off a feature stock Chrome ships enabled.
When the Windows font-metrics profile is requested, re-enable it so the feature
set matches a real browser rather than a test harness. Merged into any existing
--enable-features value instead of appending a second flag.
Python, JS and .NET wrappers.
Node pools HTTPS connections by destination, so a second launch() in the
same process reused the first proxy's tunnel to the echo service and
inherited its exit IP, timezone and locale.
cmd.exe defaults to cp850/cp1252, which carry no check mark or arrow.
Printing one raised UnicodeEncodeError and aborted the report at the
Launch line, so everything after it was lost — including on the success
path, once the launch probe stopped failing.
Marks now degrade to plain text per glyph when the console cannot
encode them. UTF-8 consoles (Linux, macOS, Windows Terminal) keep the
original output byte for byte.
.NET substitutes rather than throwing here (verified on the reference
box), so only the Python wrapper needed the change.
Chromium only handles --version on POSIX, so on Windows the switch is
ignored and a browser starts instead of printing. The 10s probe then
timed out and a healthy install was reported as broken, flashing a
window on screen each run.
Probe with --no-startup-window there: it exits immediately, opens no
window, and a broken binary still exits non-zero. No version is
reported on Windows, as nothing is printed. Linux and macOS unchanged.
Test uses a stub that hangs unless the flag is passed, mirroring the
real binary rather than a print-and-exit stub.
A license.key or cached Pro marker in the developer's cache dir resolved the
box as Pro, flipping version-gated defaults (headless no_viewport, inline proxy
auth) and failing 9 Python + 4 JS tests locally. Both suites now run against a
temp cache dir.
Add a preview release channel system to all wrappers (Python, JavaScript, .NET),
allowing users to opt into newer binary builds before they go stable.
- Python: preview/stable channel resolution in download, launch, CLI
(install, update, clear-cache), and license validation
- JavaScript: preview/stable channel in CLI, launch, license, and config
- .NET: preview/stable channel in CLI, license, diagnostics, and config
- All wrappers: channel fallback indicator, auto-update gate,
CLOAKBROWSER_RELEASE_CHANNEL env var support
- Harden channel resolution: reuse the resolution sidecar on offline lookups,
defensive marker/JSON parsing, atomic version-marker writes, a launch-time
preview-to-stable fallback notice, channel-aware info download URL, and
Python/JS/.NET parity for the fallback default
- Binary 150.0.7871.114.4 changelog (Linux x64 + arm64)
- Updated READMEs, CHANGELOG, and tests for all wrappers
Adds ChromePool.terminate_seed() + a close route that tears down one seed's
Chrome immediately and frees its slot, instead of waiting out the idle timeout.
Profile is preserved (goes through _cleanup_process). Idempotent.
Node derives Host from the proxy hostname unless set explicitly; strict
backconnect proxies reject the mismatch, silently falling back to the
gateway's geo instead of the real exit IP.
cloakbrowser login gets a free key via GitHub sign-in (or saves a paid key); logout reverts. Launch banner and info are now tier-aware (keyless / free / pro). A free key drops any version pin so it always gets the latest build (server force-serves latest; a pinned manifest would fail checksum). Python, JS, and .NET kept in sync. README documents the free tier + commands.
A raw --fingerprint-timezone/--lang/--fingerprint-locale in args is now
promoted to an explicit value in maybe_resolve_geoip, so geoip only fills
the values the user did not set. Mirrors the timezone=/locale= param path.
Python, JS, and .NET, with tests.
zsh globs the unquoted bracket, so a user copy-pasting the printed
install hint hits a shell error. Quote it in both messages; update the
test that pinned the old string.
geoip.py used Path.rename which cannot overwrite on Windows, so a stale
DB never refreshed and every launch re-downloaded ~70 MB in a loop. No
lock meant concurrent launches each fired their own download.
- Python: os.replace + threading.Lock (initial + background paths)
- .NET: File.Move(overwrite) + SemaphoreSlim guard
- JS: shared in-flight promise (rename already overwrites)
- tests: overwrite + concurrent single-download coverage
pyproject: bump classifier to Production/Stable
Points the READMEs at 150 as the latest Pro binary (71 patches, all five
platforms) and bumps the Pro major in the launch banner and the info
upgrade hint across Python, JS, and .NET.
A Pro license now gets a "Sessions: N seats in use" line, answering
"how many sessions do I have running?" without asking support.
Never cached (a cached count is a wrong count), skipped under --quick to
keep `info` network-free, and prints "unavailable" rather than a made-up
number when the count cannot be determined.
Python, JS and .NET.
The Pro binary exits with a distinct code per license failure; the wrappers ignored it, so a user got a bare "browser closed" error with no reason.
Add CloakBrowserLicenseError plus a launch-error parser that maps the exit code to a clear message, mirrored across the Python, JS, and .NET wrappers. Non-license failures pass through unchanged.