Stealth browser automation in Rust. Passes bot detection without the bloat.
Chrome or Chromium installed. eoka launches and controls it via CDP.
[dependencies]
eoka = "0.3"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }use eoka::{Browser, Result};
#[tokio::main]
async fn main() -> Result<()> {
let browser = Browser::launch().await?;
let page = browser.new_page("https://example.com").await?;
page.human_click("#button").await?;
page.human_type("#input", "hello").await?;
let png = page.screenshot().await?;
std::fs::write("screenshot.png", png)?;
browser.close().await?;
Ok(())
}use eoka::{Browser, Result, StealthConfig};
#[tokio::main]
async fn main() -> Result<()> {
let browser = Browser::launch_with_config(StealthConfig::visible()).await?;
let page = browser.new_page("https://example.com/login").await?;
page.try_click_by_text("Accept Cookies").await?;
page.human_click_by_text("Sign In").await?;
page.wait_for_visible("#email", 10_000).await?;
page.human_fill("#email", "user@example.com").await?;
page.human_fill("#password", "secret123").await?;
page.human_click_by_text("Log In").await?;
page.wait_for_text("Welcome back", 15_000).await?;
browser.close().await?;
Ok(())
}let browser = Browser::launch().await?;
let browser = Browser::launch_with_config(config).await?;
let page = browser.new_page("https://example.com").await?;
let tabs = browser.tabs().await?;
browser.activate_tab(id).await?;
browser.close_tab(id).await?;
browser.close().await?;page.find("#button").await?; // CSS selector
page.find_all(".item").await?; // all matches
page.find_by_text("Sign In").await?; // by visible text
page.find_any(&["#email", "[name='email']"]).await?; // first match
page.exists("#popup").await; // bool
page.text_exists("Error").await; // boolpage.click("#button").await?; // instant
page.human_click("#button").await?; // with mouse movement
page.click_by_text("Submit").await?;
page.human_click_by_text("Submit").await?;
page.try_click("#optional").await?; // Ok(false) if missing
page.try_click_by_text("Accept").await?;page.fill("#email", "user@example.com").await?; // clear + type
page.human_fill("#email", "user@example.com").await?; // human-like
page.type_into("#search", "query").await?; // append (no clear)
page.human_type("#search", "query").await?;page.wait_for("#results", 10_000).await?; // in DOM
page.wait_for_visible("#email", 10_000).await?; // visible + clickable
page.wait_for_hidden(".loading", 5_000).await?;
page.wait_for_any(&["#ok", ".error"], 10_000).await?;
page.wait_for_text("Success", 10_000).await?;
page.wait_for_url_contains("dashboard", 10_000).await?;
page.wait_for_url_change(10_000).await?;
page.wait_for_network_idle(500, 30_000).await?; // XHR/fetch idlelet elem = page.find("#btn").await?;
elem.click().await?;
elem.is_visible().await?; // Result<bool>
elem.bounding_box().await; // Option<BoundingBox>
elem.get_attribute("href").await?; // Option<String>
elem.tag_name().await?;
elem.value().await?;
elem.text().await?;
elem.is_enabled().await?;
elem.is_checked().await?;
elem.css("color").await?;
elem.scroll_into_view().await?;page.press_key("Enter").await?;
page.press_key("Ctrl+A").await?;
page.select_all().await?;
page.copy().await?;
page.paste().await?;
page.hover("#menu").await?;
page.human_hover("#menu").await?;
page.select("#country", "US").await?;
page.select_by_text("#country", "United States").await?;
page.upload_file("input[type='file']", "/path/to/file.pdf").await?;
page.upload_files("input[type='file']", &["/a.pdf", "/b.pdf"]).await?;let count: i32 = page.evaluate("document.querySelectorAll('li').length").await?;
page.execute("window.scrollTo(0, 1000)").await?;
let title: String = page.evaluate_in_frame("iframe#widget", "document.title").await?;page.url().await?;
page.title().await?;
page.content().await?; // full HTML
page.text().await?; // visible text
page.screenshot().await?; // PNG bytes
page.screenshot_jpeg(80).await?; // JPEG at quality 80
page.debug_state().await?; // PageState with element counts
page.debug_screenshot("step1").await?; // timestamped screenshotlet page1 = browser.new_page("https://a.com").await?;
let page2 = browser.new_page("https://b.com").await?;
browser.activate_tab(page1.target_id()).await?;
browser.close_tab(page2.target_id()).await?;page.goto("https://example.com").await?;
page.goto_with_referrer("https://example.com", "https://google.com").await?;
page.goto_with_headers("https://example.com", headers).await?;
page.reload().await?;
page.back().await?;
page.forward().await?;let cookies = page.cookies().await?;
page.set_cookie("name", "value", Some("example.com"), None).await?;
page.delete_cookie("name", None).await?;
page.clear_all_cookies().await?;
page.enable_request_capture().await?; // start capturing XHR/fetch
let body = page.get_response_body(id).await?;
page.disable_request_capture().await?;page.set_bypass_csp(true).await?; // disable CSP
page.set_user_agent("custom UA").await?;
page.ignore_cert_errors(true).await?;
page.accept_dialog(None).await?; // accept alert/confirm
page.dismiss_dialog().await?; // dismiss dialogpage.with_retry(3, 500, || async {
page.human_click("#flaky").await
}).await?;let config = StealthConfig {
headless: false,
patch_binary: true,
human_mouse: true,
human_typing: true,
debug: true,
..Default::default()
};
// Presets
StealthConfig::visible() // headless: false
StealthConfig::debug() // headless: false, debug: truePatches 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)
~5K lines of Rust. No chromiumoxide, no puppeteer-extra. Hand-written CDP types for the ~30 commands actually needed. 9 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.
| Crate | Description |
|---|---|
| eoka-agent | AI agent layer with MCP server |
| eoka-runner | Config-based automation (YAML flows) |
MIT