CloakBrowser for .NET
A complete, faithful .NET 8 / C# port of the
CloakBrowser Python wrapper - stealth
Chromium that passes bot-detection tests, built on top of
Microsoft.Playwright.
CloakBrowser is a thin wrapper around a closed-source, source-level patched Chromium binary (58 C++ fingerprint patches). This port reproduces all of the wrapper functionality with identical behavior - same launch flags, same proxy / GeoIP / WebRTC logic, and a humanize layer whose curves, timings, and stealth paths match the Python and JavaScript clients exactly.
Table of contents
- Why a decorator layer?
- Feature matrix
- Requirements
- Installation
- Project layout
- Quick start
- Humanized interactions (transparent)
- Launch API reference
- Options reference
HumanConfigreference- How the humanize layer works
- Proxy, GeoIP & WebRTC
- CLI
- Environment variables
- Building & testing
- Test suite map
- Mapping to the Python source
- License
Why a decorator layer?
.NET's Playwright exposes sealed interfaces (IPage, ILocator, IMouse, ...)
that cannot be monkey-patched the way the Python/JS clients replace methods at
runtime. CloakBrowser bridges this gap with a transparent decorator layer:
- You pass
Humanize = trueat launch. NewPageAsync()/NewContextAsync()return a wrapped object.- Every standard Playwright call (
page.ClickAsync,page.FillAsync,page.Mouse.MoveAsync, ...) is automatically humanized - no API changes in your code. - The wrappers are generated at compile time by a Roslyn source generator
(
[GenerateInterfaceDelegation]), so they are fully statically typed with no reflection on the hot path. Non-intercepted members are delegated verbatim.
your code ──> HumanizedPage ──┬─ intercepted member ─> humanize engine ─> raw IPage
(IPage) (generated) └─ everything else ─────────────────────> raw IPage (verbatim)
Feature matrix
| Capability | Status | Source |
|---|---|---|
| Automatic binary download / cache / auto-update (SHA-256 verified) | ✅ | Download.cs, Config.cs |
| Stealth launch args (random fingerprint seed, platform spoofing) | ✅ | CloakLauncher.cs |
| Proxy - HTTP/HTTPS + SOCKS5, inline URL-encoded credentials | ✅ | ProxyResolver.cs |
| GeoIP timezone/locale from proxy exit IP (MaxMind GeoLite2) | ✅ | GeoIp.cs |
WebRTC IP spoofing (--fingerprint-webrtc-ip=auto) |
✅ | CloakLauncher.cs |
| Widevine CDM hint seeding (Linux) | ✅ | Widevine.cs |
| Persistent contexts (profile reuse) | ✅ | CloakLauncher.cs |
| Humanize - Bezier mouse, human typing, scrolling, actionability | ✅ | Human/, Wrappers/ |
| Transparent humanize via source generator | ✅ | CloakBrowser.Generators/ |
CLI (install / info / update / clear-cache) |
✅ | CloakBrowser.Cli/ |
Requirements
| Dependency | Version | Why |
|---|---|---|
| .NET SDK | 8.0 | target framework net8.0 |
Microsoft.Playwright |
1.49.0 | underlying browser automation |
MaxMind.GeoIP2 |
5.2.0 | GeoIP timezone/locale resolution |
Installation
dotnet add package CloakBrowser
The patched Chromium binary downloads automatically on first launch, cached under
~/.cloakbrowser (override with CLOAKBROWSER_CACHE_DIR).
Project layout
dotnet/
├── CloakBrowser.sln
├── src/
│ ├── CloakBrowser/ # the library
│ │ ├── Config.cs # <- cloakbrowser/config.py
│ │ ├── Download.cs # <- cloakbrowser/download.py (+ zip-slip guard)
│ │ ├── GeoIp.cs # <- cloakbrowser/geoip.py
│ │ ├── Widevine.cs # <- cloakbrowser/widevine.py
│ │ ├── CloakLauncher.cs # <- cloakbrowser/browser.py (launch funcs, build_args)
│ │ ├── ProxyResolver.cs # <- cloakbrowser/browser.py (proxy URL helpers)
│ │ ├── ProxySettings.cs # <- ProxySettings TypedDict
│ │ ├── LaunchOptions.cs # launch option records
│ │ ├── Handles.cs # CloakBrowserHandle / CloakContextHandle
│ │ ├── CloakLog.cs # logging facade
│ │ ├── Human/ # <- cloakbrowser/human/*
│ │ │ ├── HumanConfig.cs # <- human/config.py (HumanConfig + merge)
│ │ │ ├── HumanRandom.cs # <- human/config.py (rand/sleep helpers)
│ │ │ ├── HumanMouse.cs # <- human/mouse.py (Bezier engine)
│ │ │ ├── HumanKeyboard.cs # <- human/keyboard.py (typing + CDP stealth)
│ │ │ ├── HumanScroll.cs # <- human/scroll.py
│ │ │ ├── Actionability.cs # <- human/actionability.py
│ │ │ ├── IsolatedWorld.cs # <- _AsyncIsolatedWorld
│ │ │ ├── PlaywrightAdapters.cs # IMouse/IKeyboard/ICDPSession -> raw protocols
│ │ │ └── HumanPage.cs # <- patch_page flows (explicit engine)
│ │ └── Wrappers/ # transparent humanize decorators (Humanize=true)
│ │ ├── Humanize.cs # wrap entry points + helpers (idempotent)
│ │ ├── HumanCursor.cs # shared per-page cursor / CDP stealth state
│ │ ├── LocatorHumanizer.cs # locator-based humanize engine
│ │ ├── HumanizedBrowser.cs # IBrowser decorator
│ │ ├── HumanizedBrowserContext.cs # IBrowserContext decorator
│ │ ├── HumanizedPage.cs # IPage decorator
│ │ ├── HumanizedFrame.cs # IFrame decorator
│ │ ├── HumanizedLocator.cs # ILocator decorator
│ │ ├── HumanizedElementHandle.cs # IElementHandle decorator
│ │ ├── HumanizedMouse.cs # IMouse decorator
│ │ └── HumanizedKeyboard.cs # IKeyboard decorator
│ ├── CloakBrowser.Generators/ # Roslyn source generator
│ │ └── InterfaceDelegationGenerator.cs # emits delegating members
│ └── CloakBrowser.Cli/ # <- cloakbrowser/__main__.py
├── examples/CloakBrowser.Examples/ # runnable examples (see below)
└── tests/CloakBrowser.Tests/ # xUnit tests
Quick start
using CloakBrowser;
await using var browser = await CloakLauncher.LaunchAsync(new LaunchOptions
{
Headless = true,
});
var page = await browser.NewPageAsync();
await page.GotoAsync("https://bot.incolumitas.com/");
Console.WriteLine(await page.TitleAsync());
Humanized interactions (transparent)
Pass Humanize = true and write ordinary Playwright code - mouse moves,
clicks, typing, and scrolling are humanized automatically. Nothing else in your
code changes:
using CloakBrowser;
using CloakBrowser.Human;
await using var browser = await CloakLauncher.LaunchAsync(new LaunchOptions
{
Headless = false,
Humanize = true, // <- the only change
HumanPreset = HumanPreset.Careful, // optional: Default | Careful
HumanConfig = new Dictionary<string, object> { ["typing_delay"] = 90.0 }, // optional overrides
});
var page = await browser.NewPageAsync(); // returns a transparently-wrapped IPage
await page.GotoAsync("https://example.com/login");
await page.FillAsync("#username", "alice"); // per-character human typing
await page.FillAsync("#password", "s3cr3t!"); // (variable delays, thinking pauses, typos+fixes)
await page.ClickAsync("button[type=submit]"); // Bezier mouse curve to a realistic aim point
await page.Mouse.WheelAsync(0, 600); // accelerate -> cruise -> decelerate scroll
The wrapping is complete and transitive - anything you reach through the page is wrapped too:
await page.Mouse.MoveAsync(400, 300); // humanized
await page.Keyboard.TypeAsync("hello"); // humanized
var btn = page.Locator("#submit"); // wrapped ILocator
await btn.ClickAsync(); // humanized
foreach (var frame in page.Frames) { /* wrapped IFrame */ }
What changes vs. plain Playwright
| Member | Humanize = false (default) |
Humanize = true |
|---|---|---|
IPage / ILocator ClickAsync, DblClickAsync, HoverAsync, TapAsync |
direct Playwright dispatch | Bezier-curve mouse move to a randomized in-element aim point, then click |
IPage / ILocator FillAsync, TypeAsync / PressSequentiallyAsync |
instant value set / fast type | per-character typing with variable delays, thinking pauses, occasional typos that self-correct |
IPage / ILocator PressAsync |
direct | human key timing; clicks to focus first only if not already focused |
ILocator CheckAsync, UncheckAsync, SetCheckedAsync, DragToAsync |
direct | humanized click / curved drag |
ILocator SelectOptionAsync (all overloads) |
instant native select | curved hover to the <select> + pause, then native select |
ILocator ClearAsync |
instant value reset | focus (humanized click if needed) + select-all + Backspace |
IMouse MoveAsync, ClickAsync, DblClickAsync, DownAsync, UpAsync |
direct | curved, eased motion with overshoot |
IMouse WheelAsync |
single event | accelerate-cruise-decelerate microsteps |
IKeyboard TypeAsync, PressAsync, InsertTextAsync |
direct | human-timed CDP key events |
page.Mouse, page.Keyboard, page.Locator(...), page.GetBy*, page.Frames, QuerySelectorAsync, IBrowser.NewPageAsync/NewContextAsync, IBrowserContext.NewPageAsync |
raw Playwright objects | the same call returns a wrapped object so humanization stays transitive |
| every other member (navigation, waits, evaluation, screenshots, network, etc.) | - | delegated verbatim to Playwright (identical signatures, return types, exceptions, and CancellationToken support) |
| escape hatch | n/a | ((HumanizedPage)page).Original (also .Inner) returns the raw, un-wrapped object; browser.RawBrowser on the handle |
Per-call overrides are honored - e.g.
await page.ClickAsync("#x", new() { Force = true }) still works and is
humanized. ElementHandle interactions are humanized too, but prefer ILocator
(Playwright's recommendation) for the most reliable aiming.
Calling the original Playwright methods (escape hatch)
When you want a specific call to skip humanization (for raw speed, or to bypass a
behavior you don't want for one action) every wrapper exposes the underlying,
un-humanized Playwright object via Original (with Inner as an alias).
This is the .NET equivalent of the page._original escape hatch in the Python/JS
clients - the object you get back is the genuine Playwright interface, so the
whole API is available on it:
await using var browser = await CloakLauncher.LaunchAsync(new() { Humanize = true });
var page = await browser.NewPageAsync();
// Humanized (default): Bezier mouse curve, realistic aim point, etc.
await page.ClickAsync("#submit");
// Raw Playwright, no humanization - instant, via the escape hatch:
await ((HumanizedPage)page).Original.ClickAsync("#submit");
await ((HumanizedPage)page).Original.FillAsync("#token", value);
// Works on every wrapped object:
await ((HumanizedMouse)page.Mouse).Original.MoveAsync(10, 10);
var rawLocator = ((HumanizedLocator)page.Locator("#row")).Original;
The handle also exposes the un-wrapped browser/context directly:
browser.RawBrowser (and RawContext on a context handle).
Note: when
Humanize = false,NewPageAsync()already returns the raw PlaywrightIPage, so no escape hatch is needed.
Explicit HumanPage engine (advanced)
If you want the explicit, non-transparent engine object instead of the
transparent layer, NewHumanPageAsync returns a HumanPage. Its raw Playwright
page is always available via human.Page.
HumanPage human = await browser.NewHumanPageAsync();
await human.GotoAsync("https://example.com/login");
await human.FillAsync("#username", "alice");
await human.ClickAsync("button[type=submit]");
HumanPage methods (all take an optional HumanActionOptions { Timeout, Force }):
| Method | Description |
|---|---|
GotoAsync(url, options?) |
navigate (delegates to the raw page) |
ClickAsync(selector) |
Bezier move + human click |
DblClickAsync(selector) |
move + double click |
HoverAsync(selector) |
curved move, no click |
TapAsync(selector) |
same motion as click |
TypeAsync(selector, text) |
focus + per-character typing |
FillAsync(selector, value) |
click + select-all + clear + type |
PressAsync(selector, key) |
focus-aware single key press |
PressSequentiallyAsync(selector, text) |
focus + per-character typing |
ClearAsync(selector) |
focus + select-all + Backspace |
CheckAsync / UncheckAsync / SetCheckedAsync(selector, state) |
humanized toggle via click |
SelectOptionAsync(selector, values) |
curved hover + native select |
FocusAsync(selector) |
curved move + programmatic focus (no click side-effects) |
ScrollIntoViewIfNeededAsync(selector) |
humanized accelerate->cruise->decelerate->overshoot scroll |
DragAndDropAsync(src, dst) |
curved press-drag-release |
MouseMoveAsync(x, y) / MouseClickAsync(x, y) |
low-level curved motion |
KeyboardTypeAsync(text) |
low-level human typing at the focused element |
Launch API reference
All entry points live on the static CloakLauncher class and return an
await using-friendly handle.
| Method | Returns | Notes |
|---|---|---|
LaunchAsync(LaunchOptions) |
CloakBrowserHandle |
launches a browser; NewPageAsync, NewContextAsync, NewHumanPageAsync, RawBrowser |
LaunchContextAsync(LaunchContextOptions) |
CloakContextHandle |
browser-owned context with emulation |
LaunchPersistentContextAsync(userDataDir, LaunchContextOptions) |
CloakContextHandle |
reuse a profile directory (cookies/localStorage persist) |
// emulated context
await using var ctx = await CloakLauncher.LaunchContextAsync(new LaunchContextOptions
{
Locale = "en-US",
Timezone = "America/New_York",
Viewport = (1280, 800),
ColorScheme = "dark",
});
var page = await ctx.NewPageAsync();
Locale and timezone are applied via binary flags (
--lang,--fingerprint-locale,--fingerprint-timezone) - not detectable CDP emulation - matching the Python wrapper.
// persistent profile - cookies / localStorage survive across runs, so the
// browser looks like a returning user instead of a fresh incognito session
await using var ctx = await CloakLauncher.LaunchPersistentContextAsync("./profile",
new LaunchContextOptions { Headless = false });
var page = await ctx.NewPageAsync();
Options reference
LaunchOptions / LaunchContextOptions (commonly used fields):
| Option | Type | Default | Purpose |
|---|---|---|---|
Headless |
bool |
true |
run headless (detectors often flag this) |
Humanize |
bool |
false |
enable the transparent humanize layer |
HumanPreset |
HumanPreset |
Default |
Default or Careful (slower, more cautious) |
HumanConfig |
Dictionary<string,object> |
null |
per-field overrides (snake_case or PascalCase keys) |
Proxy |
string / ProxySettings |
null |
HTTP/HTTPS or SOCKS5 proxy |
GeoIp |
bool |
false |
resolve timezone/locale from the proxy exit IP |
Args |
List<string> |
[] |
extra Chromium flags (e.g. --fingerprint-webrtc-ip=auto) |
Locale |
string |
null |
BCP 47 locale -> --lang, --fingerprint-locale |
Timezone |
string |
null |
IANA timezone -> --fingerprint-timezone |
Viewport |
(int,int) |
- | window size |
NoViewport |
bool |
false |
disable viewport emulation (track the real window) |
ColorScheme |
string |
- | light / dark |
LicenseKey |
string |
null |
CloakBrowser Pro key (or CLOAKBROWSER_LICENSE_KEY env / ~/.cloakbrowser/license.key) |
BrowserVersion |
string |
null |
Pin an exact Chromium version (e.g. "148.0.7778.215.2"). Also reads from CLOAKBROWSER_VERSION env var. Works with Free and Pro. |
CloakBrowser Pro
CloakBrowser ships in two tiers:
- Free (v146) — free forever on GitHub Releases. Unlimited sessions. Works today, goes stale as detection evolves.
- Pro (latest, v148) — the newest patches and Chromium upgrades first, so detection stays green as anti-bot systems change. Linux, Windows, and macOS (Apple Silicon + Intel).
Pro plans → cloakbrowser.dev
Activate with a license key — the LicenseKey option, the CLOAKBROWSER_LICENSE_KEY
env var, or ~/.cloakbrowser/license.key:
await using var browser = await CloakLauncher.LaunchAsync(new LaunchOptions
{
LicenseKey = "cb_xxxxxxxx", // or via CLOAKBROWSER_LICENSE_KEY / ~/.cloakbrowser/license.key
});
A valid key downloads the latest Pro binary from cloakbrowser.dev; without one, the
free binary downloads from GitHub Releases. Validation is cached locally for 24h, and
the Pro binary is authenticated with the same pinned Ed25519 signature as the free
binary — a key whose Pro download or signature check fails surfaces a clear error
rather than silently downgrading. Download.BinaryInfo() exposes a Tier field
("pro" / "free"); License.ValidateLicense / License.LicenseInfo mirror the
Python validate_license / LicenseInfo exports.
Rolling back to a previous binary version
Pin an exact Chromium version without downgrading the wrapper — works for Free and Pro:
// Free — pin a public release
await using var browser = await CloakLauncher.LaunchAsync(new LaunchOptions
{
BrowserVersion = "146.0.7680.177.5",
});
// Pro — pin a previous Pro version
await using var browser = await CloakLauncher.LaunchAsync(new LaunchOptions
{
LicenseKey = "cb_xxxxxxxx",
BrowserVersion = "148.0.7778.215.2",
});
The pin is never sticky — unpinned launches always use the latest available version.
You can also set it globally via the CLOAKBROWSER_VERSION env var (see Environment variables).
HumanConfig reference
Every behavior is tunable. Names match the Python HumanConfig dataclass; the
override dictionary accepts both snake_case (Python parity) and PascalCase
keys. Ranges are (min, max) and may be passed as a (double,double) tuple or a
2-element array.
| Group | Field (PascalCase) | Default | Meaning |
|---|---|---|---|
| Keyboard | TypingDelay |
70 |
base inter-key delay (ms) |
TypingDelaySpread |
40 |
± jitter around TypingDelay |
|
TypingPauseChance |
0.1 |
chance of a longer "thinking" pause | |
TypingPauseRange |
(400, 1000) |
thinking-pause duration (ms) | |
ShiftDownDelay / ShiftUpDelay |
(30,70) / (20,50) |
shift key timing | |
KeyHold |
(15, 35) |
key-down hold time (ms) | |
| Mistype | MistypeChance |
0.02 |
chance of a fat-finger typo (ASCII alnum only) |
MistypeDelayNotice |
(100, 300) |
pause before noticing the typo | |
MistypeDelayCorrect |
(50, 150) |
pause after correcting | |
FieldSwitchDelay |
(800, 1500) |
delay when moving between fields | |
| Mouse - move | MouseStepsDivisor |
8 |
distance ÷ divisor = step count |
MouseMinSteps / MouseMaxSteps |
25 / 80 |
clamp on step count | |
MouseWobbleMax |
1.5 |
perpendicular wobble amplitude (px) | |
MouseOvershootChance |
0.15 |
chance of overshoot + correction | |
MouseOvershootPx |
(3, 6) |
overshoot distance | |
MouseBurstSize / MouseBurstPause |
(3,5) / (8,18) |
move-burst grouping + pause | |
| Mouse - click | ClickAimDelayInput / ...Button |
(60,140) / (80,200) |
aim delay before pressing |
ClickHoldInput / ...Button |
(40,100) / (60,150) |
mouse-down hold time | |
ClickInputXRange |
(0.05, 0.30) |
left-biased X target inside inputs | |
| Mouse - idle | IdleDriftPx |
3 |
idle drift amplitude |
IdlePauseRange |
(300, 1000) |
idle pause between drifts | |
| Scroll | ScrollDeltaBase |
(80, 130) |
wheel delta per microstep |
ScrollOvershootChance |
0.1 |
chance to overshoot then settle | |
ScrollSettleDelay |
(300, 600) |
pause after reaching the target | |
ScrollTargetZone |
(0.20, 0.80) |
viewport band the element lands in | |
| Cursor | InitialCursorX / ...Y |
(400,700) / (45,60) |
starting cursor position (address-bar area) |
| Idle between actions | IdleBetweenActions |
false |
opt-in micro-movements between actions (adds latency) |
Resolve / merge helpers:
// preset + overrides
var cfg = HumanConfigFactory.Resolve(
HumanPreset.Careful,
new Dictionary<string, object> { ["typing_delay"] = 120.0, ["key_hold"] = (40.0, 90.0) });
// merge onto an existing config (never mutates the base; returns a new instance)
var faster = cfg.With(new Dictionary<string, object> { ["TypingDelay"] = 30.0 });
With(...) is forgiving: unknown keys are ignored silently, null/empty
overrides return a clone, and non-overridden fields are preserved.
How the humanize layer works
Mouse motion (Bezier)
HumanMouse.HumanMoveAsync traces a cubic Bezier curve between the current
cursor position and a randomized aim point inside the target's bounding box:
- Step count scales with distance (
dist / MouseStepsDivisor, clamped to[MouseMinSteps, MouseMaxSteps]). - Two control points are biased perpendicular to the straight path, so the curve is never a straight line.
- Each step adds sinusoidal wobble (max amplitude
MouseWobbleMax). - Motion is eased (cubic ease-in-out) and grouped into bursts with short
pauses (
MouseBurstSize/MouseBurstPause). - With probability
MouseOvershootChance, the cursor overshoots the target and corrects back - exactly like a real hand.
Click points are computed by HumanMouse.ClickTarget: inputs get a left-biased X
and wide Y band; buttons get a centered cluster.
Human typing & the CDP stealth path
HumanKeyboard.HumanTypeAsync types one character at a time with variable delays
and occasional "thinking" pauses. ASCII alphanumeric characters can trigger a
fat-finger typo (a nearby QWERTY key) that is then noticed and corrected with
Backspace.
Shift symbols (@ # ! $ ...) are the tricky case for stealth. When a CDP
session is available, they are dispatched through
Input.dispatchKeyEvent - producing isTrusted = true events with no
evaluate stack trace for detectors to find. Without CDP, it falls back to a
(detectable) page.evaluate path.
Non-ASCII input
Cyrillic, CJK, emoji and other non-ASCII characters cannot be produced with
physical key codes, so they are inserted via InsertText, one character at a
time, while ASCII characters keep going through Down/Up key presses. For a
mixed string like "Hi Мир":
| Char | Path |
|---|---|
H, i, space |
DownAsync / UpAsync (with Shift for H) |
М, и, р |
InsertTextAsync (per character) |
Scrolling
HumanScroll performs an accelerate -> cruise -> decelerate wheel sequence in
microsteps, with an optional overshoot-and-settle, landing the element in a
natural viewport band (ScrollTargetZone).
Actionability checks
Before interacting, the humanize layer runs Playwright-style actionability
checks (Actionability.cs), with a retry/backoff loop [100, 250, 500, 1000]
ms:
| Check | Meaning | Error on failure |
|---|---|---|
attached |
element exists in the DOM | ElementNotAttachedError |
visible |
element is visible | ElementNotVisibleError |
stable |
bounding box stopped moving (post-scroll) | ElementNotStableError |
enabled |
element is enabled | ElementNotEnabledError |
editable |
element is editable | ElementNotEditableError |
pointer_events |
the click point actually hits the element | ElementNotReceivingEventsError |
Action presets: ChecksClick, ChecksHover, ChecksInput, ChecksFocus,
ChecksCheck.
Fail-open pointer check. The pointer_events probe uses
document.elementFromPoint. If it cannot run (stale handle, execution context
destroyed - EvaluateAsync throws), the check fails open and returns
promptly rather than blocking until timeout - failing closed would wrongly block
legitimate clicks. But an explicit { hit: false } (the element is genuinely
covered) still raises ElementNotReceivingEventsError.
Shared timeout budget (#307)
All sequential steps of one action share a single deadline rather than each
restarting the full timeout. The helper
Actionability.RemainingMs(deadline) returns the milliseconds left (clamped at
zero, never negative). Because every step subtracts from the same budget, the
total wall-clock time can never multiply across steps - the bug fixed in
upstream issue #307.
Focus-aware press / clear
PressAsync and ClearAsync first probe focus with
EvaluateAsync<bool>("el => el === document.activeElement"). If the element is
already focused, they skip the humanized click entirely (the cursor does
not move) and go straight to the keystrokes; otherwise they perform a humanized
focus-click first. This avoids pointless mouse motion on an already-focused
field.
Proxy, GeoIP & WebRTC
await using var browser = await CloakLauncher.LaunchAsync(new LaunchOptions
{
Proxy = "http://user:pass@proxy.example.com:8080", // or a ProxySettings
GeoIp = true, // tz/locale from exit IP
Args = new List<string> { "--fingerprint-webrtc-ip=auto" },
});
- SOCKS5 and credentialed HTTP proxies are routed through Chrome's
--proxy-serverwith inline, URL-encoded credentials (matching the Python logic, including thelinux-x64/windows-x64+ binary-version gate for HTTP inline auth). - GeoIP looks up the proxy exit IP against MaxMind GeoLite2 and applies the resolved timezone/locale via binary flags.
- WebRTC spoofing reuses that exit IP so
RTCPeerConnectioncannot leak the real address.
CLI
Installed via NuGet you don't need a CLI — the binary auto-downloads on first launch
and updates in the background, and Download.BinaryInfo() reports the installed
version, path, and tier (Download.EnsureBinary() pre-fetches it on demand).
When running from a clone of the repo, a small CLI does the same:
dotnet run --project src/CloakBrowser.Cli -- install # download the binary
dotnet run --project src/CloakBrowser.Cli -- info # version / path / platform
dotnet run --project src/CloakBrowser.Cli -- update # check + download newer
dotnet run --project src/CloakBrowser.Cli -- clear-cache # remove cached binaries
Environment variables
Same set as the Python wrapper:
| Variable | Purpose |
|---|---|
CLOAKBROWSER_BINARY_PATH |
Use a local binary, skip download |
CLOAKBROWSER_CACHE_DIR |
Override the cache directory |
CLOAKBROWSER_DOWNLOAD_URL |
Override the download URL |
CLOAKBROWSER_AUTO_UPDATE |
Enable/disable background auto-update |
CLOAKBROWSER_SKIP_CHECKSUM |
Skip SHA-256 verification |
CLOAKBROWSER_VERSION |
Pin to an exact Chromium version for rollback (e.g. 148.0.7778.215.2). Works with Free and Pro binaries |
CLOAKBROWSER_GEOIP_TIMEOUT_SECONDS |
GeoIP HTTP timeout |
CLOAKBROWSER_WIDEVINE_CDM / CLOAKBROWSER_WIDEVINE |
Widevine seeding control |
Building & testing
cd dotnet
dotnet build CloakBrowser.sln # 0 warnings, 0 errors
dotnet test CloakBrowser.sln # all green
The runnable examples accept a scenario name:
dotnet run --project examples/CloakBrowser.Examples -- basic
dotnet run --project examples/CloakBrowser.Examples -- humanize
dotnet run --project examples/CloakBrowser.Examples -- visual # on-screen cursor trail
dotnet run --project examples/CloakBrowser.Examples -- behavioral
dotnet run --project examples/CloakBrowser.Examples -- proxy-geoip
Test suite map
All tests are browser-free: production wrappers are exercised through
DispatchProxy-backed Playwright fakes (Wrappers/FakeProxy.cs,
Fake.Of<T>()), and InternalsVisibleTo lets the tests reach internal types.
A handful of genuinely browser-dependent timing tests are marked
[Fact(Skip = "requires browser")] rather than faked.
| Area | File(s) | What it proves |
|---|---|---|
| Version / download | ConfigTests.cs, DownloadConfigTests.cs |
version compare, archive names, checksum parsing, zip-slip path guard |
| Launch args | BuildArgsTests.cs, MiscTests.cs |
stealth args, build_args dedup |
| Proxy | ProxyResolverTests.cs |
URL resolution / encoding |
| Bezier math | BezierMathTests.cs |
curve produces many points, ends near target, no big jumps, deviates from a straight line |
| Humanize config | HumanConfigTests.cs |
presets, snake/Pascal overrides, range coercion, merge never mutates base, null/empty/unknown-key handling |
| Transparent layer | Wrappers/*.cs |
non-intercepted members delegate verbatim; intercepted members humanize; nested objects come back wrapped; exceptions & CancellationTokens propagate |
| Non-ASCII keyboard | Human/NonAsciiKeyboardTests.cs |
Cyrillic/CJK go via InsertText; ASCII via key presses; mixed strings route per-character |
| Pointer-events fail-open | Human/PointerEventsFailOpenTests.cs |
throwing probe returns fast (< 500ms); explicit "covered" raises ElementNotReceivingEventsError |
| Timeout budget (#307) | Human/TimeoutBudgetTests.cs |
RemainingMs never negative, decreases over time, shared (not multiplied) |
| Focus check | Wrappers/FocusCheckTests.cs |
focused element -> no humanized click (cursor doesn't move); non-focused -> click happens |
Mapping to the Python source
| .NET file | Python source |
|---|---|
Config.cs |
cloakbrowser/config.py |
Download.cs |
cloakbrowser/download.py |
GeoIp.cs |
cloakbrowser/geoip.py |
Widevine.cs |
cloakbrowser/widevine.py |
CloakLauncher.cs / ProxyResolver.cs |
cloakbrowser/browser.py |
Human/HumanConfig.cs |
cloakbrowser/human/config.py |
Human/HumanMouse.cs |
cloakbrowser/human/mouse.py |
Human/HumanKeyboard.cs |
cloakbrowser/human/keyboard.py |
Human/HumanScroll.cs |
cloakbrowser/human/scroll.py |
Human/Actionability.cs |
cloakbrowser/human/actionability.py |
Human/HumanPage.cs |
cloakbrowser/human/__init__.py (patch_page) |
CloakBrowser.Cli/ |
cloakbrowser/__main__.py |
License
MIT - same as the upstream CloakBrowser project.