eoka
Stealth browser automation in Rust. Passes bot detection without the bloat.
Requirements
Chrome or Chromium installed. eoka launches and controls it via CDP.
Install
[]
= "0.5"
= { = "1", = ["rt-multi-thread", "macros"] }
Quick Start
use ;
async
Login Flow Example
use ;
async
API
Browser
let browser = launch.await?;
let browser = launch_visible.await?;
let browser = launch_debug.await?;
let browser = launch_with.await?;
let browser = launch_with_config.await?;
let page = browser.new_page.await?;
let tabs = browser.tabs.await?;
browser.activate_tab.await?;
browser.close_tab.await?;
browser.close.await?;
Finding Elements
page.find.await?; // CSS selector
page.find_all.await?; // all matches
page.find_by_text.await?; // by visible text
page.find_any.await?; // first match
page.exists.await; // bool
page.text_exists.await; // bool
Clicking
page.click.await?; // instant
page.human_click.await?; // with mouse movement
page.click_by_text.await?;
page.human_click_by_text.await?;
page.try_click.await?; // Ok(false) if missing
page.try_click_by_text.await?;
Typing
page.fill.await?; // clear + type
page.human_fill.await?; // human-like
page.type_into.await?; // append (no clear)
page.human_type.await?;
Waiting
page.wait_for.await?; // in DOM
page.wait_for_visible.await?; // visible + clickable
page.wait_for_hidden.await?;
page.wait_for_any.await?;
page.wait_for_text.await?;
page.wait_for_url_contains.await?;
page.wait_for_url_change.await?;
page.wait_for_network_idle.await?; // XHR/fetch idle
Elements
let elem = page.find.await?;
elem.click.await?;
elem.is_visible.await?; // Result<bool>
elem.bounding_box.await; // Option<BoundingBox>
elem.get_attribute.await?; // Option<String>
elem.tag_name.await?;
elem.value.await?;
elem.text.await?;
elem.is_enabled.await?;
elem.is_checked.await?;
elem.css.await?;
elem.scroll_into_view.await?;
Keyboard, Hover, Select, Upload
page.press_key.await?;
page.press_key.await?;
page.select_all.await?;
page.copy.await?;
page.paste.await?;
page.hover.await?;
page.human_hover.await?;
page.select.await?;
page.select_by_text.await?;
page.upload_file.await?;
page.upload_files.await?;
JavaScript & Frames
let count: i32 = page.evaluate.await?;
page.execute.await?;
let title: String = page.evaluate_in_frame.await?;
Page Info & Debug
page.url.await?;
page.title.await?;
page.content.await?; // full HTML
page.text.await?; // visible text
page.screenshot.await?; // PNG bytes
page.screenshot_jpeg.await?; // JPEG at quality 80
page.debug_state.await?; // PageState with element counts
page.debug_screenshot.await?; // timestamped screenshot
Multi-Tab
let page1 = browser.new_page.await?;
let page2 = browser.new_page.await?;
browser.activate_tab.await?;
browser.close_tab.await?;
Connect to an existing Chrome
Attach to a browser you already launched with --remote-debugging-port instead of
spawning one. Defaults to StealthConfig::live(), so the user's tabs are left
untouched (no evasion injection).
// Start Chrome yourself: chrome --remote-debugging-port=9222
let browser = connect_port.await?; // HTTP discovery
// or, with the ws:// URL from http://localhost:9222/json/version:
let browser = connect.await?;
for tab in browser.tabs.await?
let page = browser.attach_page.await?;
browser.disconnect.await?; // leaves Chrome running
Navigation
page.goto.await?;
page.goto_with_referrer.await?;
page.goto_with_headers.await?;
page.reload.await?;
page.back.await?;
page.forward.await?;
Network & Cookies
let cookies = page.cookies.await?;
page.set_cookie.await?;
page.delete_cookie.await?;
page.clear_all_cookies.await?;
page.enable_request_capture.await?; // start capturing XHR/fetch
let body = page.get_response_body.await?;
page.disable_request_capture.await?;
Configuration & Dialogs
page.set_bypass_csp.await?; // disable CSP
page.set_javascript_enabled.await?; // disable JS entirely (before navigation)
page.set_user_agent.await?;
page.ignore_cert_errors.await?;
page.accept_dialog.await?; // accept alert/confirm
page.dismiss_dialog.await?; // dismiss dialog
page.ignore_cert_errors takes effect only for navigations issued after the
call — too late for new_page(url)'s own initial navigate. For a bad cert on
the very first load (e.g. an .onion service redirecting http:// →
untrusted https://), set StealthConfig.ignore_cert_errors = true instead,
or use new_blank_page() + ignore_cert_errors(true) + goto().
Retry
page.with_retry.await?;
Config
Most code does not need to construct a config. Browser::launch() starts with
stealth defaults in headless mode. Browser::launch_visible() and
Browser::launch_debug() are explicit opt-ins for local workflows that need a
window.
For small tweaks, mutate the default config inline:
let browser = launch_with.await?;
Two server-side reputation signals are handled automatically for spawned
stealth browsers (see StealthConfig):
strip_x_client_data: true(default) — Chrome'sX-Client-Datarequest header (field-trial variation IDs) is stripped via CDP Fetch interception on every page, and--disable-field-trial-configis passed at launch.geo_align: false— opt-in. Aligns timezone andnavigator.languageswith the browser's apparent public IP via a one-shot lookup before the first real navigation. Ignored whentimezoneis set explicitly.
For reusable presets or advanced setup, build a StealthConfig directly:
let config = StealthConfig ;
let browser = launch_with_config.await?;
// Presets for advanced config composition
visible // headless: false
debug // headless: false, debug: true
Detection Results
Patches Chrome binary, injects 15 evasion scripts, blocks detectable CDP commands at the transport layer, simulates human input with Bezier curves.
- Passes: sannysoft, rebrowser bot detector (6/6), areyouheadless, browserleaks
- Partial: creepjs (33% trust score)
How it Works
~9K lines of Rust (about 1.3K of that is tests). No chromiumoxide, no puppeteer-extra. Hand-written CDP types for the ~30 commands actually needed. Minimal dependency graph.
src/
├── cdp/ # raw websocket transport, command filtering
├── stealth/ # evasions, binary patcher, human simulation
├── browser.rs # chrome launcher
├── page.rs # page api
└── session.rs # cookie export
The key insight: most detection comes from CDP commands leaking (Runtime.enable fires consoleAPICalled events that pages can detect). eoka blocks those at the transport layer and defines navigator properties on the prototype instead of the instance.
Ecosystem
| Crate | Description |
|---|---|
| eoka-agent | AI agent layer with MCP server |
| eoka-runner | Config-based automation (YAML flows) |
License
MIT