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.
Humanized actions in the Playwright wrappers (Python, JS, .NET) now run through a
single shared engine built on Playwright's own selector engine: full locator support
(roles, chains, filters, frame locators, strict mode), Playwright options honoured,
value inputs operated with real input, persona-aware keys, and errors instead of
silent fallbacks. Puppeteer: frame-scoped actions, button/count, Shift on typos.
Follow-ups: macOS dropdowns via type-ahead, nested same-box element handles,
stale handles fail fast, CI fails on a missing test binary.
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.
* fix(download): make concurrent first-run downloads safe
_extract_archive deleted the install dir and then extracted into it in
place. When several processes or threads start on an empty cache, a
second caller can delete the first caller's install mid-extraction, or
pass the "binary exists and is executable" check on a binary that is
still being written. On Linux that surfaces as ETXTBSY on spawn; on
macOS as dlopen failures and "Directory not empty", and the cache can
be left permanently incomplete.
Extract into a uniquely named sibling staging dir and rename it into
place. If the destination already holds a complete install, another
caller won and the staged copy is discarded; an incomplete leftover is
still replaced as before.
* fix(js): make concurrent first-run downloads safe
Same race as the Python wrapper: extractArchive deleted the install dir
and extracted into it in place, so concurrent ensureBinary() calls on an
empty cache could delete each other's install or return a binary that
was still being written (spawn ETXTBSY on Linux).
Extract into a uniquely named sibling staging dir and rename it into
place, keeping an existing complete install if another caller won.
Also name the temporary archive with randomUUID() instead of
Date.now(), which collided for calls made in the same millisecond.
* fix(dotnet): make concurrent first-run downloads safe
Same race as the Python and JS wrappers: ExtractArchive deleted the
install dir and extracted into it in place, so concurrent
EnsureBinaryAsync() calls on an empty cache failed with "already
exists" / DirectoryNotFoundException or saw a half-written install.
Extract into a uniquely named sibling staging dir and move it into
place, keeping an existing complete install if another caller won.
* fix(download): replace a partial install safely under concurrent callers
When an incomplete install dir was left over from an interrupted run,
every concurrent caller saw it as incomplete and deleted it in place.
The first to finish renamed its staging dir into place; the others'
unguarded second rename then failed (ENOTEMPTY, FileNotFoundError,
DirectoryNotFoundException), and a late delete could remove files from
an install another caller had just moved in. With 8 concurrent callers,
7 raised in each round.
Rename the leftover aside to a unique <install>.stale-<id> dir and
delete it from there, then retry the staging rename a bounded number
of times, keeping a complete install if another caller got there
first. Applies to the Python, JS and .NET clients, each with a
regression test that starts several concurrent callers on a partial
install. The JS test uses separate processes, since callers in one
process cannot interleave inside the synchronous rename block.
* fix(download): retry directory renames Windows transiently denies
Antivirus and indexers on Windows briefly hold freshly extracted files
open, which fails the directory rename with access denied. Retry that
rename for about 2.5 seconds while the destination does not exist, in
all three clients. Cleanup of the staging and stale dirs is now best
effort so a failed delete never masks the error being raised.
---------
Co-authored-by: CloakHQ <cloakhq@pm.me>
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).
* fix(humanize): scroll the x axis in scroll-into-view (#521)
humanScrollIntoView only measured and scrolled the y axis. A target past
the right edge of a horizontally overflowing page counted as in view, so
no wheel events were sent, the click point landed offscreen and the
pointer check failed with "element is covered by <none>" after the full
timeout.
After the vertical pass, a horizontal pass now checks that the box fits
inside the viewport width and, if not, centres it with the same
humanized wheel burst on the x axis. It skips the scroll when the page
is already pinned at the needed horizontal boundary, mirroring the y
pinned check. Applied to Python (sync and async), JS Playwright,
Puppeteer and .NET.
.NET: IRawScrollPage.GetScrollStateAsync now also returns X and MaxX.
Fixes#521
* fix(humanize): handle RTL scrollX in the horizontal pinned check (#521)
In right-to-left documents Chrome's window.scrollX runs from 0 down to
-(scrollWidth - clientWidth). The x pass treated "scrollX <= 0" as pinned
at the left edge, so on an RTL page at its start a target in the left
overflow was never scrolled into view and the click failed with "covered
by <none>".
The scroll-state script now also reads the viewport direction (taken from
<body> when present, as Chrome does) and returns an RTL-aware minX and
maxX. Python, JS Playwright and .NET compare against those. The .NET
adapter shifts X and MaxX by minX so IRawScrollPage keeps its tuple shape
and X still runs 0..MaxX.
Puppeteer has no pinned check on the x axis and is unchanged.
* fix(humanize): skip the x pass for sub-pixel distances
A box wider than the viewport never passes the containment check, so
every action on it ran the x pass even when it was already centred: a
mouse move to the scroll area and a 0 or 1px wheel. Skip the pass when
the distance to centre is under 1px. Applied to Python (sync and async),
JS Playwright, Puppeteer and .NET.
* test(humanize): reword the set_content note on the #521 browser tests
The comment stated as fact that set_content never settles in
CloakBrowser. What I observed is narrower: it timed out in local runs of
these tests, so say that instead.
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.