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.3"
= { = "1", = ["rt-multi-thread", "macros"] }
Quick Start
use ;
async
Login Flow Example
use ;
async
API
Browser
let browser = launch.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?;
Retry & Cookies
page.with_retry.await?;
let cookies = page.cookies.await?;
page.set_cookie.await?;
Config
let config = StealthConfig ;
// Presets
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
~4.5K lines of Rust. No chromiumoxide, no puppeteer-extra. Hand-written CDP types for the ~30 commands actually needed. 8 crate dependencies.
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