Hexium Browser
Launch like Chrome. Stay undetected.
Playwright in. Chrome 151 out. Built by HeadlessX Labs.
Sites see Google Chrome 151. You see Hexium Browser — a C++ persona compiled into Chromium, persistent disk profiles, humanize, and GeoIP locale. No JS injectors.
Headless Google search, new profile — examples/google_search_human_headless_new_profile.py
Features
- Playwright
launch()— headed and--headless=new - Linux x86_64 binary in this alpha (Windows and macOS not supported yet)
- Persistent disk profiles (real tabs, not Incognito)
- C++ persona presets — Linux default
linux-native; see Personas - Seeded fingerprint sample (
fingerprint=or auto--hexium-seed=) — not a new GPU every page - Humanized mouse, keys, and scroll (
humanize=True) - GeoIP timezone, locale, and WebRTC IP from the egress IP (on by default)
- Chrome 151 web identity (User-Agent and related surface)
Depth lives in docs/. This README is the quickstart.
Launch
from hexium_browser import launch
browser = launch(headless=True) # new ~/.hexium/profiles/hexium-session-* + new fingerprint
browser = launch(
profile="Work",
headless=True,
proxy="http://user:pass@host:port",
humanize=True,
fingerprint="account-1",
) # sticky ~/.hexium/profiles/Work
browser.new_page() opens a tab. Bare launch() is a new visitor each time; use profile="Work" or HEXIUM_USER_DATA_DIR to return to the same identity.
python examples/linux_chrome_persona.py
python examples/windows_chrome_persona.py --headed
python examples/stealth_test.py
python examples/open_google.py
Install
The Python package is not on PyPI. Install from source:
pip install -e '.[geoip]'
Requires Python 3.9+ and the Playwright driver (ships with the playwright package). Do not install Playwright’s Chromium as the browser — Hexium launches its own Chrome 151 binary.
Optional GeoIP extra: geoip2 + socksio. GeoIP is on by default; without the extra, launch continues and timezone stays the sampled UTC.
Profiles
Hexium is a real Chrome user-data-dir, not Incognito. Bare launch() creates a new ~/.hexium/profiles/hexium-session-* and a new fingerprint. Named profiles stick:
hexium-browser profiles
hexium-browser profiles new Work
hexium-browser profiles use Work
hexium-browser profiles last
from hexium_browser import launch
launch() # new identity
launch(profile="Work") # returning visitor
The sampled persona sticks on a named profile until you pass a new fingerprint= or use another profile=.
Migrating from Playwright? One-line change:
- from playwright.sync_api import sync_playwright
- pw = sync_playwright().start()
- browser = pw.chromium.launch()
+ from hexium_browser import launch
+ browser = launch()
page = browser.new_page()
page.goto("https://example.com")
# ... rest of your code works unchanged
page.goto(), click(), fill(), locator(), and browser.close() are the same Playwright objects. Do not pass executable_path or channel. browser.new_page() is a tab on a persistent disk profile (not Incognito). If you used browser.new_context(), switch to launch(profile="Work") or keep calling new_page().
Why Hexium Browser?
- JS stealth injectors break — they patch
navigatorin page JS. Detection sites look for the patch. Hexium does not inject stealth scripts. - Persona is compiled in — GPU, screen, UA, and hardware reporting follow a sampled Chrome persona at the C++ layer, plus a persistent profile on disk.
- Same Playwright API —
launch(),new_page(),click(),fill(). Swap the import. - Humanize is a flag — Bézier mouse, per-character typing, realistic scroll. A virtual mouse pointer is on whenever
humanizeis on; passshow_cursor=Falsefor stealth oracles.
Hexium does not solve CAPTCHAs. Bring your own proxy. Use the Playwright API you already know.
Test results
Captured 12 Sep 2026 against live oracles with Chrome 151 (examples/stealth_test.py, examples/recaptcha_score.py, examples/fingerprint_scan_test.py). These are screenshots, not a guarantee that every site will score the same.
| Oracle | What the capture shows |
|---|---|
| reCAPTCHA v3 demo | score 0.9 |
| BrowserScan bot detection | Normal |
| deviceandbrowserinfo.com | You are human! (isBot: false) |
| bot.sannysoft.com | WebDriver missing, window.chrome present, plugins 5, UA Chrome/151 |
| Rebrowser bot detector | no webdriver / no __pwInitScripts (some checks need a click to fire) |
| reCAPTCHA v3 demo — score 0.9 | BrowserScan bot detection — Normal |
| deviceandbrowserinfo.com — “You are human!” | bot.sannysoft.com — WebDriver missing, Chrome present, plugins 5 |
| Rebrowser bot detector — no webdriver, no Playwright init scripts | bot.incolumitas.com — examples/stealth_test.py |
CreepJS
CreepJS (noise=false) — examples/stealth_test.py |
CreepJS — examples/fingerprint_scan_test.py |
fingerprint-scan.com
fingerprint-scan.com — examples/fingerprint_scan_test.py
Stealth/oracle examples pass humanize=True and show_cursor=False. The virtual pointer is a DOM tell. launch() turns it on whenever humanize is on; pass False to hide it.
Humanize
launch() defaults to humanize=True, and show_cursor follows that. Playwright never moves the OS cursor — a standard mouse arrow is the virtual pointer.
from hexium_browser import launch
browser = launch(headless=False, humanize=True)
page = browser.new_page()
page.goto("https://example.com")
page.locator("#email").fill("user@example.com")
page.locator("button[type=submit]").click()
| Interaction | Playwright default | humanize=True |
|---|---|---|
| Mouse | Instant teleport | Bézier curve, easing, slight overshoot |
| Clicks | Instant | Aim point + hold |
| Keyboard | Instant fill | Per-character timing |
| Scroll | Jump | Accelerate → cruise → decelerate |
| Humanize click demo | Still |
|---|---|
| Same demo — virtual highlighter mid-path |
| Headed Google search | Still |
|---|---|
| After the headed search — “Nextjs Latest version” |
| DABI interaction oracle | Still |
|---|---|
| Same interaction run — still frame |
| Headless, reused profile | Headless, fresh profile |
|---|---|
Personas
launch(persona=…) or HEXIUM_PERSONA. Linux default is linux-native. Windows default is windows-native. macOS default is macos-native (alias mac-native). Pass windows-chrome or linux-chrome to sample. A *-native name that does not match this OS remaps to the host native. The engine binary alone uses the same host defaults.
| Preset | What sites see | GPU / fonts | When |
|---|---|---|---|
linux-native |
This machine | Host GPU, fonts, screen, CPU | Linux default. |
windows-native |
This Windows machine | Host GPU, fonts, screen, CPU | Windows default. |
macos-native |
This macOS machine | Host GPU/fonts | macOS default. Alias mac-native. No macos-chrome. |
linux-chrome |
Linux Chrome 151 | Host GPU and fonts; screen/CPU/RAM still sampled | persona="linux-chrome" |
windows-chrome |
Win32 + Chrome 151 | Sampled D3D11 WebGL + Segoe pack. On Linux: known WebGL −5% pixel-vs-name tell (not claimed fixed). | persona="windows-chrome" (alias windows-1080p) |
from hexium_browser import launch
launch() # Linux: linux-native + GeoIP tz/lang
launch(persona="linux-chrome") # Linux UA, real GPU, sampled screen
launch(persona="windows-chrome") # Win32 UA + D3D + Segoe pack
launch(fingerprint="off") # fingerprint patches off
launch(persona="windows-chrome", fingerprint="account-1") # same machine next time
Windows-on-Linux needs a Segoe pack (HEXIUM_FONTS_DIR, or the engine Windows font bundle). Desktop UA-CH model is empty (stock Chrome 151). Do not cartesian OS×GPU — one OS + one joint sample.
python examples/linux_chrome_persona.py
python examples/windows_chrome_persona.py --headed
What is random vs sticky
A fingerprint seed picks one coherent device. Same seed → same machine. Bare launch() mints a new seed (and a new hexium-session-*). profile="Work" or fingerprint="account-1" reuses persona.json.
| Surface | Source | Sticky with seed? |
|---|---|---|
| Screen (width/height/avail/DPR) | Joint Chrome desktop sample | Yes |
hardwareConcurrency, deviceMemory |
Same sample (Windows desktop min 4 GB) | Yes |
| WebGL vendor/renderer, WebGPU vendor/arch | Same sample (windows-chrome only) |
Yes |
UA-CH platformVersion (Win10 vs Win11) |
Same sample; clamped to 10.0.0 / 15.0.0 (never 19.0.0); UA rewritten to 151.0.7922.174 | Yes |
| Fonts | Fixed Windows pack on windows-chrome; host on linux-* |
Yes (not shuffled per page) |
Timezone, locale, navigator.languages, WebRTC IP |
GeoIP from egress IP (default on) | Follows the proxy/IP, not the seed |
| Canvas LSB noise | Off unless args=["--hexium-noise=on"] |
Seed-stable if you turn it on |
| Mouse / keys / scroll | humanize=True (default) |
No — new path each action (human_preset="default" or "careful") |
GeoIP does not re-roll the GPU. Explicit timezone= / locale= always win over GeoIP.
linux-chrome persona on example.com (examples/linux_chrome_persona.py)
Binary
Linux x86_64 only for this alpha. Windows and macOS are not supported yet — we are actively working on them. Thanks for your patience.
Archives are named Hexium + engine version + OS + arch:
Hexium-{VERSION}-{os}-{arch}.tar.gz
This ship:
Hexium-151.0.7922.174.1-linux-x64.tar.gz
Unpack it and you get hexium-v151.0.7922.174.1/chrome (same layout as ~/.hexium/hexium-v{VERSION}/). Point HEXIUM_BINARY_PATH at that chrome, or let hexium-browser fetch unpack into the cache when downloads are published.
We are not cutting GitHub Releases until the Python package is on PyPI. Until then, use a local build or a tarball you were given — not a release asset.
Resolution order:
HEXIUM_BINARY_PATH(aliasHEXIUM_BINARY)$HEXIUM_OUT/hexium-v{VERSION}/chromeif it exists- Cache under
~/.hexium/hexium-v{VERSION}/ hexium-browser fetchfromhttps://headlessx.dev/api/download(when that tarball exists)
Pin the engine with HEXIUM_VERSION.
| Env | Job |
|---|---|
HEXIUM_DOWNLOAD_URL |
Download prefix (default https://headlessx.dev/api/download) |
HEXIUM_VERSION |
Engine version (151.0.7922.174.1) |
HEXIUM_BINARY_PATH |
Exact chrome path |
HEXIUM_OUT |
Local out root |
HEXIUM_CACHE_DIR |
Cache root (~/.hexium) |
hexium-browser info
hexium-browser fetch
GeoIP
GeoIP is on by default. With hexium-browser[geoip], Hexium maps the egress IP (proxy exit, or the machine public IP when there is no proxy) to timezone, locale, languages, and --hexium-webrtc-ip=. Pass geoip=False to opt out. Explicit timezone= / locale= always win. Without the extra, launch continues and timezone stays the sampled UTC. Missing GeoLite2 still returns the exit IP so WebRTC does not leak LAN.
Docs
| Doc | What |
|---|---|
| Changelog | 0.1.0 alpha and later |
| Usage | launch(), personas, GeoIP, humanize, profiles |
| Personas | linux-native, linux-chrome, windows-native, windows-chrome, macos-native |
| Contributing | Install, pytest, bug reports |
| Support | How to file issues |
| Security | Vulnerability reports |
| CLAUDE.md | Agent / contributor product rules |
Persona notes: linux-native · linux-chrome · windows-native · windows-chrome · macos-native
Troubleshooting
Still getting blocked on aggressive sites (DataDome, Turnstile)?
Some sites detect --headless=new even with the C++ persona. Run headed on a virtual display when you have no monitor:
# Install Xvfb (virtual framebuffer)
sudo apt install xvfb
# Start virtual display
Xvfb :99 -screen 0 1920x1080x24 &
export DISPLAY=:99
from hexium_browser import launch
# Headed Chrome + residential proxy
browser = launch(headless=False, proxy="http://your-residential-proxy:port")
page = browser.new_page()
page.goto("https://example.com")
browser.close()
That is a real headed window on Xvfb — no physical monitor. Combine with the config below. Pass show_cursor=False on stealth oracles (the highlighter is a DOM tell). Hexium does not solve CAPTCHAs.
Recommended config for anti-bot sites
Most blocks come from missing one of these three things, not from browser fingerprint detection:
from hexium_browser import launch
browser = launch(
proxy="http://your-residential-proxy:port", # residential IP — datacenter IPs get blocked by reputation alone
geoip=True, # default; timezone + locale follow the proxy exit IP
headless=False, # headed — some sites still detect --headless=new
humanize=True, # default; mouse, keys, scroll
)
geoip=True needs pip install -e '.[geoip]'. Without the extra, launch still works and timezone stays the sampled UTC.
If the proxy supports SOCKS5, prefer it — SOCKS5 tunnels raw TCP and avoids HTTP CONNECT issues some proxies have with HTTP/2:
browser = launch(
proxy="socks5://user:pass@proxy:1080",
geoip=True,
headless=False,
humanize=True,
)
Linux default persona is linux-native. Windows default is windows-native. macOS default is macos-native. windows-chrome needs a Segoe pack (HEXIUM_FONTS_DIR, or the engine Windows font bundle). Named profile= keeps the same fingerprint; a bare launch() is a new visitor each time.
Sites challenge fresh sessions but work after first visit
Some sites challenge first-time visitors with no cookies over HTTP/2. That is a Chromium / site behavior, not a Hexium-only tell. Warm cookies once on a named profile, then reuse it:
from hexium_browser import launch
# First run: warm up with --disable-http2
browser = launch(profile="Shop", args=["--disable-http2"])
page = browser.new_page()
page.goto("https://example.com") # writes cookies into ~/.hexium/profiles/Shop
browser.close()
# Later runs — same profile, no --disable-http2
browser = launch(profile="Shop")
page = browser.new_page()
page.goto("https://example.com") # returning visitor
browser.close()
A bare launch() is a new hexium-session-* every time, so cookies never come back. Pin profile= (or user_data_dir=) when you need a returning visitor.
For a one-shot session, launch(args=["--disable-http2"]) forces HTTP/1.1. Only use that flag on sites that need it — most are fine on HTTP/2. SOCKS5 (proxy="socks5://user:pass@host:port") avoids HTTP CONNECT entirely.
What's next
Python hexium_browser already ships in this repo. First-party packages still to land (thin bindings over the same launch() contract — not a new fingerprint stack):
- .NET / NuGet
- npm
- Go
- Rust crate
- GitHub Action that caches the Hexium binary
- Java/JVM bindings if demand appears
License
- Python API — GNU Affero GPL v3.0 only (not “or later”)
chromebinary — BINARY-LICENSE.md (Chromium notices + Hexium build)
Last reviewed: 13 Sep 2026
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hexium_browser-0.1.0.tar.gz.
File metadata
- Download URL: hexium_browser-0.1.0.tar.gz
- Upload date:
- Size: 1.8 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
90dd020ee5fcc25abcf45ae0293cc40243ccda8636be22d7652e4f668c830abb
|
|
| MD5 |
e760fc2bf74ec4c80392e2cecf3f9a7d
|
|
| BLAKE2b-256 |
99578f4c1bee8f12adecb49ace4c0a32d6d0eaa2edc1bed4512aff5f29db52ca
|
File details
Details for the file hexium_browser-0.1.0-py3-none-any.whl.
File metadata
- Download URL: hexium_browser-0.1.0-py3-none-any.whl
- Upload date:
- Size: 1.8 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
58c878b9ab72bb6b8b56841f1a1b9a50b20cf71ae10e9318fb8e432bc1afdd1d
|
|
| MD5 |
9328c77fce4094d66414f5f578a83c0e
|
|
| BLAKE2b-256 |
e3c36195e33eff78ac86ca0ab796cf2413a36391481511e6090c558e155673d1
|