Compare commits

..
Author SHA1 Message Date
CloakHQ f46f8e9364 release: v0.3.19 — upgrade Linux x64 binary to 145.0.7632.159.8 (42 patches) 2026-03-30 19:17:41 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
592b3d5661 chore(deps): bump docker/build-push-action from 6.19.2 to 7.0.0 (#90)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.2 to 7.0.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/10e90e3645eae34f1e60eeb005ba3a3d33f178e8...d08e5c354a6adb9ed34480a06d141179aa583294)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-29 22:54:19 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a0a8210e35 chore(deps): bump actions/setup-python from 5.6.0 to 6.2.0 (#89)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.6.0 to 6.2.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/a26af69be951a213d495a4c3e4e4022e16d87065...a309ff8b426b58ec0e2a45f0f869d46889d02405)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-29 22:54:17 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
468964ff30 chore(deps): bump actions/setup-node from 4.4.0 to 6.3.0 (#88)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 6.3.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/49933ea5288caeca8642d1e84afbd3f7d6820020...53b83947a5a98c8d113130e565377fae1a50d02f)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-29 22:54:15 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c1b93e634b chore(deps): bump sigstore/cosign-installer from 3.9.1 to 4.1.1 (#87)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.9.1 to 4.1.1.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/398d4b0eeef1380460a10c8013a76f728fb906ac...cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.1.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-29 22:54:12 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5ccb4a32a5 chore(deps): bump actions/attest-build-provenance from 2.4.0 to 4.1.0 (#86)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.4.0 to 4.1.0.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/e8998f949152b193b063cb0ec769d69d929409be...a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-29 22:54:10 +02:00
CloakHQ 49d80d3b57 security: pin GitHub Actions to commit SHAs, add Dependabot
Pin all 22 action references across ci.yml, publish.yml, and
attest-release.yml to immutable commit SHAs. Mutable tags can be
force-pushed by attackers (cf. TeamPCP supply chain campaign).

Add Dependabot for github-actions to get weekly PRs when pinned
actions have new versions.
2026-03-27 21:20:08 +01:00
CloakHQ 2813b3dc4c docs: warn that ElementHandle bypasses humanize patches 2026-03-18 07:51:07 +01:00
dgtlmoonandGitHub 6550f3ad6c fix: ensure pw.stop() runs even if browser.close() raises or is cancelled (#60) 2026-03-15 17:51:03 +01:00
CloakHQ 132cafe13c release: v0.3.18 — fix welcome banner polluting stdout (fixes #59) 2026-03-15 17:19:44 +01:00
CloakHQ 6c94b9e985 feat: add GitHub issue template for bug reports 2026-03-15 07:11:33 +01:00
CloakHQ fdc1ae0484 fix: add --ignore-gpu-blocklist to cloakserve for Docker WebGL support
cloakserve bypasses the wrapper and launches Chrome directly, missing
the GPU blocklist fix from 1380c86. Fixes #58.
2026-03-15 06:02:47 +01:00
CloakHQ 2ded0c1866 docs: add Crawlee integration example 2026-03-15 05:42:23 +01:00
CloakHQ f91700c4a4 release: v0.3.17 — Windows x64 binary upgrade to 145.0.7632.159.7
- Bump wrapper version to 0.3.17 (Python + JS)
- Update PLATFORM_CHROMIUM_VERSIONS: windows-x64 109.2 → 159.7
- Update patch counts in platform tables (Linux 33, Windows 33)
- Add Linux arm64 to JS README platform table
- Update CHANGELOG with all changes since v0.3.16
2026-03-15 02:42:13 +01:00
CloakHQ 1380c86847 fix: auto-inject --ignore-gpu-blocklist for headed mode and Windows
Headed mode (all platforms): Chromium's GPU blocklist disables WebGL on
software GPUs in Docker/VNC/Xvfb. Flag lets SwiftShader serve WebGL.
Harmless on real GPUs. Headless unaffected. Ref #56.

Windows (all modes): GPU blocklist also blocks WebGPU for the Microsoft
Basic Render Driver. Dawn's adapter_blocklist bypass alone isn't enough.
2026-03-15 02:23:48 +01:00
CloakHQ 83e3b30117 feat: add 8 framework integration examples + README integrations section
Add examples/integrations/ with tested examples for browser-use, Crawl4AI,
Scrapling, LangChain, Selenium, undetected-chromedriver, and agent-browser.
Add js/examples/stagehand.ts for Stagehand (TypeScript).

README: new "Framework Integrations" subsection with two integration
patterns (direct binary launch vs CDP connect) and table linking all 8 examples.
2026-03-14 20:56:25 +01:00
CloakHQ 5649620545 release: v0.3.16 — Linux ARM64 binary, multi-arch Docker, donate link
- Add linux-arm64 to PLATFORM_CHROMIUM_VERSIONS (Python + JS)
- Multi-arch Docker build (linux/amd64 + linux/arm64) via QEMU in CI
- Add ko-fi donate link to welcome banner (Python + JS)
- Version bump to 0.3.16
2026-03-14 00:42:06 +01:00
CloakHQ d2a42fc86b release: v0.3.15 — upgrade Linux binary to .159.7, StorageBuckets normalization
Binary:
- Upgrade Linux x64 build to Chromium 145.0.7632.159.7 (33 C++ patches)
- StorageBuckets API quota normalization — closes last storage-based incognito detection vector

Wrapper:
- Fix non-ASCII character support in humanized typing (Cyrillic, CJK, emoji)
- Document storage quota tradeoff for persistent contexts
- Add Ko-fi funding link
2026-03-13 18:59:18 +01:00
lilos 1af25d67bc fix: support non-ASCII characters (Cyrillic, CJK, emoji) in humanized typing 2026-03-13 00:34:01 +01:00
CloakHQ 1bef989404 test: add download fallback tests for primary → GitHub failover
Verify that HTTP errors (429, 503, etc.) from cloakbrowser.dev
correctly trigger GitHub Releases fallback for both binary and
checksum downloads. Also test that custom CLOAKBROWSER_DOWNLOAD_URL
disables fallback, and both-sources-fail returns gracefully.
2026-03-12 19:47:49 +01:00
CloakHQ 0aa4ea56bd docs: add Browser Profile Manager section to README 2026-03-12 01:58:26 +01:00
35 changed files with 1006 additions and 91 deletions
+1
View File
@@ -0,0 +1 @@
ko_fi: cloakhq
+29
View File
@@ -0,0 +1,29 @@
---
name: Bug Report
about: Report a bug or detection issue
labels: bug
---
Description: <!-- What happened? What did you expect? -->
CloakBrowser version: <!-- pip show cloakbrowser / npm list cloakbrowser -->
Wrapper: <!-- Python or JavaScript -->
Environment: <!-- OS, Docker y/n, base image, architecture -->
Launch options:
Tested with a different IP or proxy? <!-- Yes (same result) / Yes (works with different IP) / No -->
Works outside Docker / on host machine? <!-- Yes / No / Not using Docker -->
Steps to reproduce:
Error output / screenshots:
Dockerfile (if applicable):
Additional notes:
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: true
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
- name: Attest build provenance
uses: actions/attest-build-provenance@v2
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: |
cloakbrowser-*.tar.gz
+4 -4
View File
@@ -10,8 +10,8 @@ jobs:
python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Install dependencies
@@ -22,8 +22,8 @@ jobs:
javascript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 20
- name: Install and build
+18 -16
View File
@@ -24,15 +24,15 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Python tests
run: |
pip install -e ".[dev]" pytest pytest-asyncio
pytest tests/ -v -m "not slow"
- uses: actions/setup-node@v4
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
- name: JavaScript tests
@@ -42,8 +42,8 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Check tag matches package versions
@@ -62,8 +62,8 @@ jobs:
permissions:
id-token: write # OIDC trusted publishing — no PYPI_TOKEN needed
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Build
@@ -71,7 +71,7 @@ jobs:
pip install build
python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1
publish-npm:
needs: [test, validate-version]
@@ -80,8 +80,8 @@ jobs:
permissions:
id-token: write # OIDC trusted publishing + provenance — no NPM_TOKEN needed
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
@@ -102,32 +102,34 @@ jobs:
attestations: write
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Extract version
run: |
VERSION=$(python -c 'import re; print(re.search(r"__version__\s*=\s*[\"'\'']([^\"'\'']+)", open("cloakbrowser/_version.py").read()).group(1))')
echo "VERSION=$VERSION" >> $GITHUB_ENV
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PAT }}
- name: Build and push
id: build
uses: docker/build-push-action@v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
cloakhq/cloakbrowser:${{ env.VERSION }}
cloakhq/cloakbrowser:latest
provenance: true
sbom: true
- uses: sigstore/cosign-installer@v3
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
- name: Sign image
run: cosign sign --yes cloakhq/cloakbrowser@${{ steps.build.outputs.digest }}
- name: Attest build provenance
uses: actions/attest-build-provenance@v2
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-name: index.docker.io/cloakhq/cloakbrowser
subject-digest: ${{ steps.build.outputs.digest }}
+37
View File
@@ -6,6 +6,43 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi
---
## [0.3.19] — 2026-03-30
- **[binary]** Upgrade Linux x64 build to 145.0.7632.159.8 — 42 source-level C++ patches (up from 33)
- **[binary]** 9 new fingerprint patches covering additional browser APIs and cross-platform consistency
- **[binary]** New `--fingerprint-noise` flag — disable noise injection while keeping deterministic fingerprint seed active
- **[binary]** Improved fingerprint noise reliability and determinism across all patched APIs
- **[binary]** Expanded platform-aware fingerprint spoofing for more realistic cross-platform profiles
- **[binary]** Font rendering and detection accuracy improvements for Windows profiles
- **[binary]** Removed experimental patches that caused compatibility issues with certain anti-bot systems
- **[binary]** Docker/VNC environment compatibility improvements
- **[wrapper]** Fix Playwright cleanup — `pw.stop()` now runs even if `browser.close()` raises or is cancelled (fixes #60, thanks [@dgtlmoon](https://github.com/dgtlmoon))
- **[meta]** Pin GitHub Actions to commit SHAs, add Dependabot for automated dependency updates
## [0.3.18] — 2026-03-15
- **[wrapper]** Fix welcome banner printing to stdout — now writes to stderr so it won't corrupt JSON output in programmatic usage (fixes #59)
- **[wrapper]** Fix `cloakserve` Docker WebGL by adding `--ignore-gpu-blocklist` flag
- **[docs]** Add Crawlee integration example
- **[meta]** Add GitHub issue template for bug reports
## [0.3.17] — 2026-03-15
- **[binary]** Windows x64 build upgraded to 145.0.7632.159.7 — 33 source-level C++ patches, matching Linux
- **[wrapper]** Auto-inject GPU blocklist bypass for headed mode and Windows — fixes WebGL/WebGPU on software GPUs in Docker/VNC (fixes #56)
- **[wrapper]** Add 8 framework integration examples (Scrapy, Crawlee, BrowserBase, etc.) and README integrations section
## [0.3.16] — 2026-03-14
- **[binary]** Linux arm64 build available — Raspberry Pi, AWS Graviton, Oracle Ampere now supported
- **[wrapper]** Add donate link to first-launch welcome banner
## [0.3.15] — 2026-03-13
- **[binary]** Upgrade Linux build to 145.0.7632.159.7 — 33 source-level C++ patches
- **[binary]** StorageBuckets API quota normalization — closes the last storage-based incognito detection vector
- **[wrapper]** Fix non-ASCII character support in humanized typing — Cyrillic, CJK, and emoji now type correctly (thanks [@evelaa123](https://github.com/evelaa123))
## [0.3.14] — 2026-03-12
- **[binary]** Upgrade Linux build to 145.0.7632.159.6 — fix persistent context detection by FingerprintJS
+74 -21
View File
@@ -14,6 +14,10 @@
<a href="https://hub.docker.com/r/cloakhq/cloakbrowser"><img src="https://img.shields.io/docker/pulls/cloakhq/cloakbrowser?label=docker&logo=docker&logoColor=white" alt="Docker Pulls"></a>
</p>
<p align="center">
<a href="https://ko-fi.com/cloakhq"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support on Ko-fi"></a>
</p>
<br>
<h3 align="center">Stealth Chromium that passes every bot detection test.</h3>
@@ -36,7 +40,7 @@ Drop-in Playwright/Puppeteer replacement for Python and JavaScript.<br>
Same API, same code — just swap the import. <strong>3 lines of code, 30 seconds to unblock.</strong>
</p>
- **32 source-level C++ patches** — canvas, WebGL, audio, fonts, GPU, screen, automation signals, CDP input behavior
- **33 source-level C++ patches** — canvas, WebGL, audio, fonts, GPU, screen, automation signals, CDP input behavior
- **`humanize=True`** — human-like mouse curves, keyboard timing, and scroll patterns. One flag, behavioral detection passes
- **0.9 reCAPTCHA v3 score** — human-level, server-verified
- **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — tested against 30+ detection sites
@@ -110,13 +114,27 @@ page.goto("https://example.com")
> ⭐ **Star** to show support — **[Watch releases](https://github.com/CloakHQ/CloakBrowser/subscription)** to get notified when new builds drop.
## Latest: v0.3.14 (Chromium 145.0.7632.159.6)
## Browser Profile Manager
Self-hosted alternative to Multilogin, GoLogin, and AdsPower. Create browser profiles with unique fingerprints, proxies, and persistent sessions. Launch and interact with them in your browser via noVNC.
```bash
docker run -p 8080:8080 -v cloakprofiles:/data cloakhq/cloakbrowser-manager
```
Open [http://localhost:8080](http://localhost:8080). Create a profile. Click **Launch**. Done.
**[CloakBrowser Manager](https://github.com/CloakHQ/CloakBrowser-Manager)** — free, open source (MIT)
---
## Latest: v0.3.17 (Chromium 145.0.7632.159.7)
- **`humanize=True`** — one flag makes all mouse, keyboard, and scroll interactions behave like a real user. Bézier curves, per-character typing, realistic scroll patterns. Two presets: `default` and `careful`
- **CDP input behavior mimicking** — input events sent via CDP now produce the same signals as real user interactions. 5 new source-level patches covering pointer, keyboard, and mouse behavior
- **Native locale spoofing** — new C++ patch replaces detectable CDP-level locale emulation
- **WebGPU fingerprint hardening** — adapter features, limits, and device ID spoofed for cross-API consistency
- **32 fingerprint patches** (Linux x64) — all 4 platforms on Chromium 145
- **33 fingerprint patches** (Linux x64) — all 4 platforms on Chromium 145
- **Stealthy with zero flags** — binary auto-generates a random fingerprint seed at startup. No configuration required
- **Timezone & locale from proxy IP** — `launch(proxy="...", geoip=True)` auto-detects timezone and locale
- **Persistent profiles** — `launch_persistent_context()` keeps cookies and localStorage across sessions, bypasses incognito detection
@@ -129,7 +147,7 @@ See the full [CHANGELOG.md](CHANGELOG.md) for details.
- **CloakBrowser patches Chromium source code** — fingerprints are modified at the C++ level, compiled into the binary. Detection sites see a real browser because it *is* a real browser.
- **Source-level stealth** — C++ patches handle fingerprints (GPU, screen, UA, hardware reporting) at the binary level. No JavaScript injection, no config-level hacks. Most stealth tools only patch at the surface.
- **Same behavior everywhere** — works identically local, in Docker, and on VPS. No environment-specific patches or config needed.
- **Works with AI agents and automation frameworks** — drop-in stealth for browser-use, Crawl4AI, agent-browser, Claude computer use, and OpenAI Operator. Also tested with Playwright, Puppeteer, and Selenium — point any Chromium-based framework at the binary path.
- **Works with AI agents and automation frameworks** — drop-in stealth for browser-use, Crawl4AI, Scrapling, Stagehand, LangChain, Selenium, and more. See [integrations](#framework-integrations).
CloakBrowser doesn't solve CAPTCHAs — it prevents them from appearing. No CAPTCHA-solving services, no proxy rotation built in — bring your own proxies, use the Playwright API you already know.
@@ -203,7 +221,7 @@ CloakBrowser is a thin wrapper (Python + JavaScript) around a custom-built Chrom
3. **Every launch** → Playwright or Puppeteer starts with our binary + stealth args
4. **You write code** → standard Playwright/Puppeteer API, nothing new to learn
The binary includes 32 source-level patches covering canvas, WebGL, audio, fonts, GPU, screen properties, hardware reporting, automation signal removal, and CDP input behavior mimicking.
The binary includes 33 source-level patches covering canvas, WebGL, audio, fonts, GPU, screen properties, hardware reporting, automation signal removal, and CDP input behavior mimicking.
These are compiled into the Chromium binary — not injected via JavaScript, not set via flags.
@@ -288,7 +306,7 @@ context.close()
### `launch_persistent_context()`
Same as `launch_context()`, but with a persistent user profile. Cookies, localStorage, and cache persist across sessions. Also avoids incognito detection by services like BrowserScan.
Same as `launch_context()`, but with a persistent user profile. Cookies, localStorage, and cache persist across sessions.
Use this when you need to:
- **Stay logged in** across runs (cookies/sessions survive restarts)
@@ -313,6 +331,17 @@ Supports all the same options as `launch_context()`: `proxy`, `user_agent`, `vie
Async version: `launch_persistent_context_async()`.
**Storage quota and detection tradeoff:** By default, the binary normalizes storage quota to pass FingerprintJS, which blocks persistent contexts that report non-incognito quota values. This means detection services that penalize incognito mode (like BrowserScan's `notPrivate` check, -10 points) will still flag it. If your target site penalizes incognito but doesn't use FingerprintJS, set a higher quota to appear as a regular profile:
```python
ctx = launch_persistent_context("./my-profile", args=["--fingerprint-storage-quota=5000"])
```
| Quota setting | FingerprintJS | BrowserScan `notPrivate` |
|---|---|---|
| Default (auto, ~500MB) | PASS | -10 (flagged as incognito) |
| `--fingerprint-storage-quota=5000` | May trigger detection | PASS (appears non-incognito) |
### CLI
Pre-download the binary or check installation status from the command line:
@@ -472,6 +501,8 @@ const browser = await launch({
Access the original un-patched Playwright page at `page._original` if you need raw speed for a specific call.
> **Note:** Always use `page.click(selector)`, `page.type(selector, text)`, `page.hover(selector)`, or `page.locator(selector).*` — these go through the full humanize pipeline. Avoid `page.query_selector()` — `ElementHandle` objects bypass all patches, so mouse movement teleports, keyboard events fire without timing, and scroll has no human curve.
> Contributed by [@evelaa123](https://github.com/evelaa123) — full Playwright API coverage.
## Configuration
@@ -539,6 +570,7 @@ Supported by the binary but **not set by default** — pass via `args` to custom
| `--fingerprint-location` | Geolocation coordinates |
| `--fingerprint-timezone` | Timezone (e.g. `America/New_York`) |
| `--fingerprint-locale` | Locale (e.g. `en-US`) |
| `--fingerprint-storage-quota` | Override storage quota in MB — affects `storage.estimate()`, `storageBuckets`, and legacy webkit APIs. Auto-normalized when `--fingerprint` is set |
| `--fingerprint-taskbar-height` | Override taskbar height (binary defaults: Win=48, Mac=95, Linux=0) |
| `--fingerprint-fonts-dir` | Path to cross-platform font directory |
| `--enable-blink-features=FakeShadowRoot` | Access closed shadow DOM elements |
@@ -580,14 +612,44 @@ browser = launch(args=[
- [`basic-puppeteer.ts`](js/examples/basic-puppeteer.ts) — Puppeteer launch and load
- [`stealth-test.ts`](js/examples/stealth-test.ts) — Run against 6 detection sites
### Framework Integrations
CloakBrowser works with any framework that uses Playwright or Chromium:
```python
# Option 1: Framework launches our binary directly (Selenium, Stagehand, UC)
from cloakbrowser.download import ensure_binary
from cloakbrowser.config import get_default_stealth_args
binary_path = ensure_binary() # auto-downloads if needed
stealth_args = get_default_stealth_args() # all fingerprint flags
# Option 2: CloakBrowser launches first, framework connects via CDP (browser-use, Crawl4AI, Scrapling)
from cloakbrowser import launch_async
browser = await launch_async(args=["--remote-debugging-port=9242"])
# Connect your framework to http://127.0.0.1:9242 — all stealth flags are set
```
| Framework | Stars | Language | Example |
|-----------|-------|----------|---------|
| [browser-use](https://github.com/browser-use/browser-use) | 70K | Python | [`browser_use_example.py`](examples/integrations/browser_use_example.py) |
| [Crawl4AI](https://github.com/unclecode/crawl4ai) | 58K | Python | [`crawl4ai_example.py`](examples/integrations/crawl4ai_example.py) |
| [Crawlee](https://github.com/apify/crawlee-python) | 8.6K | Python | [`crawlee_example.py`](examples/integrations/crawlee_example.py) |
| [Scrapling](https://github.com/D4Vinci/Scrapling) | 21K | Python | [`scrapling_example.py`](examples/integrations/scrapling_example.py) |
| [Stagehand](https://github.com/browserbase/stagehand) | 21K | TypeScript | [`stagehand.ts`](js/examples/stagehand.ts) |
| [LangChain](https://github.com/langchain-ai/langchain) | 100K+ | Python | [`langchain_loader.py`](examples/integrations/langchain_loader.py) |
| [Selenium](https://github.com/SeleniumHQ/selenium) | — | Python | [`selenium_example.py`](examples/integrations/selenium_example.py) |
| [undetected-chromedriver](https://github.com/ultrafunkamsterdam/undetected-chromedriver) | 12K | Python | [`undetected_chromedriver.py`](examples/integrations/undetected_chromedriver.py) |
| [agent-browser](https://github.com/nichochar/agent-browser) | — | Shell | [`agent_browser.sh`](examples/integrations/agent_browser.sh) |
## Platforms
| Platform | Chromium | Patches | Status |
|---|---|---|---|
| Linux x86_64 | 145 | 31 | ✅ Latest |
| Linux x86_64 | 145 | 33 | ✅ Latest |
| Linux arm64 (RPi, Graviton) | 145 | 33 | ✅ Latest |
| macOS arm64 (Apple Silicon) | 145 | 26 | ✅ Latest |
| macOS x86_64 (Intel) | 145 | 26 | ✅ Latest |
| Windows x86_64 | 145 | 26 | ✅ Latest |
| Windows x86_64 | 145 | 33 | ✅ Latest |
The wrapper auto-downloads the correct binary for your platform.
@@ -861,25 +923,15 @@ The macOS fingerprint profile has known inconsistencies that aggressive bot dete
### Site detects incognito / private browsing mode
By default, `launch()` opens an incognito context. Some sites (like BrowserScan) detect this. Use `launch_persistent_context()` instead — it runs with a real user profile, so incognito detection passes:
By default, `launch()` opens an incognito context. Some sites penalize this. Use `launch_persistent_context()` to get a real profile with cookie persistence:
```python
from cloakbrowser import launch_persistent_context
ctx = launch_persistent_context("./my-profile", headless=False)
page = ctx.new_page()
```
```javascript
import { launchPersistentContext } from 'cloakbrowser';
const ctx = await launchPersistentContext({
userDataDir: './my-profile',
headless: false,
});
```
This also gives you cookie and localStorage persistence across sessions.
If the site still flags incognito, raise the storage quota to appear as a regular browsing session. See the [storage quota tradeoff](#launch_persistent_context) for details on how this affects different detection services.
---
@@ -949,6 +1001,7 @@ A: Yes. Pass `proxy="http://user:pass@host:port"` to `launch()`.
- 🐛 **Bug reports & feature requests** — [GitHub Issues](https://github.com/CloakHQ/CloakBrowser/issues)
- 📦 **PyPI** — [pypi.org/project/cloakbrowser](https://pypi.org/project/cloakbrowser/)
- 📦 **npm** — [npmjs.com/package/cloakbrowser](https://www.npmjs.com/package/cloakbrowser)
- ☕ **Support** — [ko-fi.com/cloakhq](https://ko-fi.com/cloakhq)
- 📧 **Contact** — cloakhq@pm.me
## Security
@@ -958,7 +1011,7 @@ All releases are signed for supply chain verification.
```bash
# Verify GPG signature (binary release tag)
gpg --keyserver keyserver.ubuntu.com --recv-keys C60C0DDC9D0DE2DD
git verify-tag chromium-v145.0.7632.159.6
git verify-tag chromium-v145.0.7632.159.7
# Verify GitHub binary attestation (Sigstore)
gh attestation verify cloakbrowser-linux-x64.tar.gz --repo CloakHQ/cloakbrowser
+3
View File
@@ -33,6 +33,9 @@ chrome_args = [
"--disable-popup-blocking",
"--disable-background-networking",
"--metrics-recording-only",
# GPU blocklist bypass: Chromium blocks WebGL on software GPUs in
# Docker/Xvfb. Without this, WebGL vendor/renderer spoofing fails. #58
"--ignore-gpu-blocklist",
] + get_default_stealth_args() + sys.argv[1:]
chrome = subprocess.Popen(chrome_args)
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.3.14"
__version__ = "0.3.19"
+35 -14
View File
@@ -102,7 +102,7 @@ def launch(
binary_path = ensure_binary()
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
logger.debug("Launching stealth Chromium (headless=%s, args=%d)", headless, len(chrome_args))
@@ -120,8 +120,10 @@ def launch(
_original_close = browser.close
def _close_with_cleanup() -> None:
_original_close()
pw.stop()
try:
_original_close()
finally:
pw.stop()
browser.close = _close_with_cleanup
@@ -185,7 +187,7 @@ async def launch_async( # noqa: C901
binary_path = ensure_binary()
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
logger.debug("Launching stealth Chromium async (headless=%s, args=%d)", headless, len(chrome_args))
@@ -203,8 +205,10 @@ async def launch_async( # noqa: C901
_original_close = browser.close
async def _close_with_cleanup() -> None:
await _original_close()
await pw.stop()
try:
await _original_close()
finally:
await pw.stop()
browser.close = _close_with_cleanup
@@ -281,7 +285,7 @@ def launch_persistent_context(
binary_path = ensure_binary()
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
logger.debug(
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
@@ -314,8 +318,10 @@ def launch_persistent_context(
_original_close = context.close
def _close_with_cleanup() -> None:
_original_close()
pw.stop()
try:
_original_close()
finally:
pw.stop()
context.close = _close_with_cleanup
@@ -394,7 +400,7 @@ async def launch_persistent_context_async(
binary_path = ensure_binary()
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
logger.debug(
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
@@ -427,8 +433,10 @@ async def launch_persistent_context_async(
_original_close = context.close
async def _close_with_cleanup() -> None:
await _original_close()
await pw.stop()
try:
await _original_close()
finally:
await pw.stop()
context.close = _close_with_cleanup
@@ -514,8 +522,10 @@ def launch_context(
_original_ctx_close = context.close
def _close_context_with_cleanup() -> None:
_original_ctx_close()
browser.close()
try:
_original_ctx_close()
finally:
browser.close()
context.close = _close_context_with_cleanup
@@ -609,6 +619,7 @@ def _build_args(
extra_args: list[str] | None,
timezone: str | None = None,
locale: str | None = None,
headless: bool = True,
) -> list[str]:
"""Combine stealth args with user-provided args and locale flags.
@@ -621,6 +632,16 @@ def _build_args(
for arg in get_default_stealth_args():
seen[arg.split("=", 1)[0]] = arg
# GPU blocklist bypass:
# - Headed mode (all platforms): Chromium blocks WebGL on software GPUs
# in Docker/Xvfb. Flag lets SwiftShader serve WebGL. See issue #56.
# - Windows (all modes): Chromium's GPU blocklist blocks WebGPU for the
# Microsoft Basic Render Driver. Dawn's adapter_blocklist bypass alone
# isn't enough — need this flag too. Linux doesn't need it.
import platform as _platform
if not headless or _platform.system() == "Windows":
seen["--ignore-gpu-blocklist"] = "--ignore-gpu-blocklist"
if extra_args:
for arg in extra_args:
key = arg.split("=", 1)[0]
+4 -3
View File
@@ -15,13 +15,14 @@ from ._version import __version__
# CHROMIUM_VERSION is the latest across all platforms (for display/reference).
# Use get_chromium_version() for the current platform's actual version.
# ---------------------------------------------------------------------------
CHROMIUM_VERSION = "145.0.7632.159.6"
CHROMIUM_VERSION = "145.0.7632.159.8"
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
"linux-x64": "145.0.7632.159.6",
"linux-x64": "145.0.7632.159.8",
"linux-arm64": "145.0.7632.159.7",
"darwin-arm64": "145.0.7632.109.2",
"darwin-x64": "145.0.7632.109.2",
"windows-x64": "145.0.7632.109.2",
"windows-x64": "145.0.7632.159.7",
}
# ---------------------------------------------------------------------------
+9 -7
View File
@@ -12,6 +12,7 @@ import os
import platform
import stat
import subprocess
import sys
import tarfile
import tempfile
import threading
@@ -55,13 +56,14 @@ def _show_welcome() -> None:
marker = get_cache_dir() / ".welcome_shown"
if marker.exists():
return
print()
print(" CloakBrowser — stealth Chromium for automation")
print(" https://github.com/CloakHQ/CloakBrowser")
print()
print(" Issues? https://github.com/CloakHQ/CloakBrowser/issues")
print(" Star us if CloakBrowser helps your project!")
print()
sys.stderr.write("\n")
sys.stderr.write(" CloakBrowser — stealth Chromium for automation\n")
sys.stderr.write(" https://github.com/CloakHQ/CloakBrowser\n")
sys.stderr.write("\n")
sys.stderr.write(" Issues? https://github.com/CloakHQ/CloakBrowser/issues\n")
sys.stderr.write(" Donate? https://ko-fi.com/cloakhq\n")
sys.stderr.write(" Star us if CloakBrowser helps your project!\n")
sys.stderr.write("\n")
try:
marker.parent.mkdir(parents=True, exist_ok=True)
marker.write_text("")
+9 -1
View File
@@ -41,7 +41,15 @@ def _get_nearby_key(ch: str) -> str:
def human_type(page: Any, raw: RawKeyboard, text: str, cfg: HumanConfig) -> None:
for i, ch in enumerate(text):
# Mistype chance — press wrong key, notice, backspace, then correct
# Non-ASCII characters (Cyrillic, CJK, emoji) — use insertText
if not ch.isascii():
sleep_ms(rand_range(cfg.key_hold))
raw.insert_text(ch)
if i < len(text) - 1:
_inter_char_delay(cfg)
continue
# Mistype chance — only for ASCII alphanumeric
if random.random() < cfg.mistype_chance and ch.isalnum():
wrong = _get_nearby_key(ch)
_type_normal_char(raw, wrong, cfg)
+9 -1
View File
@@ -22,7 +22,15 @@ class AsyncRawKeyboard(Protocol):
async def async_human_type(page: Any, raw: AsyncRawKeyboard, text: str, cfg: HumanConfig) -> None:
for i, ch in enumerate(text):
# Mistype chance — press wrong key, notice, backspace, then correct
# Non-ASCII characters (Cyrillic, CJK, emoji) — use insertText
if not ch.isascii():
await async_sleep_ms(rand_range(cfg.key_hold))
await raw.insert_text(ch)
if i < len(text) - 1:
await _inter_char_delay(cfg)
continue
# Mistype chance — only for ASCII alphanumeric
if random.random() < cfg.mistype_chance and ch.isalnum():
wrong = _get_nearby_key(ch)
await _type_normal_char(raw, wrong, cfg)
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# agent-browser + CloakBrowser: AI browser agent with stealth fingerprints.
#
# agent-browser is a Node.js CLI for browser automation with session management.
# CloakBrowser provides the stealth Chromium binary.
#
# Requires: npm install -g agent-browser
# pip install cloakbrowser (to auto-download the binary)
#
# Note: agent-browser launches Chrome itself via env vars — it can't connect
# to an existing browser via CDP. So we pass the binary path and stealth args directly.
# Get CloakBrowser binary path (auto-downloads if needed)
BINARY_PATH=$(python3 -c "from cloakbrowser.download import ensure_binary; print(ensure_binary())")
# Get stealth args from our wrapper (comma-separated for agent-browser)
STEALTH_ARGS=$(python3 -c "from cloakbrowser.config import get_default_stealth_args; print(','.join(get_default_stealth_args()))")
# Point agent-browser at CloakBrowser
export AGENT_BROWSER_EXECUTABLE_PATH="$BINARY_PATH"
export AGENT_BROWSER_ARGS="$STEALTH_ARGS"
# Open a page
agent-browser --session stealth-test open "https://example.com"
# Get page title
agent-browser --session stealth-test eval "document.title"
# Check stealth
agent-browser --session stealth-test eval "JSON.stringify({webdriver: navigator.webdriver, plugins: navigator.plugins.length, platform: navigator.platform})"
@@ -0,0 +1,43 @@
"""browser-use + CloakBrowser: AI agent with stealth fingerprints.
browser-use handles AI agent logic, CloakBrowser handles bot detection.
Your agent can now browse sites behind Cloudflare, reCAPTCHA, DataDome.
Requires: pip install browser-use cloakbrowser langchain-openai
Set OPENAI_API_KEY (or swap for another LLM provider).
"""
import asyncio
from browser_use import Agent, Browser, BrowserConfig
from langchain_openai import ChatOpenAI
from cloakbrowser import launch_async
async def main():
# Step 1: Launch CloakBrowser (handles binary, stealth args, fingerprints)
cb_browser = await launch_async(
headless=True,
args=["--remote-debugging-port=9242", "--remote-debugging-address=127.0.0.1"],
)
# Step 2: Connect browser-use to the stealth browser via CDP
config = BrowserConfig(cdp_url="http://127.0.0.1:9242")
browser = Browser(config=config)
# Step 3: Run your AI agent — it browses through CloakBrowser
agent = Agent(
task="Go to https://www.google.com and search for 'browser automation'",
llm=ChatOpenAI(model="gpt-4o-mini"),
browser=browser,
)
result = await agent.run()
print(result)
await cb_browser.close()
if __name__ == "__main__":
asyncio.run(main())
+39
View File
@@ -0,0 +1,39 @@
"""Crawl4AI + CloakBrowser: LLM-ready web crawling with stealth fingerprints.
Crawl4AI handles extraction and markdown conversion,
CloakBrowser handles bot detection.
Requires: pip install crawl4ai cloakbrowser
"""
import asyncio
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
from cloakbrowser import launch_async
async def main():
# Step 1: Launch CloakBrowser with remote debugging
cb_browser = await launch_async(
headless=True,
args=["--remote-debugging-port=9243", "--remote-debugging-address=127.0.0.1"],
)
# Step 2: Connect Crawl4AI to the stealth browser via CDP
browser_config = BrowserConfig(cdp_url="http://127.0.0.1:9243")
run_config = CrawlerRunConfig()
async with AsyncWebCrawler(config=browser_config) as crawler:
result = await crawler.arun(
"https://example.com",
config=run_config,
)
print(f"Extracted {len(result.markdown)} chars of markdown")
print(result.markdown[:500])
await cb_browser.close()
if __name__ == "__main__":
asyncio.run(main())
+72
View File
@@ -0,0 +1,72 @@
"""Crawlee + CloakBrowser: stealth web crawling with PlaywrightCrawler.
Uses a custom BrowserPlugin to swap Crawlee's default Chromium
for CloakBrowser's patched binary with source-level fingerprint patches.
Requires: pip install cloakbrowser "crawlee[playwright]"
"""
import asyncio
from cloakbrowser.config import IGNORE_DEFAULT_ARGS, get_default_stealth_args
from cloakbrowser.download import ensure_binary
from typing_extensions import override
from crawlee.browsers import (
BrowserPool,
PlaywrightBrowserController,
PlaywrightBrowserPlugin,
)
from crawlee.crawlers import PlaywrightCrawler, PlaywrightCrawlingContext
class CloakBrowserPlugin(PlaywrightBrowserPlugin):
"""Browser plugin that uses CloakBrowser's patched Chromium,
but otherwise keeps the functionality of PlaywrightBrowserPlugin.
"""
@override
async def new_browser(self) -> PlaywrightBrowserController:
if not self._playwright:
raise RuntimeError('Playwright browser plugin is not initialized.')
binary_path = ensure_binary()
stealth_args = get_default_stealth_args()
# Merge CloakBrowser stealth args with any user-provided launch options.
launch_options = dict(self._browser_launch_options)
launch_options.pop('executable_path', None)
launch_options.pop('chromium_sandbox', None)
existing_args = list(launch_options.pop('args', []))
launch_options['args'] = [*existing_args, *stealth_args]
return PlaywrightBrowserController(
browser=await self._playwright.chromium.launch(
executable_path=binary_path,
ignore_default_args=IGNORE_DEFAULT_ARGS,
**launch_options,
),
max_open_pages_per_browser=1,
# CloakBrowser handles fingerprints at the binary level.
header_generator=None,
)
async def main() -> None:
crawler = PlaywrightCrawler(
max_requests_per_crawl=10,
browser_pool=BrowserPool(plugins=[CloakBrowserPlugin()]),
)
@crawler.router.default_handler
async def request_handler(context: PlaywrightCrawlingContext) -> None:
context.log.info(f'Processing {context.request.url} ...')
title = await context.page.title()
await context.push_data({'url': context.request.url, 'title': title})
await context.enqueue_links()
await crawler.run(['https://example.com'])
if __name__ == '__main__':
asyncio.run(main())
+51
View File
@@ -0,0 +1,51 @@
"""LangChain + CloakBrowser: load web pages behind bot detection into LangChain Documents.
LangChain's PlaywrightURLLoader hardcodes chromium.launch() with no way to pass
a custom binary. This example uses CloakBrowser directly as a stealth document loader
that produces LangChain Document objects.
Requires: pip install langchain-core cloakbrowser
"""
import asyncio
from langchain_core.documents import Document
from cloakbrowser import launch_async
async def load_urls_stealth(urls: list[str], **launch_kwargs) -> list[Document]:
"""Load URLs using CloakBrowser stealth browser, return LangChain Documents."""
browser = await launch_async(headless=True, **launch_kwargs)
page = await browser.new_page()
docs = []
for url in urls:
await page.goto(url, wait_until="domcontentloaded")
text = await page.evaluate("document.body.innerText")
title = await page.title()
docs.append(Document(
page_content=text,
metadata={"source": url, "title": title},
))
await browser.close()
return docs
async def main():
urls = [
"https://example.com",
"https://httpbin.org/html",
]
docs = await load_urls_stealth(urls)
for doc in docs:
print(f"--- {doc.metadata['title']} ({doc.metadata['source']}) ---")
print(doc.page_content[:300])
print()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,42 @@
"""Scrapling + CloakBrowser: adaptive web scraping with stealth fingerprints.
Scrapling handles parsing and element tracking,
CloakBrowser handles bot detection.
Requires: pip install scrapling[all] cloakbrowser
"""
import asyncio
import json
from urllib.request import urlopen
from scrapling.fetchers import StealthyFetcher
from cloakbrowser import launch_async
async def main():
# Launch CloakBrowser with remote debugging
cb_browser = await launch_async(
headless=True,
args=["--remote-debugging-port=9245", "--remote-debugging-address=127.0.0.1"],
)
# Get the WebSocket URL from Chrome (Scrapling requires ws:// scheme)
info = json.loads(urlopen("http://127.0.0.1:9245/json/version").read())
ws_url = info["webSocketDebuggerUrl"]
# Connect Scrapling to the stealth browser via CDP
page = await StealthyFetcher.async_fetch(
"https://example.com",
cdp_url=ws_url,
)
print(f"Title: {page.css('title::text').get()}")
print(f"Text: {page.css('p::text').getall()}")
await cb_browser.close()
if __name__ == "__main__":
asyncio.run(main())
+41
View File
@@ -0,0 +1,41 @@
"""Selenium + CloakBrowser: use stealth Chromium with Selenium WebDriver.
CloakBrowser provides the binary and stealth args.
Selenium drives it via ChromeDriver.
Requires: pip install selenium cloakbrowser
Note: ChromeDriver version must match Chromium 145.
pip install chromedriver-autoinstaller or download manually.
"""
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from cloakbrowser.config import get_default_stealth_args
from cloakbrowser.download import ensure_binary
binary_path = ensure_binary()
stealth_args = get_default_stealth_args()
options = Options()
options.binary_location = binary_path
options.add_argument("--headless")
for arg in stealth_args:
options.add_argument(arg)
driver = webdriver.Chrome(options=options)
driver.get("https://example.com")
print(f"Selenium + CloakBrowser: {driver.title}")
# Verify stealth
result = driver.execute_script("""
return {
webdriver: navigator.webdriver,
plugins: navigator.plugins.length,
platform: navigator.platform,
}
""")
print(f"Stealth checks: {result}")
driver.quit()
@@ -0,0 +1,40 @@
"""undetected-chromedriver + CloakBrowser: double stealth layer.
undetected-chromedriver patches ChromeDriver detection signals,
CloakBrowser patches the browser fingerprints at the C++ level.
Requires: pip install undetected-chromedriver cloakbrowser
"""
import undetected_chromedriver as uc
from cloakbrowser.config import get_chromium_version, get_default_stealth_args
from cloakbrowser.download import ensure_binary
binary_path = ensure_binary()
stealth_args = get_default_stealth_args()
chromium_major = int(get_chromium_version().split(".")[0])
options = uc.ChromeOptions()
options.binary_location = binary_path
options.add_argument("--headless")
for arg in stealth_args:
options.add_argument(arg)
driver = uc.Chrome(options=options, version_main=chromium_major)
driver.get("https://example.com")
print(f"undetected-chromedriver + CloakBrowser: {driver.title}")
# Verify stealth
result = driver.execute_script("""
return {
webdriver: navigator.webdriver,
plugins: navigator.plugins.length,
platform: navigator.platform,
hardwareConcurrency: navigator.hardwareConcurrency,
}
""")
print(f"Stealth checks: {result}")
driver.quit()
+6 -5
View File
@@ -11,12 +11,12 @@
Drop-in Playwright/Puppeteer replacement. Same API, same code — just swap the import. **3 lines of code, 30 seconds to unblock.**
- **32 source-level C++ patches** — canvas, WebGL, audio, fonts, GPU, screen, automation signals
- **33 source-level C++ patches** — canvas, WebGL, audio, fonts, GPU, screen, automation signals
- **0.9 reCAPTCHA v3 score** — human-level, server-verified
- **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — tested against 30+ detection sites
- **`npm install cloakbrowser`** — binary auto-downloads, auto-updates, zero config
- **Free and open source** — no subscriptions, no usage limits
- **Works with any framework** also tested with Selenium, undetected-chromedriver, browser-use, Crawl4AI, and agent-browser
- **Works with any framework** — tested with browser-use, Crawl4AI, Scrapling, Stagehand ([example](examples/stagehand.ts)), LangChain, Selenium, and more
## Install
@@ -203,10 +203,11 @@ const page = await browser.newPage();
| Platform | Chromium | Patches | Status |
|---|---|---|---|
| Linux x86_64 | 145 | 26 | ✅ Latest |
| Linux x86_64 | 145 | 33 | ✅ Latest |
| Linux arm64 (RPi, Graviton) | 145 | 33 | ✅ Latest |
| macOS arm64 (Apple Silicon) | 145 | 26 | ✅ Latest |
| macOS x86_64 (Intel) | 145 | 26 | ✅ Latest |
| Windows x86_64 | 145 | 26 | ✅ Latest |
| Windows x86_64 | 145 | 33 | ✅ Latest |
## Requirements
@@ -264,7 +265,7 @@ export CLOAKBROWSER_BINARY_PATH=~/.cloakbrowser/chromium-145.0.7632.159.2/chrome
export CLOAKBROWSER_BINARY_PATH=~/.cloakbrowser/chromium-145.0.7632.109.2/Chromium.app/Contents/MacOS/Chromium
# Windows
set CLOAKBROWSER_BINARY_PATH=%USERPROFILE%\.cloakbrowser\chromium-145.0.7632.109.2\chrome.exe
set CLOAKBROWSER_BINARY_PATH=%USERPROFILE%\.cloakbrowser\chromium-145.0.7632.159.7\chrome.exe
```
## Links
+36
View File
@@ -0,0 +1,36 @@
/**
* Stagehand + CloakBrowser: AI browser automation with stealth fingerprints.
*
* Stagehand handles AI-powered navigation and actions,
* CloakBrowser handles bot detection.
*
* Requires: npm install @browserbasehq/stagehand cloakbrowser
* Set OPENAI_API_KEY for the AI model.
*
* Usage:
* CLOAKBROWSER_BINARY_PATH=/path/to/chrome npx tsx examples/stagehand.ts
*/
import { Stagehand } from "@browserbasehq/stagehand";
import { ensureBinary } from "../src/download.js";
import { getDefaultStealthArgs } from "../src/config.js";
const binaryPath = await ensureBinary();
const stealthArgs = getDefaultStealthArgs();
const stagehand = new Stagehand({
env: "LOCAL",
localBrowserLaunchOptions: {
executablePath: binaryPath,
args: stealthArgs,
headless: true,
},
});
await stagehand.init();
const page = stagehand.context.pages()[0];
await page.goto("https://example.com");
console.log(`Stagehand + CloakBrowser: ${await page.title()}`);
await stagehand.close();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cloakbrowser",
"version": "0.3.14",
"version": "0.3.19",
"description": "Stealth Chromium that passes every bot detection test. Drop-in Playwright/Puppeteer replacement with source-level fingerprint patches.",
"type": "module",
"main": "dist/index.js",
+9
View File
@@ -20,6 +20,15 @@ export function buildArgs(options: LaunchOptions): string[] {
seen.set(arg.split("=")[0], arg);
}
}
// GPU blocklist bypass:
// - Headed mode (all platforms): Chromium blocks WebGL on software GPUs
// in Docker/Xvfb. Flag lets SwiftShader serve WebGL. See issue #56.
// - Windows (all modes): Chromium's GPU blocklist blocks WebGPU for the
// Microsoft Basic Render Driver. Dawn's adapter_blocklist bypass alone
// isn't enough. Linux doesn't need it.
if (options.headless === false || process.platform === "win32") {
seen.set("--ignore-gpu-blocklist", "--ignore-gpu-blocklist");
}
if (options.args) {
for (const arg of options.args) {
const key = arg.split("=")[0];
+4 -3
View File
@@ -27,13 +27,14 @@ export { WRAPPER_VERSION };
// CHROMIUM_VERSION is the latest across all platforms (for display/reference).
// Use getChromiumVersion() for the current platform's actual version.
// ---------------------------------------------------------------------------
export const CHROMIUM_VERSION = "145.0.7632.159.6";
export const CHROMIUM_VERSION = "145.0.7632.159.8";
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
"linux-x64": "145.0.7632.159.6",
"linux-x64": "145.0.7632.159.8",
"linux-arm64": "145.0.7632.159.7",
"darwin-arm64": "145.0.7632.109.2",
"darwin-x64": "145.0.7632.109.2",
"windows-x64": "145.0.7632.109.2",
"windows-x64": "145.0.7632.159.7",
};
// ---------------------------------------------------------------------------
+10 -8
View File
@@ -146,13 +146,14 @@ export async function checkForUpdate(): Promise<string | null> {
function showWelcome(): void {
const marker = path.join(getCacheDir(), ".welcome_shown");
if (fs.existsSync(marker)) return;
console.log();
console.log(" CloakBrowser — stealth Chromium for automation");
console.log(" https://github.com/CloakHQ/CloakBrowser");
console.log();
console.log(" Issues? https://github.com/CloakHQ/CloakBrowser/issues");
console.log(" Star us if CloakBrowser helps your project!");
console.log();
console.error();
console.error(" CloakBrowser — stealth Chromium for automation");
console.error(" https://github.com/CloakHQ/CloakBrowser");
console.error();
console.error(" Issues? https://github.com/CloakHQ/CloakBrowser/issues");
console.error(" Donate? https://ko-fi.com/cloakhq");
console.error(" Star us if CloakBrowser helps your project!");
console.error();
try {
fs.mkdirSync(getCacheDir(), { recursive: true });
fs.writeFileSync(marker, "");
@@ -227,7 +228,8 @@ async function verifyDownloadChecksum(filePath: string, version?: string): Promi
await verifyChecksum(filePath, expected);
}
async function fetchChecksums(version?: string): Promise<Map<string, string> | null> {
/** @internal Exported for testing only. */
export async function fetchChecksums(version?: string): Promise<Map<string, string> | null> {
const v = version || getChromiumVersion();
const hasCustomUrl = !!process.env.CLOAKBROWSER_DOWNLOAD_URL;
+22 -5
View File
@@ -22,6 +22,11 @@ const NEARBY_KEYS: Record<string, string> = {
'6': '57ty', '7': '68yu', '8': '79ui', '9': '80io', '0': '9p',
};
function isAscii(ch: string): boolean {
const code = ch.codePointAt(0);
return code !== undefined && code < 128;
}
function getNearbyKey(ch: string): string {
const lower = ch.toLowerCase();
if (lower in NEARBY_KEYS) {
@@ -38,11 +43,23 @@ export async function humanType(
text: string,
cfg: HumanConfig,
): Promise<void> {
for (let i = 0; i < text.length; i++) {
const ch = text[i];
const chars = [...text]; // Handle emoji surrogate pairs correctly
// Mistype chance — press wrong key, notice, backspace, then correct
if (Math.random() < cfg.mistype_chance && /[a-zA-Z0-9]/.test(ch)) {
for (let i = 0; i < chars.length; i++) {
const ch = chars[i];
// Non-ASCII characters (Cyrillic, CJK, emoji) — use insertText
if (!isAscii(ch)) {
await sleep(randRange(cfg.key_hold));
await raw.insertText(ch);
if (i < chars.length - 1) {
await interCharDelay(cfg);
}
continue;
}
// Mistype chance — only for ASCII alphanumeric
if (Math.random() < cfg.mistype_chance && /^[a-zA-Z0-9]$/.test(ch)) {
const wrong = getNearbyKey(ch);
await typeNormalChar(raw, wrong, cfg);
await sleep(randRange(cfg.mistype_delay_notice));
@@ -60,7 +77,7 @@ export async function humanType(
await typeNormalChar(raw, ch, cfg);
}
if (i < text.length - 1) {
if (i < chars.length - 1) {
await interCharDelay(cfg);
}
}
+75
View File
@@ -530,6 +530,81 @@ function buildMockPage(overrides: Record<string, any> = {}): any {
return page;
}
// =========================================================================
// humanType non-ASCII
// =========================================================================
describe("humanType non-ASCII", () => {
function makeRawKeyboardMock() {
const downKeys: string[] = [];
const insertedChars: string[] = [];
const raw = {
down: vi.fn(async (k: string) => { downKeys.push(k); }),
up: vi.fn(async () => {}),
type: vi.fn(async () => {}),
insertText: vi.fn(async (t: string) => { insertedChars.push(t); }),
};
return { raw, downKeys, insertedChars };
}
it("types Cyrillic via insertText, not down", async () => {
const { humanType } = await import("../src/human/keyboard.js");
const cfg = resolveConfig("default", { mistype_chance: 0 });
const { raw, downKeys, insertedChars } = makeRawKeyboardMock();
await humanType({} as any, raw, "Привет", cfg);
expect(insertedChars.join("")).toBe("Привет");
for (const k of downKeys) {
expect(k.charCodeAt(0)).toBeLessThan(128);
}
});
it("types mixed ASCII + Cyrillic correctly", async () => {
const { humanType } = await import("../src/human/keyboard.js");
const cfg = resolveConfig("default", { mistype_chance: 0 });
const { raw, downKeys, insertedChars } = makeRawKeyboardMock();
await humanType({} as any, raw, "Hi Мир", cfg);
expect(downKeys).toContain("H");
expect(downKeys).toContain("i");
expect(insertedChars.join("")).toContain("М");
expect(insertedChars.join("")).toContain("и");
expect(insertedChars.join("")).toContain("р");
});
it("types CJK via insertText", async () => {
const { humanType } = await import("../src/human/keyboard.js");
const cfg = resolveConfig("default", { mistype_chance: 0 });
const { raw, insertedChars } = makeRawKeyboardMock();
await humanType({} as any, raw, "你好", cfg);
expect(insertedChars.join("")).toBe("你好");
});
it("types emoji via insertText", async () => {
const { humanType } = await import("../src/human/keyboard.js");
const cfg = resolveConfig("default", { mistype_chance: 0 });
const { raw, insertedChars } = makeRawKeyboardMock();
await humanType({} as any, raw, "Hi 👋", cfg);
expect(insertedChars.join("")).toContain("👋");
});
it("mistype only triggers for ASCII, not Cyrillic", async () => {
const { humanType } = await import("../src/human/keyboard.js");
const cfg = resolveConfig("default", { mistype_chance: 1.0 });
const { raw, downKeys } = makeRawKeyboardMock();
await humanType({} as any, raw, "AБ", cfg);
expect(downKeys).toContain("Backspace");
});
});
function buildMockFrame(): any {
return {
+49
View File
@@ -14,6 +14,7 @@ import {
checkWrapperUpdate,
clearCache,
ensureBinary,
fetchChecksums,
getLatestChromiumVersion,
parseChecksums,
resetWrapperUpdateChecked,
@@ -269,6 +270,54 @@ describe("parseChecksums", () => {
});
});
describe("download fallback", () => {
afterEach(() => {
vi.restoreAllMocks();
delete process.env.CLOAKBROWSER_DOWNLOAD_URL;
});
it("checksum fetch falls back to GitHub on primary 429", async () => {
const HASH =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
const checksumText = `${HASH} cloakbrowser-${getPlatformTag()}.tar.gz`;
vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: (input as Request).url;
if (url.includes("cloakbrowser.dev")) {
return {
ok: false,
status: 429,
statusText: "Too Many Requests",
} as Response;
}
// GitHub fallback
return { ok: true, text: async () => checksumText } as Response;
});
const result = await fetchChecksums();
expect(result).not.toBeNull();
expect(
result!.has(`cloakbrowser-${getPlatformTag()}.tar.gz`)
).toBe(true);
});
it("checksum fetch returns null when both sources fail", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: false,
status: 429,
statusText: "Too Many Requests",
} as Response);
const result = await fetchChecksums();
expect(result).toBeNull();
});
});
describe("effective version", () => {
it("returns platform version when no marker exists", () => {
// Default behavior — no marker file in test environment
+121
View File
@@ -406,6 +406,127 @@ class TestSelectAllPlatform:
assert _SELECT_ALL == "Control+a"
# =========================================================================
# 11. Non-ASCII keyboard input
# =========================================================================
class TestNonAsciiKeyboard:
def test_cyrillic_uses_insert_text(self):
from cloakbrowser.human.keyboard import human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock
cfg = resolve_config("default", {"mistype_chance": 0})
page = MagicMock()
raw = MagicMock()
down_keys = []
inserted = []
raw.down = MagicMock(side_effect=lambda k: down_keys.append(k))
raw.up = MagicMock()
raw.insert_text = MagicMock(side_effect=lambda t: inserted.append(t))
human_type(page, raw, "Привет", cfg)
assert "".join(inserted) == "Привет"
for k in down_keys:
assert ord(k[0]) < 128 or k in ("Shift", "Backspace")
def test_mixed_ascii_cyrillic(self):
from cloakbrowser.human.keyboard import human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock
cfg = resolve_config("default", {"mistype_chance": 0})
page = MagicMock()
raw = MagicMock()
down_keys = []
inserted = []
raw.down = MagicMock(side_effect=lambda k: down_keys.append(k))
raw.up = MagicMock()
raw.insert_text = MagicMock(side_effect=lambda t: inserted.append(t))
human_type(page, raw, "Hi Мир", cfg)
assert "H" in down_keys
assert "i" in down_keys
assert "М" in "".join(inserted)
def test_cjk_uses_insert_text(self):
from cloakbrowser.human.keyboard import human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock
cfg = resolve_config("default", {"mistype_chance": 0})
page = MagicMock()
raw = MagicMock()
inserted = []
raw.down = MagicMock()
raw.up = MagicMock()
raw.insert_text = MagicMock(side_effect=lambda t: inserted.append(t))
human_type(page, raw, "你好", cfg)
assert "".join(inserted) == "你好"
def test_mistype_only_ascii(self):
from cloakbrowser.human.keyboard import human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock
cfg = resolve_config("default", {"mistype_chance": 1.0})
page = MagicMock()
raw = MagicMock()
down_keys = []
raw.down = MagicMock(side_effect=lambda k: down_keys.append(k))
raw.up = MagicMock()
raw.insert_text = MagicMock()
human_type(page, raw, "", cfg)
assert "Backspace" in down_keys
def test_no_error_on_cyrillic(self):
from cloakbrowser.human.keyboard import human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock
cfg = resolve_config("default", {"mistype_chance": 0})
page = MagicMock()
raw = MagicMock()
raw.down = MagicMock()
raw.up = MagicMock()
raw.insert_text = MagicMock()
# Should not raise
human_type(page, raw, "Тест кириллицы", cfg)
class TestNonAsciiKeyboardAsync:
@pytest.mark.asyncio
async def test_async_cyrillic_uses_insert_text(self):
from cloakbrowser.human.keyboard_async import async_human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock, AsyncMock
cfg = resolve_config("default", {"mistype_chance": 0})
page = MagicMock()
raw = MagicMock()
inserted = []
raw.down = AsyncMock()
raw.up = AsyncMock()
raw.insert_text = AsyncMock(side_effect=lambda t: inserted.append(t))
await async_human_type(page, raw, "Привет", cfg)
assert "".join(inserted) == "Привет"
# =========================================================================
# SLOW TESTS — require browser (skipped in CI unless pytest -m slow)
# =========================================================================
+74
View File
@@ -20,6 +20,8 @@ from cloakbrowser.config import (
)
from cloakbrowser.download import (
_check_wrapper_update,
_download_and_extract,
_fetch_checksums,
_get_latest_chromium_version,
_parse_checksums,
_should_check_for_update,
@@ -474,3 +476,75 @@ class TestWriteVersionMarker:
marker = tmp_path / f"latest_version_{get_platform_tag()}"
assert marker.exists()
assert marker.read_text() == "999.0.0.0"
class TestDownloadFallback:
"""Verify primary server (cloakbrowser.dev) → GitHub Releases fallback on HTTP errors."""
def test_binary_download_falls_back_on_http_error(self, tmp_path):
"""HTTP error from primary triggers GitHub Releases fallback for binary download."""
with patch.dict(os.environ, {
"CLOAKBROWSER_CACHE_DIR": str(tmp_path),
"CLOAKBROWSER_DOWNLOAD_URL": "",
"CLOAKBROWSER_SKIP_CHECKSUM": "true",
}):
urls_called = []
def mock_download_file(url, dest):
urls_called.append(url)
if "cloakbrowser.dev" in url:
raise Exception("HTTP 429 Too Many Requests")
# GitHub fallback succeeds
dest.write_bytes(b"fake")
with patch("cloakbrowser.download._download_file", side_effect=mock_download_file), \
patch("cloakbrowser.download._extract_archive"), \
patch("cloakbrowser.download._show_welcome"):
_download_and_extract()
assert len(urls_called) == 2
assert "cloakbrowser.dev" in urls_called[0]
assert "github.com" in urls_called[1]
def test_binary_download_no_fallback_with_custom_url(self, tmp_path):
"""Custom CLOAKBROWSER_DOWNLOAD_URL disables GitHub fallback — error propagates."""
with patch.dict(os.environ, {
"CLOAKBROWSER_CACHE_DIR": str(tmp_path),
"CLOAKBROWSER_DOWNLOAD_URL": "https://my-mirror.com/releases",
"CLOAKBROWSER_SKIP_CHECKSUM": "true",
}):
with patch("cloakbrowser.download._download_file", side_effect=Exception("503")):
with pytest.raises(Exception, match="503"):
_download_and_extract()
def test_checksum_fetch_falls_back_on_http_error(self):
"""HTTP error from primary checksum URL triggers GitHub fallback."""
valid_checksums = (
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
" cloakbrowser-linux-x64.tar.gz\n"
)
def mock_get(url, **kwargs):
resp = MagicMock()
if "cloakbrowser.dev" in url:
resp.raise_for_status.side_effect = Exception("HTTP 429")
return resp
# GitHub URL succeeds
resp.text = valid_checksums
resp.raise_for_status = MagicMock()
return resp
with patch.dict(os.environ, {"CLOAKBROWSER_DOWNLOAD_URL": ""}):
with patch("cloakbrowser.download.httpx.get", side_effect=mock_get):
result = _fetch_checksums()
assert result is not None
assert "cloakbrowser-linux-x64.tar.gz" in result
def test_checksum_fetch_returns_none_when_both_fail(self):
"""Both primary and GitHub checksum URLs fail → returns None (skip verification)."""
with patch.dict(os.environ, {"CLOAKBROWSER_DOWNLOAD_URL": ""}):
with patch("cloakbrowser.download.httpx.get", side_effect=Exception("network error")):
result = _fetch_checksums()
assert result is None