{"id":"94832b2c-cd63-4c9d-8725-367064782074","entityType":"agent","slug":"clawhub-psyb0t-stealthy-auto-browse","name":"stealthy-auto-browse","canonicalUrl":"https://www.xpersona.co/agent/clawhub-psyb0t-stealthy-auto-browse","canonicalPath":"/agent/clawhub-psyb0t-stealthy-auto-browse","generatedAt":"2026-10-09T21:21:09.458Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":null},"description":"Browser automation that passes CreepJS, BrowserScan, Pixelscan, and Cloudflare — zero CDP exposure, OS-level input, persistent fingerprints. Use when standard browser skills get 403s or CAPTCHAs. Skill: stealthy-auto-browse Owner: psyb0t Summary: Browser automation that passes CreepJS, BrowserScan, Pixelscan, and Cloudflare — zero CDP exposure, OS-level input, persistent fingerprints. Use when standard browser skills get 403s or CAPTCHAs. Tags: latest:1.3.0 Version history: v1.3.0 | 2026-02-10T08:03:05.571Z | user better wording v1.2.1 | 2026-02-09T21:48:32.930Z | user Rewrite SKILL.md as a proper usage guide","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 2.3K downloads reported by the source. Last updated 4/15/2026.","installCommand":"clawhub skill install kn79dhvmpjng4rp2jjk8k0v5xx80ccbk:stealthy-auto-browse","sourceUrl":"https://clawhub.ai/psyb0t/stealthy-auto-browse","homepage":"https://clawhub.ai/psyb0t/stealthy-auto-browse","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/psyb0t/stealthy-auto-browse","kind":"source"}],"safetyScore":84,"overallRank":62,"popularityScore":67,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Browser automation that passes CreepJS, BrowserScan, Pixelscan, and Cloudflare — zero CDP exposure, OS-level input, persistent fingerprints. Use when standard b"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":null},"stars":null,"forks":null,"downloads":2265,"packageName":null,"latestVersion":"1.3.0","tractionLabel":"2.3K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-02-28T18:54:04.129Z","emptyReason":null},"lastUpdatedAt":"2026-04-15T00:45:39.800Z","lastCrawledAt":"2026-02-28T18:54:04.129Z","lastIndexedAt":null,"nextCrawlAt":"2026-03-01T18:54:04.129Z","lastVerifiedAt":null,"highlights":[{"version":"1.3.0","createdAt":"2026-02-10T08:03:05.571Z","changelog":"better wording","fileCount":2,"zipByteSize":13021},{"version":"1.2.1","createdAt":"2026-02-09T21:48:32.930Z","changelog":"Rewrite SKILL.md as a proper usage guide instead of a dry API reference - Every action now explains what it actually does, not just a terse label - Documented response data fields for all actions so agents know what comes back - Explained the difference between system_click, mouse_click, and click (three ways to click, different tradeoffs) - Added parameter details (required/optional, defaults, valid values) for every action - Included example response JSON for complex returns (get_interactive_elements, get_network_log, get_cookies, list_tabs, get_last_dialog, get_last_download) - Added \"When to use which\" decision guide for system vs Playwright input - Documented gotchas: handle_dialog timing, calibrate after fullscreen, upload_file still needs form submit, storage is per-origin - Added full workflow section showing the typical interaction sequence - Documented container options, pre-installed extensions, page loaders with match rule logic","fileCount":2,"zipByteSize":12172},{"version":"1.2.0","createdAt":"2026-02-09T21:31:14.431Z","changelog":"New actions: - Tabs: list_tabs, new_tab, switch_tab, close_tab - Dialogs: handle_dialog, get_last_dialog (alert/confirm/prompt) - Cookies: get_cookies, set_cookie, delete_cookies - Storage: get_storage, set_storage, clear_storage (local + session) - Downloads: get_last_download - Uploads: upload_file (Playwright set_input_files) - Network: enable/disable/get/clear_network_log - Waits: wait_for_element, wait_for_text, wait_for_url, wait_for_network_idle - Proxy: PROXY_URL environment variable for HTTP proxy support - XPath selector support (xpath= prefix) on all element actions","fileCount":null,"zipByteSize":null},{"version":"1.1.0","createdAt":"2026-02-09T10:57:25.525Z","changelog":"**stealthy-auto-browse 1.1.0 — Improved documentation, new usage guidance, and enhanced setup instructions** - Rewrote and reorganized documentation for clarity, including sections for when and when not to use this skill. - Added a detailed comparison with standard browser automation to highlight stealth advantages (no CDP exposure, OS-level input). - Clarified the setup process and included OpenClaw config and environment variable examples. - Expanded API examples with precise JSON payloads and usage tips for undetectable interaction. - Added container run examples for custom resolutions, timezones, persistent profiles, and URL auto-start. - Revised tips and best practices for maximizing stealth when automating sites with anti-bot protections.","fileCount":null,"zipByteSize":null},{"version":"1.0.1","createdAt":"2026-02-07T06:02:08.462Z","changelog":"remove runtime resulution setters","fileCount":null,"zipByteSize":null},{"version":"1.0.0","createdAt":"2026-02-02T00:32:50.919Z","changelog":"Initial release of stealthy-auto-browse: control a stealthy browser that evades bot detection using OS-level mouse/keyboard input. - Control a headless Firefox browser (Camoufox) that avoids common bot detection methods. - Supports both \"system\" (OS-level, undetectable) and Playwright (detectable) input modes. - Full API for navigation, mouse, keyboard, scrolling, screenshots, and state queries. - Includes workflow for undetectable interaction: navigate, discover elements and coordinates, perform mouse/keyboard input at the OS level. - Requires running the `stealthy-auto-browse` Docker container and setting `STEALTHY_AUTO_BROWSE_URL`.","fileCount":null,"zipByteSize":null}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install kn79dhvmpjng4rp2jjk8k0v5xx80ccbk:stealthy-auto-browse","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-stealthy-auto-browse/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-stealthy-auto-browse/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-stealthy-auto-browse/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-stealthy-auto-browse/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-stealthy-auto-browse/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-stealthy-auto-browse/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-09T21:21:09.457Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-stealthy-auto-browse/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-stealthy-auto-browse/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-stealthy-auto-browse/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-stealthy-auto-browse/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":null},"readme":"Skill: stealthy-auto-browse\n\nOwner: psyb0t\n\nSummary: Browser automation that passes CreepJS, BrowserScan, Pixelscan, and Cloudflare — zero CDP exposure, OS-level input, persistent fingerprints. Use when standard browser skills get 403s or CAPTCHAs.\n\nTags: latest:1.3.0\n\nVersion history:\n\nv1.3.0 | 2026-02-10T08:03:05.571Z | user\n\nbetter wording\n\nv1.2.1 | 2026-02-09T21:48:32.930Z | user\n\nRewrite SKILL.md as a proper usage guide instead of a dry API reference \n                                                                          \n  - Every action now explains what it actually does, not just a terse     \n  label                                                                   \n  - Documented response data fields for all actions so agents know what   \n  comes back                                                              \n  - Explained the difference between system_click, mouse_click, and click \n  (three ways to click, different tradeoffs)\n  - Added parameter details (required/optional, defaults, valid values)\n  for every action\n  - Included example response JSON for complex returns\n  (get_interactive_elements, get_network_log, get_cookies, list_tabs,\n  get_last_dialog, get_last_download)\n  - Added \"When to use which\" decision guide for system vs Playwright\n  input\n  - Documented gotchas: handle_dialog timing, calibrate after fullscreen,\n  upload_file still needs form submit, storage is per-origin\n  - Added full workflow section showing the typical interaction sequence\n  - Documented container options, pre-installed extensions, page loaders\n  with match rule logic\n\nv1.2.0 | 2026-02-09T21:31:14.431Z | user\n\nNew actions:\n\n  - Tabs: list_tabs, new_tab, switch_tab, close_tab\n  - Dialogs: handle_dialog, get_last_dialog (alert/confirm/prompt)\n  - Cookies: get_cookies, set_cookie, delete_cookies\n  - Storage: get_storage, set_storage, clear_storage (local + session)\n  - Downloads: get_last_download\n  - Uploads: upload_file (Playwright set_input_files)\n  - Network: enable/disable/get/clear_network_log\n  - Waits: wait_for_element, wait_for_text, wait_for_url,\n  wait_for_network_idle\n  - Proxy: PROXY_URL environment variable for HTTP proxy support\n  - XPath selector support (xpath= prefix) on all element actions\n\nv1.1.0 | 2026-02-09T10:57:25.525Z | user\n\n**stealthy-auto-browse 1.1.0 — Improved documentation, new usage guidance, and enhanced setup instructions**\n\n- Rewrote and reorganized documentation for clarity, including sections for when and when not to use this skill.\n- Added a detailed comparison with standard browser automation to highlight stealth advantages (no CDP exposure, OS-level input).\n- Clarified the setup process and included OpenClaw config and environment variable examples.\n- Expanded API examples with precise JSON payloads and usage tips for undetectable interaction.\n- Added container run examples for custom resolutions, timezones, persistent profiles, and URL auto-start.\n- Revised tips and best practices for maximizing stealth when automating sites with anti-bot protections.\n\nv1.0.1 | 2026-02-07T06:02:08.462Z | user\n\nremove runtime resulution setters\n\nv1.0.0 | 2026-02-02T00:32:50.919Z | user\n\nInitial release of stealthy-auto-browse: control a stealthy browser that evades bot detection using OS-level mouse/keyboard input.\n\n- Control a headless Firefox browser (Camoufox) that avoids common bot detection methods.\n- Supports both \"system\" (OS-level, undetectable) and Playwright (detectable) input modes.\n- Full API for navigation, mouse, keyboard, scrolling, screenshots, and state queries.\n- Includes workflow for undetectable interaction: navigate, discover elements and coordinates, perform mouse/keyboard input at the OS level.\n- Requires running the `stealthy-auto-browse` Docker container and setting `STEALTHY_AUTO_BROWSE_URL`.\n\nArchive index:\n\nArchive v1.3.0: 2 files, 13021 bytes\n\nFiles: SKILL.md (38884b), _meta.json (139b)\n\nFile v1.3.0:SKILL.md\n\n---\nname: stealthy-auto-browse\ndescription: Browser automation that passes CreepJS, BrowserScan, Pixelscan, and Cloudflare — zero CDP exposure, OS-level input, persistent fingerprints. Use when standard browser skills get 403s or CAPTCHAs.\nhomepage: https://github.com/psyb0t/docker-stealthy-auto-browse\nuser-invocable: true\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🕵️\", \"primaryEnv\": \"STEALTHY_AUTO_BROWSE_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# stealthy-auto-browse\n\nA stealth browser running in Docker. It uses Camoufox (a custom Firefox fork) instead of Chromium, so there are zero Chrome DevTools Protocol (CDP) signals for bot detectors to find. Mouse and keyboard input happens at the OS level via PyAutoGUI — the browser itself doesn't know it's being automated, which means behavioral analysis can't detect it either.\n\n## Why This Exists\n\nStandard browser automation (Playwright + Chromium, Puppeteer, Selenium) exposes CDP signals that bot detection services (Cloudflare, DataDome, PerimeterX, Akamai) catch instantly. Even with stealth plugins, the CDP protocol is still there and detectable. This skill eliminates that entirely by using Firefox (no CDP at all) and generating input events at the OS level rather than through the browser's automation API.\n\n## When To Use This Skill\n\n- Site has bot detection (Cloudflare challenge pages, DataDome, PerimeterX, Akamai)\n- Site blocks headless browsers or serves CAPTCHAs\n- You need a logged-in session that doesn't get banned\n- Another browser skill is getting 403s or empty/blocked responses\n- You're scraping a site that actively fights automation\n\n## When NOT To Use This Skill\n\n- Simple fetches with no bot protection — use `curl` or `WebFetch`\n- Sites that don't care about automation — use a regular browser skill, it's faster to set up\n- You only need static HTML — use `curl`\n\n## Setup\n\n**1. Start the container:**\n\n```bash\ndocker run -d -p 8080:8080 -p 5900:5900 psyb0t/stealthy-auto-browse\n```\n\nPort 8080 is the HTTP API. Port 5900 is a noVNC web viewer where you can watch the browser in real time.\n\n**2. Set the environment variable:**\n\n```bash\nexport STEALTHY_AUTO_BROWSE_URL=http://localhost:8080\n```\n\nOr via OpenClaw config (`~/.openclaw/openclaw.json`):\n\n```json\n{\n  \"skills\": {\n    \"entries\": {\n      \"stealthy-auto-browse\": {\n        \"env\": {\n          \"STEALTHY_AUTO_BROWSE_URL\": \"http://localhost:8080\"\n        }\n      }\n    }\n  }\n}\n```\n\n**3. Verify:** `curl $STEALTHY_AUTO_BROWSE_URL/health` returns `ok` when the browser is ready.\n\n## How It Works\n\nThe container runs a virtual X display (Xvfb at 1920x1080), the Camoufox browser, and an HTTP API server. You send JSON commands to the API and get JSON responses back. All commands go to `POST $STEALTHY_AUTO_BROWSE_URL/` with `{\"action\": \"<name>\", ...params}`.\n\nEvery response has this shape:\n\n```json\n{\n  \"success\": true,\n  \"timestamp\": 1234567890.123,\n  \"data\": { ... },\n  \"error\": \"only present when success is false\"\n}\n```\n\nThe `data` field contents vary by action — documented below for each one.\n\n## Understanding the Two Input Modes\n\nThis is the most important concept. There are two ways to interact with pages:\n\n### System Input (Undetectable)\n\nActions: `system_click`, `mouse_move`, `mouse_click`, `system_type`, `send_key`, `scroll`\n\nThese use PyAutoGUI to generate real OS-level mouse movements and keystrokes. The browser receives these as genuine user input — there is no way for any website JavaScript to distinguish these from a real human. **Use these for stealth.**\n\nSystem input works with **viewport coordinates** (x, y pixel positions within the browser content area). Get these coordinates from `get_interactive_elements`.\n\n### Playwright Input (Detectable)\n\nActions: `click`, `fill`, `type`\n\nThese use Playwright's DOM automation to interact with elements by CSS selector or XPath. They're faster and more reliable (no coordinate math), but they inject events through the browser's automation layer. Sophisticated behavioral analysis can potentially detect the timing patterns. **Use these when speed matters more than stealth, or when you have a selector but no coordinates.**\n\n### When to Use Which\n\n- **Stealth-critical sites** (Cloudflare, login forms, anything with bot detection): Always use system input.\n- **Simple scraping** where the site isn't actively fighting you: Playwright input is fine and easier.\n- **Form filling**: Use `system_click` to focus the field, then `system_type` to enter text. This is undetectable. Using `fill` is faster but detectable.\n- **Clicking buttons**: If you have coordinates from `get_interactive_elements`, use `system_click`. If you only have a CSS selector, use `click`.\n\n## Workflow\n\nThis is the typical sequence for interacting with a page:\n\n1. **Navigate**: `goto` to load the URL\n2. **Read the page**: `get_text` returns all visible text — usually enough to understand the page\n3. **If text isn't clear**: `get_html` gives you the full DOM structure\n4. **If still confused**: Take a screenshot (`GET /screenshot/browser?whLargest=512`)\n5. **Find interactive elements**: `get_interactive_elements` returns all buttons, links, inputs with their x,y coordinates\n6. **Interact**: `system_click` to click, `system_type` to type, `send_key` for Enter/Tab/Escape\n7. **Wait for results**: `wait_for_element` or `wait_for_text` instead of sleeping\n8. **Verify**: `get_text` again to confirm the page changed as expected\n\n## Actions Reference\n\n### Navigation\n\n#### goto\n\nNavigates to a URL. This is how you load pages.\n\n```json\n{\"action\": \"goto\", \"url\": \"https://example.com\"}\n{\"action\": \"goto\", \"url\": \"https://example.com\", \"wait_until\": \"networkidle\"}\n```\n\n**Parameters:**\n- `url` (required): The URL to navigate to.\n- `wait_until` (optional, default `\"domcontentloaded\"`): When to consider the page loaded. Options: `\"domcontentloaded\"` (DOM parsed, fast), `\"load\"` (all resources loaded), `\"networkidle\"` (no network activity for 500ms, slowest but most complete).\n\n**Response data:** `{\"url\": \"https://example.com/\", \"title\": \"Example Domain\"}`\n\n**Note:** If a page loader matches the URL (see Page Loaders section), the loader's steps execute instead of the default navigation. The response will include `\"loader\": \"loader name\"` when this happens.\n\n#### refresh\n\nReloads the current page.\n\n```json\n{\"action\": \"refresh\"}\n{\"action\": \"refresh\", \"wait_until\": \"networkidle\"}\n```\n\n**Parameters:**\n- `wait_until` (optional, default `\"domcontentloaded\"`): Same options as `goto`.\n\n**Response data:** `{\"url\": \"https://example.com/current-page\", \"title\": \"Current Page\"}`\n\n### System Input (Undetectable)\n\n#### system_click\n\nMoves the mouse to viewport coordinates with a human-like curve (random jitter, eased acceleration), then clicks. This is the primary way to click things stealthily.\n\n```json\n{\"action\": \"system_click\", \"x\": 500, \"y\": 300}\n{\"action\": \"system_click\", \"x\": 500, \"y\": 300, \"duration\": 0.5}\n```\n\n**Parameters:**\n- `x`, `y` (required): Viewport coordinates — get these from `get_interactive_elements`.\n- `duration` (optional): How long the mouse movement takes in seconds. If omitted, a random duration between 0.2-0.6s is used for realism.\n\n**Response data:** `{\"system_clicked\": {\"x\": 500, \"y\": 300}}`\n\n**How it differs from `mouse_click`:** `system_click` always moves the mouse first (smooth human-like path), then clicks. `mouse_click` can click at a position instantly without the smooth movement, or click wherever the mouse currently is.\n\n#### mouse_move\n\nMoves the mouse to viewport coordinates with human-like movement (jitter, eased curve) but does NOT click. Use this to hover over elements (to trigger hover menus, tooltips) or to simulate natural mouse behavior between actions.\n\n```json\n{\"action\": \"mouse_move\", \"x\": 500, \"y\": 300}\n{\"action\": \"mouse_move\", \"x\": 500, \"y\": 300, \"duration\": 0.4}\n```\n\n**Parameters:**\n- `x`, `y` (required): Viewport coordinates.\n- `duration` (optional): Movement time in seconds. Random 0.2-0.6s if omitted.\n\n**Response data:** `{\"moved_to\": {\"x\": 500, \"y\": 300}}`\n\n#### mouse_click\n\nClicks at a position or at the current mouse location. Unlike `system_click`, this does NOT do a smooth mouse movement first — it's a direct click via PyAutoGUI.\n\n```json\n{\"action\": \"mouse_click\"}\n{\"action\": \"mouse_click\", \"x\": 500, \"y\": 300}\n```\n\n**Parameters:**\n- `x`, `y` (optional): If provided, clicks at that viewport position directly. If omitted, clicks wherever the mouse currently is.\n\n**Response data:** `{\"clicked_at\": {\"x\": 500, \"y\": 300}}` or `{\"clicked_at\": \"current\"}`\n\n**When to use:** After a `mouse_move` when you want to separate the movement and click into two steps. Or when the mouse is already positioned and you just need to click.\n\n#### system_type\n\nTypes text character-by-character via real OS keystrokes. Each keystroke has a randomized delay (jittered around the interval) to mimic human typing speed. Completely undetectable.\n\n```json\n{\"action\": \"system_type\", \"text\": \"hello world\"}\n{\"action\": \"system_type\", \"text\": \"hello world\", \"interval\": 0.12}\n```\n\n**Parameters:**\n- `text` (required): The text to type. Must click/focus an input field first.\n- `interval` (optional, default `0.08`): Base delay between keystrokes in seconds. Actual delay is randomized +-30ms around this value.\n\n**Response data:** `{\"typed_len\": 11}`\n\n**Important:** You must click on the input field first (using `system_click` or `click`) before calling `system_type`. This action types into whatever is currently focused.\n\n#### send_key\n\nSends a single keyboard key or key combination via OS-level input. Use this for pressing Enter to submit forms, Tab to move between fields, Escape to close dialogs, or any key combos like Ctrl+A, Ctrl+C, etc.\n\n```json\n{\"action\": \"send_key\", \"key\": \"enter\"}\n{\"action\": \"send_key\", \"key\": \"tab\"}\n{\"action\": \"send_key\", \"key\": \"escape\"}\n{\"action\": \"send_key\", \"key\": \"ctrl+a\"}\n{\"action\": \"send_key\", \"key\": \"ctrl+shift+t\"}\n```\n\n**Parameters:**\n- `key` (required): Key name or combo with `+` separator. Key names follow PyAutoGUI naming: `enter`, `tab`, `escape`, `backspace`, `delete`, `up`, `down`, `left`, `right`, `home`, `end`, `pageup`, `pagedown`, `f1`-`f12`, `ctrl`, `alt`, `shift`, `space`, etc.\n\n**Response data:** `{\"send_key\": \"enter\"}`\n\n#### scroll\n\nScrolls the page using the mouse scroll wheel. Generates real OS-level scroll events.\n\n```json\n{\"action\": \"scroll\", \"amount\": -3}\n{\"action\": \"scroll\", \"amount\": 5, \"x\": 500, \"y\": 300}\n```\n\n**Parameters:**\n- `amount` (optional, default `-3`): Scroll amount. **Negative = scroll down**, positive = scroll up. Each unit is roughly one \"click\" of a mouse wheel.\n- `x`, `y` (optional): If provided, moves the mouse to these viewport coordinates first, then scrolls. Useful for scrolling inside a specific scrollable element rather than the whole page.\n\n**Response data:** `{\"scrolled\": -3}`\n\n### Playwright Input (Detectable)\n\nThese are faster and more convenient but use Playwright's DOM event injection, which is detectable by sophisticated behavioral analysis.\n\n#### click\n\nClicks an element by CSS selector or XPath. Playwright finds the element in the DOM, scrolls it into view if needed, and dispatches click events.\n\n```json\n{\"action\": \"click\", \"selector\": \"#submit-btn\"}\n{\"action\": \"click\", \"selector\": \"button.primary\"}\n{\"action\": \"click\", \"selector\": \"xpath=//button[@id='submit-btn']\"}\n```\n\n**Parameters:**\n- `selector` (required): CSS selector or XPath (prefix with `xpath=`).\n\n**Response data:** `{\"clicked\": \"#submit-btn\"}`\n\n**When to use over system_click:** When you have a selector but don't want to bother getting coordinates. When the element might move around and coordinates aren't reliable. When stealth isn't critical.\n\n#### fill\n\nFills an input field by selector. Clears any existing content first, then sets the value. This is the fastest way to fill forms but is detectable because it doesn't generate individual keystroke events.\n\n```json\n{\"action\": \"fill\", \"selector\": \"input[name='email']\", \"value\": \"user@example.com\"}\n```\n\n**Parameters:**\n- `selector` (required): CSS selector or XPath of the input element.\n- `value` (required): Text to fill in.\n\n**Response data:** `{\"filled\": \"input[name='email']\"}`\n\n#### type\n\nTypes text into an element character-by-character via Playwright (NOT the OS). Each keystroke has a configurable delay. This is a middle ground between `fill` (instant but obviously automated) and `system_type` (OS-level, undetectable). The typing pattern is more realistic than `fill` but still comes through Playwright's event system.\n\n```json\n{\"action\": \"type\", \"selector\": \"#search\", \"text\": \"query\", \"delay\": 0.05}\n```\n\n**Parameters:**\n- `selector` (required): CSS selector or XPath of the element.\n- `text` (required): Text to type.\n- `delay` (optional, default `0.05`): Delay between keystrokes in seconds.\n\n**Response data:** `{\"typed\": \"#search\"}`\n\n### Screenshots\n\nScreenshots are GET requests (not POST actions).\n\n#### GET /screenshot/browser\n\nCaptures the browser viewport as a PNG image. This is what the page looks like to a user.\n\n```bash\ncurl -s \"$STEALTHY_AUTO_BROWSE_URL/screenshot/browser?whLargest=512\" -o screenshot.png\n```\n\n**Always resize screenshots** to avoid huge images. Resize query parameters (all optional):\n\n| Parameter | What it does |\n|-----------|-------------|\n| `whLargest=512` | Scales so the largest dimension is 512px, keeps aspect ratio. **Use this by default.** |\n| `width=800` | Scales to 800px wide, keeps aspect ratio |\n| `height=300` | Scales to 300px tall, keeps aspect ratio |\n| `width=400&height=400` | Forces exact 400x400 dimensions |\n\n#### GET /screenshot/desktop\n\nCaptures the entire virtual desktop (including window chrome, taskbar, etc.) using `scrot`. Same resize parameters as above. Useful when you need to see things outside the browser viewport.\n\n```bash\ncurl -s \"$STEALTHY_AUTO_BROWSE_URL/screenshot/desktop?whLargest=512\" -o desktop.png\n```\n\n### Page Inspection\n\n#### get_interactive_elements\n\nScans the page and returns every interactive element (buttons, links, inputs, selects, textareas, etc.) with their viewport coordinates. This is how you find what to click and where.\n\n```json\n{\"action\": \"get_interactive_elements\"}\n{\"action\": \"get_interactive_elements\", \"visible_only\": true}\n```\n\n**Parameters:**\n- `visible_only` (optional, default `true`): Only return elements that are currently visible on screen.\n\n**Response data:**\n```json\n{\n  \"count\": 5,\n  \"elements\": [\n    {\n      \"i\": 0,\n      \"tag\": \"button\",\n      \"id\": \"submit-btn\",\n      \"text\": \"Submit\",\n      \"selector\": \"#submit-btn\",\n      \"x\": 400,\n      \"y\": 250,\n      \"w\": 120,\n      \"h\": 40,\n      \"visible\": true\n    },\n    {\n      \"i\": 1,\n      \"tag\": \"input\",\n      \"id\": null,\n      \"text\": \"\",\n      \"selector\": \"input[name='email']\",\n      \"x\": 300,\n      \"y\": 180,\n      \"w\": 250,\n      \"h\": 35,\n      \"visible\": true\n    }\n  ]\n}\n```\n\nThe `x`, `y` are the center of the element — pass these directly to `system_click`. The `selector` can be used with Playwright actions like `click` or `fill`. The `w`, `h` give you the element dimensions.\n\n**This is your primary tool for understanding what you can interact with on a page.** Call this before clicking anything.\n\n#### get_text\n\nReturns all visible text content of the page body. Text is truncated to 10,000 characters.\n\n```json\n{\"action\": \"get_text\"}\n```\n\n**Response data:** `{\"text\": \"Page title\\nSome content here...\", \"length\": 1234}`\n\nThis is usually the first thing to call after navigating — it tells you what's on the page without needing a screenshot.\n\n#### get_html\n\nReturns the full HTML source of the current page.\n\n```json\n{\"action\": \"get_html\"}\n```\n\n**Response data:** `{\"html\": \"<!DOCTYPE html>...\", \"length\": 45678}`\n\nUse when `get_text` doesn't give enough structure to understand the page layout, or when you need to find specific elements in the DOM.\n\n#### eval\n\nExecutes arbitrary JavaScript in the page context and returns the result. The expression is evaluated via `page.evaluate()`.\n\n```json\n{\"action\": \"eval\", \"expression\": \"document.title\"}\n{\"action\": \"eval\", \"expression\": \"document.querySelectorAll('a').length\"}\n{\"action\": \"eval\", \"expression\": \"JSON.stringify(performance.timing)\"}\n```\n\n**Parameters:**\n- `expression` (required): JavaScript expression to evaluate. Must return a JSON-serializable value.\n\n**Response data:** `{\"result\": \"Example Domain\"}` — the result is whatever the expression returns.\n\n### Wait Conditions\n\nUse these instead of `sleep` to wait for page content. They're more reliable because they wait for the exact condition rather than an arbitrary time.\n\n#### wait_for_element\n\nWaits for an element matching a CSS selector or XPath to reach a certain state (visible, hidden, attached to DOM, detached).\n\n```json\n{\"action\": \"wait_for_element\", \"selector\": \"#results\", \"timeout\": 10}\n{\"action\": \"wait_for_element\", \"selector\": \"xpath=//div[@class='loaded']\", \"timeout\": 15}\n{\"action\": \"wait_for_element\", \"selector\": \".spinner\", \"state\": \"hidden\", \"timeout\": 10}\n```\n\n**Parameters:**\n- `selector` (required): CSS selector or XPath (prefix with `xpath=`).\n- `state` (optional, default `\"visible\"`): What state to wait for. Options: `\"visible\"` (rendered and not hidden), `\"hidden\"` (not visible), `\"attached\"` (in DOM regardless of visibility), `\"detached\"` (removed from DOM).\n- `timeout` (optional, default `30`): Max wait time in seconds. Throws error if exceeded.\n\n**Response data:** `{\"selector\": \"#results\", \"state\": \"visible\"}`\n\n#### wait_for_text\n\nWaits for specific text to appear anywhere in the page body.\n\n```json\n{\"action\": \"wait_for_text\", \"text\": \"Search results\", \"timeout\": 10}\n```\n\n**Parameters:**\n- `text` (required): Exact text to look for (substring match on `document.body.innerText`).\n- `timeout` (optional, default `30`): Max wait time in seconds.\n\n**Response data:** `{\"text\": \"Search results\", \"found\": true}`\n\n#### wait_for_url\n\nWaits for the page URL to match a pattern. Useful after form submissions or redirects.\n\n```json\n{\"action\": \"wait_for_url\", \"url\": \"**/dashboard\", \"timeout\": 10}\n{\"action\": \"wait_for_url\", \"url\": \"https://example.com/success*\", \"timeout\": 15}\n```\n\n**Parameters:**\n- `url` (required): URL pattern to match. Supports `*` (any chars except `/`) and `**` (any chars including `/`) glob patterns. Can also be a full URL for exact match.\n- `timeout` (optional, default `30`): Max wait time in seconds.\n\n**Response data:** `{\"url\": \"https://example.com/dashboard\"}`\n\n#### wait_for_network_idle\n\nWaits until there are no network requests in flight for 500ms. Useful for pages that load content dynamically after the initial page load.\n\n```json\n{\"action\": \"wait_for_network_idle\", \"timeout\": 30}\n```\n\n**Parameters:**\n- `timeout` (optional, default `30`): Max wait time in seconds.\n\n**Response data:** `{\"idle\": true}`\n\n### Tab Management\n\nThe browser can have multiple tabs open. One tab is \"active\" at a time — all actions operate on the active tab.\n\n#### list_tabs\n\nReturns all open tabs with their URLs and which one is active.\n\n```json\n{\"action\": \"list_tabs\"}\n```\n\n**Response data:**\n```json\n{\n  \"count\": 2,\n  \"tabs\": [\n    {\"index\": 0, \"url\": \"https://example.com/\", \"active\": false},\n    {\"index\": 1, \"url\": \"https://other.com/\", \"active\": true}\n  ]\n}\n```\n\n#### new_tab\n\nOpens a new browser tab. Optionally navigates it to a URL. The new tab becomes the active tab.\n\n```json\n{\"action\": \"new_tab\"}\n{\"action\": \"new_tab\", \"url\": \"https://example.com\"}\n```\n\n**Parameters:**\n- `url` (optional): URL to navigate to in the new tab.\n- `wait_until` (optional, default `\"domcontentloaded\"`): Same as `goto`.\n\n**Response data:** `{\"index\": 1, \"url\": \"https://example.com/\"}`\n\n#### switch_tab\n\nSwitches the active tab by index (0-based). All subsequent actions will operate on this tab.\n\n```json\n{\"action\": \"switch_tab\", \"index\": 0}\n```\n\n**Parameters:**\n- `index` (required): Tab index from `list_tabs`.\n\n**Response data:** `{\"index\": 0, \"url\": \"https://example.com/\"}`\n\n#### close_tab\n\nCloses a tab. After closing, the last remaining tab becomes active.\n\n```json\n{\"action\": \"close_tab\"}\n{\"action\": \"close_tab\", \"index\": 1}\n```\n\n**Parameters:**\n- `index` (optional): Tab index to close. If omitted, closes the currently active tab.\n\n**Response data:** `{\"closed\": true, \"remaining\": 1}`\n\n### Dialog Handling\n\nBrowsers have modal dialogs (alert, confirm, prompt). By default, dialogs are auto-accepted (clicks OK). Use `handle_dialog` if you need to dismiss a dialog or provide text for a prompt.\n\n#### handle_dialog\n\n**Call BEFORE the action that triggers the dialog** if you want to dismiss it or provide prompt text. If you don't call this, the dialog is auto-accepted (clicks OK).\n\n```json\n{\"action\": \"handle_dialog\", \"accept\": true}\n{\"action\": \"handle_dialog\", \"accept\": false}\n{\"action\": \"handle_dialog\", \"accept\": true, \"text\": \"my response\"}\n```\n\n**Parameters:**\n- `accept` (optional, default `true`): `true` clicks OK/Accept, `false` clicks Cancel/Dismiss.\n- `text` (optional): Response text for prompt dialogs. Ignored for alert/confirm.\n\n**Response data:** `{\"configured\": {\"accept\": true, \"text\": null}}`\n\n**Example — handling a confirm dialog:**\n```bash\n# Step 1: Tell the browser to accept the next dialog\ncurl -X POST $API -H 'Content-Type: application/json' -d '{\"action\": \"handle_dialog\", \"accept\": true}'\n# Step 2: Now click the button that triggers the confirm\ncurl -X POST $API -H 'Content-Type: application/json' -d '{\"action\": \"system_click\", \"x\": 300, \"y\": 200}'\n```\n\n#### get_last_dialog\n\nReturns information about the most recent dialog that appeared.\n\n```json\n{\"action\": \"get_last_dialog\"}\n```\n\n**Response data:**\n```json\n{\n  \"dialog\": {\n    \"type\": \"confirm\",\n    \"message\": \"Are you sure you want to delete this?\",\n    \"default_value\": \"\",\n    \"buttons\": [\"ok\", \"cancel\"]\n  }\n}\n```\n\nReturns `{\"dialog\": null}` if no dialog has appeared yet. The `type` field is one of: `\"alert\"`, `\"confirm\"`, `\"prompt\"`, `\"beforeunload\"`.\n\n### Cookies\n\n#### get_cookies\n\nReturns all cookies for the browser context, or cookies for specific URLs.\n\n```json\n{\"action\": \"get_cookies\"}\n{\"action\": \"get_cookies\", \"urls\": [\"https://example.com\"]}\n```\n\n**Parameters:**\n- `urls` (optional): Array of URLs to filter cookies by. If omitted, returns all cookies.\n\n**Response data:**\n```json\n{\n  \"count\": 3,\n  \"cookies\": [\n    {\"name\": \"session\", \"value\": \"abc123\", \"domain\": \".example.com\", \"path\": \"/\", \"httpOnly\": true, \"secure\": true, ...}\n  ]\n}\n```\n\n#### set_cookie\n\nSets a cookie in the browser context.\n\n```json\n{\"action\": \"set_cookie\", \"name\": \"session\", \"value\": \"abc123\", \"url\": \"https://example.com\"}\n{\"action\": \"set_cookie\", \"name\": \"pref\", \"value\": \"dark\", \"domain\": \".example.com\", \"path\": \"/\", \"httpOnly\": false, \"secure\": true}\n```\n\n**Parameters:** Any standard cookie fields — `name`, `value`, `url`, `domain`, `path`, `httpOnly`, `secure`, `sameSite`, `expires`. At minimum you need `name`, `value`, and either `url` or `domain`.\n\n**Response data:** `{\"set\": \"session\"}`\n\n#### delete_cookies\n\nClears all cookies from the browser context.\n\n```json\n{\"action\": \"delete_cookies\"}\n```\n\n**Response data:** `{\"cleared\": true}`\n\n### Storage\n\nAccess the page's localStorage and sessionStorage. These are per-origin — you must be on the right page for the storage to be accessible.\n\n#### get_storage\n\nReturns all items from localStorage or sessionStorage as a key-value object.\n\n```json\n{\"action\": \"get_storage\", \"type\": \"local\"}\n{\"action\": \"get_storage\", \"type\": \"session\"}\n```\n\n**Parameters:**\n- `type` (optional, default `\"local\"`): `\"local\"` for localStorage, `\"session\"` for sessionStorage.\n\n**Response data:** `{\"items\": {\"theme\": \"dark\", \"lang\": \"en\"}, \"type\": \"local\"}`\n\n#### set_storage\n\nSets a single key-value pair in localStorage or sessionStorage.\n\n```json\n{\"action\": \"set_storage\", \"type\": \"local\", \"key\": \"theme\", \"value\": \"dark\"}\n```\n\n**Parameters:**\n- `type` (optional, default `\"local\"`): `\"local\"` or `\"session\"`.\n- `key` (required): Storage key.\n- `value` (required): Storage value (string).\n\n**Response data:** `{\"set\": \"theme\", \"type\": \"local\"}`\n\n#### clear_storage\n\nClears all items from localStorage or sessionStorage.\n\n```json\n{\"action\": \"clear_storage\", \"type\": \"local\"}\n{\"action\": \"clear_storage\", \"type\": \"session\"}\n```\n\n**Response data:** `{\"cleared\": \"local\"}`\n\n### Downloads\n\nThe browser automatically tracks file downloads triggered by page interactions (clicking download links, form submissions that return files, etc.).\n\n#### get_last_download\n\nReturns information about the most recently downloaded file.\n\n```json\n{\"action\": \"get_last_download\"}\n```\n\n**Response data:**\n```json\n{\n  \"download\": {\n    \"url\": \"https://example.com/file.pdf\",\n    \"filename\": \"file.pdf\",\n    \"path\": \"/tmp/playwright-downloads/abc123/file.pdf\"\n  }\n}\n```\n\nReturns `{\"download\": null}` if nothing has been downloaded yet. The `path` is the local path inside the container where the file was saved. The `filename` is what the server suggested as the download name.\n\n### Uploads\n\n#### upload_file\n\nProgrammatically sets a file on an `<input type=\"file\">` element without opening the OS file picker. The file must exist inside the container — use `docker cp` to copy files in if needed.\n\n```json\n{\"action\": \"upload_file\", \"selector\": \"#file-input\", \"file_path\": \"/tmp/document.pdf\"}\n```\n\n**Parameters:**\n- `selector` (required): CSS selector of the file input element.\n- `file_path` (required): Absolute path to the file inside the container.\n\n**Response data:** `{\"selector\": \"#file-input\", \"file\": \"document.pdf\", \"size\": 12345}`\n\n**Note:** After setting the file, you still need to submit the form (click the submit button) for the upload to actually happen.\n\n### Network Logging\n\nCapture all HTTP requests and responses the page makes. Useful for debugging, finding API endpoints the page calls, or verifying that certain resources loaded.\n\n#### enable_network_log\n\nStarts recording all HTTP requests and responses from the active page.\n\n```json\n{\"action\": \"enable_network_log\"}\n```\n\n**Response data:** `{\"enabled\": true}`\n\n#### disable_network_log\n\nStops recording network activity. Already-captured entries remain.\n\n```json\n{\"action\": \"disable_network_log\"}\n```\n\n**Response data:** `{\"enabled\": false}`\n\n#### get_network_log\n\nReturns all captured network entries since logging was enabled (or last cleared).\n\n```json\n{\"action\": \"get_network_log\"}\n```\n\n**Response data:**\n```json\n{\n  \"count\": 4,\n  \"log\": [\n    {\"type\": \"request\", \"url\": \"https://api.example.com/data\", \"method\": \"GET\", \"resource_type\": \"fetch\", \"timestamp\": 1234567890.123},\n    {\"type\": \"response\", \"url\": \"https://api.example.com/data\", \"status\": 200, \"timestamp\": 1234567890.456},\n    {\"type\": \"request\", \"url\": \"https://cdn.example.com/style.css\", \"method\": \"GET\", \"resource_type\": \"stylesheet\", \"timestamp\": 1234567890.789},\n    {\"type\": \"response\", \"url\": \"https://cdn.example.com/style.css\", \"status\": 200, \"timestamp\": 1234567890.999}\n  ]\n}\n```\n\nEach entry is either a `\"request\"` or `\"response\"`. Requests include `method` and `resource_type` (fetch, document, stylesheet, script, image, etc.). Responses include `status` code.\n\n#### clear_network_log\n\nDeletes all captured network entries but keeps logging enabled if it was on.\n\n```json\n{\"action\": \"clear_network_log\"}\n```\n\n**Response data:** `{\"cleared\": true}`\n\n### Scrolling\n\n#### scroll_to_bottom\n\nScrolls the entire page from top to bottom using JavaScript `window.scrollBy()`. Scrolls one viewport height at a time with a fixed delay between scrolls. When it reaches the bottom (scroll position stops changing), it scrolls back to the top. Useful for triggering lazy-loaded content.\n\n```json\n{\"action\": \"scroll_to_bottom\"}\n{\"action\": \"scroll_to_bottom\", \"delay\": 0.6}\n```\n\n**Parameters:**\n- `delay` (optional, default `0.4`): Seconds to wait between each scroll step.\n\n**Response data:** `{\"scrolled\": \"bottom\"}`\n\n#### scroll_to_bottom_humanized\n\nSame as `scroll_to_bottom` but uses real OS-level mouse wheel scrolling (via PyAutoGUI) with randomized scroll amounts and jittered delays to look like a human scrolling. Undetectable by behavioral analysis.\n\n```json\n{\"action\": \"scroll_to_bottom_humanized\"}\n{\"action\": \"scroll_to_bottom_humanized\", \"min_clicks\": 3, \"max_clicks\": 8, \"delay\": 0.7}\n```\n\n**Parameters:**\n- `min_clicks` (optional, default `2`): Minimum mouse wheel clicks per scroll step.\n- `max_clicks` (optional, default `6`): Maximum mouse wheel clicks per scroll step. A random value between min and max is chosen each time.\n- `delay` (optional, default `0.5`): Base delay between scroll steps. Actual delay is jittered +-30%.\n\n**Response data:** `{\"scrolled\": \"bottom_humanized\"}`\n\n### Display\n\n#### calibrate\n\nRecalculates the mapping between viewport coordinates (what `get_interactive_elements` returns) and screen coordinates (what PyAutoGUI uses). The browser has window chrome (title bar, address bar) that offsets the viewport from the screen origin.\n\n```json\n{\"action\": \"calibrate\"}\n```\n\n**Response data:** `{\"window_offset\": {\"x\": 0, \"y\": 74}}`\n\n**When to call this:** After entering/exiting fullscreen, after the browser window is resized, or if `system_click` coordinates seem off. The offset is auto-calculated at startup, so you rarely need this.\n\n#### get_resolution\n\nReturns the virtual display resolution (from the XVFB_RESOLUTION environment variable).\n\n```json\n{\"action\": \"get_resolution\"}\n```\n\n**Response data:** `{\"width\": 1920, \"height\": 1080}`\n\n#### enter_fullscreen / exit_fullscreen\n\nToggles browser fullscreen mode (hides address bar and window chrome). In fullscreen, the viewport takes up the entire screen, so coordinates map differently.\n\n```json\n{\"action\": \"enter_fullscreen\"}\n{\"action\": \"exit_fullscreen\"}\n```\n\n**Response data:** `{\"fullscreen\": true, \"changed\": true}` — `changed` is `false` if already in the requested state.\n\n**Important:** Call `calibrate` after entering/exiting fullscreen to update the coordinate mapping.\n\n### Utility\n\n#### ping\n\nHealth check that returns the current page URL. Use to verify the API is responding and the browser is alive.\n\n```json\n{\"action\": \"ping\"}\n```\n\n**Response data:** `{\"message\": \"pong\", \"url\": \"https://example.com/\"}`\n\n#### sleep\n\nPauses execution for a specified duration. Prefer `wait_for_element` or `wait_for_text` when waiting for page content — use `sleep` only for fixed timing needs.\n\n```json\n{\"action\": \"sleep\", \"duration\": 2}\n```\n\n**Parameters:**\n- `duration` (optional, default `1`): Seconds to sleep.\n\n**Response data:** `{\"slept\": 2}`\n\n#### close\n\nShuts down the browser. The container will stop after this.\n\n```json\n{\"action\": \"close\"}\n```\n\n**Response data:** `{\"message\": \"closing\"}`\n\n### State Endpoints (GET)\n\n#### GET /state\n\nReturns the current browser state.\n\n```bash\ncurl -s \"$STEALTHY_AUTO_BROWSE_URL/state\"\n```\n\n**Response:**\n```json\n{\n  \"status\": \"ready\",\n  \"url\": \"https://example.com/\",\n  \"title\": \"Example Domain\",\n  \"window_offset\": {\"x\": 0, \"y\": 74}\n}\n```\n\n#### GET /health\n\nSimple health check. Returns `ok` as plain text when the API is ready.\n\n```bash\ncurl -s \"$STEALTHY_AUTO_BROWSE_URL/health\"\n```\n\n## Container Options\n\n```bash\n# Custom display resolution\ndocker run -d -p 8080:8080 -e XVFB_RESOLUTION=1280x720 psyb0t/stealthy-auto-browse\n\n# Match timezone to your IP's geographic location (important for stealth — mismatched\n# timezone is a common bot detection signal)\ndocker run -d -p 8080:8080 -e TZ=Europe/Bucharest psyb0t/stealthy-auto-browse\n\n# Route browser traffic through an HTTP proxy\ndocker run -d -p 8080:8080 -e PROXY_URL=http://user:pass@proxy:8888 psyb0t/stealthy-auto-browse\n\n# Persistent browser profile — cookies, sessions, and fingerprint survive container restarts\ndocker run -d -p 8080:8080 -v ./profile:/userdata psyb0t/stealthy-auto-browse\n\n# Open a URL automatically on startup\ndocker run -d -p 8080:8080 psyb0t/stealthy-auto-browse https://example.com\n```\n\n## Page Loaders (URL-Triggered Automation)\n\nPage loaders are like **Greasemonkey/Tampermonkey userscripts** but for the HTTP API. You define a set of actions that automatically run whenever the browser navigates to a matching URL. Instead of manually sending a sequence of commands every time you visit a site, you write it once as a YAML file and the container handles it.\n\nThis is useful for things like: removing cookie popups, dismissing overlays, waiting for dynamic content, cleaning up pages before scraping, or any repetitive setup you'd otherwise do manually every time.\n\n### How They Work\n\n1. You create YAML files that define URL patterns and a list of steps\n2. Mount those files into the container at `/loaders`\n3. Whenever `goto` navigates to a URL that matches a loader's pattern, the loader's steps run automatically instead of the default navigation\n\n**The steps are the exact same actions as the HTTP API.** Every action you can send via `POST /` (goto, eval, click, system_click, sleep, scroll, wait_for_element, etc.) works as a loader step. Same names, same parameters.\n\n### Setup\n\n```bash\ndocker run -d -p 8080:8080 -p 5900:5900 \\\n  -v ./my-loaders:/loaders \\\n  psyb0t/stealthy-auto-browse\n```\n\n### Loader Format\n\n```yaml\nname: Human-readable name for this loader\nmatch:\n  domain: example.com         # Exact hostname match (www. is stripped automatically)\n  path_prefix: /articles      # URL path must start with this\n  regex: \"article/\\\\d+\"       # Full URL must match this regex\nsteps:\n  - action: goto              # Same actions as the HTTP API\n    url: \"${url}\"             # ${url} is replaced with the original URL\n    wait_until: networkidle\n  - action: eval\n    expression: \"document.querySelector('.cookie-banner')?.remove()\"\n  - action: wait_for_element\n    selector: \"#main-content\"\n    timeout: 10\n```\n\n### Match Rules\n\nAll match fields are **optional**, but at least one is required. If you specify multiple fields, **all** of them must match for the loader to trigger:\n\n- **`domain`**: Exact hostname. `www.` is stripped from both sides before comparing, so `domain: example.com` matches `www.example.com` too.\n- **`path_prefix`**: The URL path must start with this string. `path_prefix: /blog` matches `/blog`, `/blog/post-1`, `/blog/archive`, etc.\n- **`regex`**: The full URL is tested against this regular expression.\n\n### The `${url}` Placeholder\n\nIn any string value within a step, `${url}` is replaced with the original URL that was passed to `goto`. This lets you navigate to the URL with custom wait settings, or pass it to JavaScript:\n\n```yaml\nsteps:\n  - action: goto\n    url: \"${url}\"\n    wait_until: networkidle\n  - action: eval\n    expression: \"console.log('Loaded:', '${url}')\"\n```\n\n### Practical Example: Clean Scraping\n\nSay you're scraping a news site that has cookie popups, newsletter modals, and lazy-loaded content. Without a loader, you'd send 5+ commands after every `goto`. With a loader:\n\n```yaml\n# loaders/news_site.yaml\nname: News Site Cleanup\nmatch:\n  domain: news-site.com\nsteps:\n  # Navigate with full network wait so everything loads\n  - action: goto\n    url: \"${url}\"\n    wait_until: networkidle\n\n  # Wait for the main content to be there\n  - action: wait_for_element\n    selector: \"article\"\n    timeout: 10\n\n  # Kill the cookie popup\n  - action: eval\n    expression: \"document.querySelector('.cookie-consent')?.remove()\"\n\n  # Kill the newsletter modal\n  - action: eval\n    expression: \"document.querySelector('.newsletter-overlay')?.remove()\"\n\n  # Scroll to trigger lazy-loaded images\n  - action: scroll_to_bottom\n    delay: 0.3\n\n  # Small pause for everything to settle\n  - action: sleep\n    duration: 1\n```\n\nNow when you `goto` any URL on `news-site.com`, all of this happens automatically. Your response includes `\"loader\": \"News Site Cleanup\"` so you know it triggered.\n\n### Response When a Loader Triggers\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"loader\": \"News Site Cleanup\",\n    \"steps_executed\": 6,\n    \"last_result\": { \"success\": true, \"timestamp\": 1234567890.456, \"data\": { \"slept\": 1 } }\n  }\n}\n```\n\n## Pre-installed Extensions\n\nThe browser comes with these extensions pre-installed:\n\n- **uBlock Origin**: Ad and tracker blocking\n- **LocalCDN**: Serves common CDN resources locally to prevent tracking\n- **ClearURLs**: Strips tracking parameters from URLs\n- **Consent-O-Matic**: Automatically handles cookie consent popups (clicks \"reject all\" or minimal consent)\n\n## Example: Full Login Flow (Undetectable)\n\n```bash\nAPI=$STEALTHY_AUTO_BROWSE_URL\n\n# Navigate to login page\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"goto\", \"url\": \"https://example.com/login\"}'\n\n# See what's on the page\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"get_text\"}'\n\n# Find all interactive elements and their coordinates\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"get_interactive_elements\"}'\n\n# Click the email field (coordinates from get_interactive_elements)\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"system_click\", \"x\": 400, \"y\": 200}'\n\n# Type email with human-like keystrokes\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"system_type\", \"text\": \"user@example.com\"}'\n\n# Tab to password field\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"send_key\", \"key\": \"tab\"}'\n\n# Type password\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"system_type\", \"text\": \"secretpassword\"}'\n\n# Press Enter to submit\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"send_key\", \"key\": \"enter\"}'\n\n# Wait for redirect to dashboard\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"wait_for_url\", \"url\": \"**/dashboard\", \"timeout\": 15}'\n\n# Verify we're logged in\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"get_text\"}'\n```\n\n## Tips\n\n1. **Always call `get_interactive_elements` before clicking** — don't guess coordinates\n2. **Use system methods for stealth** — `system_click`, `system_type`, `send_key` are undetectable\n3. **Use `get_text` first, screenshots second** — text is faster and smaller\n4. **Match TZ to your IP location** — timezone mismatch is a common bot detection signal\n5. **Resize screenshots with `?whLargest=512`** — full resolution is unnecessarily large\n6. **Mount `/userdata`** for persistent sessions — cookies, fingerprint, and profile survive restarts\n7. **Use wait conditions instead of `sleep`** — `wait_for_element`, `wait_for_text`, `wait_for_url`\n8. **Call `handle_dialog` BEFORE the action that triggers it** — if you need to dismiss or provide prompt text (dialogs are auto-accepted otherwise)\n9. **Call `calibrate` after fullscreen changes** — coordinate mapping shifts\n10. **Add slight delays between actions for realism** — `sleep` with 0.5-1.5s between clicks looks more human\n\nFile v1.3.0:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"stealthy-auto-browse\",\n  \"version\": \"1.3.0\",\n  \"publishedAt\": 1770710585571\n}\n\nArchive v1.2.1: 2 files, 12172 bytes\n\nFiles: SKILL.md (36364b), _meta.json (139b)\n\nFile v1.2.1:SKILL.md\n\n---\nname: stealthy-auto-browse\ndescription: Browser automation that passes CreepJS, BrowserScan, Pixelscan, and Cloudflare — zero CDP exposure, OS-level input, persistent fingerprints. Use when standard browser skills get 403s or CAPTCHAs.\nhomepage: https://github.com/psyb0t/docker-stealthy-auto-browse\nuser-invocable: true\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🕵️\", \"primaryEnv\": \"STEALTHY_AUTO_BROWSE_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# stealthy-auto-browse\n\nA stealth browser running in Docker. It uses Camoufox (a custom Firefox fork) instead of Chromium, so there are zero Chrome DevTools Protocol (CDP) signals for bot detectors to find. Mouse and keyboard input happens at the OS level via PyAutoGUI — the browser itself doesn't know it's being automated, which means behavioral analysis can't detect it either.\n\n## Why This Exists\n\nStandard browser automation (Playwright + Chromium, Puppeteer, Selenium) exposes CDP signals that bot detection services (Cloudflare, DataDome, PerimeterX, Akamai) catch instantly. Even with stealth plugins, the CDP protocol is still there and detectable. This skill eliminates that entirely by using Firefox (no CDP at all) and generating input events at the OS level rather than through the browser's automation API.\n\n## When To Use This Skill\n\n- Site has bot detection (Cloudflare challenge pages, DataDome, PerimeterX, Akamai)\n- Site blocks headless browsers or serves CAPTCHAs\n- You need a logged-in session that doesn't get banned\n- Another browser skill is getting 403s or empty/blocked responses\n- You're scraping a site that actively fights automation\n\n## When NOT To Use This Skill\n\n- Simple fetches with no bot protection — use `curl` or `WebFetch`\n- Sites that don't care about automation — use a regular browser skill, it's faster to set up\n- You only need static HTML — use `curl`\n\n## Setup\n\n**1. Start the container:**\n\n```bash\ndocker run -d -p 8080:8080 -p 5900:5900 psyb0t/stealthy-auto-browse\n```\n\nPort 8080 is the HTTP API. Port 5900 is a noVNC web viewer where you can watch the browser in real time.\n\n**2. Set the environment variable:**\n\n```bash\nexport STEALTHY_AUTO_BROWSE_URL=http://localhost:8080\n```\n\nOr via OpenClaw config (`~/.openclaw/openclaw.json`):\n\n```json\n{\n  \"skills\": {\n    \"entries\": {\n      \"stealthy-auto-browse\": {\n        \"env\": {\n          \"STEALTHY_AUTO_BROWSE_URL\": \"http://localhost:8080\"\n        }\n      }\n    }\n  }\n}\n```\n\n**3. Verify:** `curl $STEALTHY_AUTO_BROWSE_URL/health` returns `ok` when the browser is ready.\n\n## How It Works\n\nThe container runs a virtual X display (Xvfb at 1920x1080), the Camoufox browser, and an HTTP API server. You send JSON commands to the API and get JSON responses back. All commands go to `POST $STEALTHY_AUTO_BROWSE_URL/` with `{\"action\": \"<name>\", ...params}`.\n\nEvery response has this shape:\n\n```json\n{\n  \"success\": true,\n  \"timestamp\": 1234567890.123,\n  \"data\": { ... },\n  \"error\": \"only present when success is false\"\n}\n```\n\nThe `data` field contents vary by action — documented below for each one.\n\n## Understanding the Two Input Modes\n\nThis is the most important concept. There are two ways to interact with pages:\n\n### System Input (Undetectable)\n\nActions: `system_click`, `mouse_move`, `mouse_click`, `system_type`, `send_key`, `scroll`\n\nThese use PyAutoGUI to generate real OS-level mouse movements and keystrokes. The browser receives these as genuine user input — there is no way for any website JavaScript to distinguish these from a real human. **Use these for stealth.**\n\nSystem input works with **viewport coordinates** (x, y pixel positions within the browser content area). Get these coordinates from `get_interactive_elements`.\n\n### Playwright Input (Detectable)\n\nActions: `click`, `fill`, `type`\n\nThese use Playwright's DOM automation to interact with elements by CSS selector or XPath. They're faster and more reliable (no coordinate math), but they inject events through the browser's automation layer. Sophisticated behavioral analysis can potentially detect the timing patterns. **Use these when speed matters more than stealth, or when you have a selector but no coordinates.**\n\n### When to Use Which\n\n- **Stealth-critical sites** (Cloudflare, login forms, anything with bot detection): Always use system input.\n- **Simple scraping** where the site isn't actively fighting you: Playwright input is fine and easier.\n- **Form filling**: Use `system_click` to focus the field, then `system_type` to enter text. This is undetectable. Using `fill` is faster but detectable.\n- **Clicking buttons**: If you have coordinates from `get_interactive_elements`, use `system_click`. If you only have a CSS selector, use `click`.\n\n## Workflow\n\nThis is the typical sequence for interacting with a page:\n\n1. **Navigate**: `goto` to load the URL\n2. **Read the page**: `get_text` returns all visible text — usually enough to understand the page\n3. **If text isn't clear**: `get_html` gives you the full DOM structure\n4. **If still confused**: Take a screenshot (`GET /screenshot/browser?whLargest=512`)\n5. **Find interactive elements**: `get_interactive_elements` returns all buttons, links, inputs with their x,y coordinates\n6. **Interact**: `system_click` to click, `system_type` to type, `send_key` for Enter/Tab/Escape\n7. **Wait for results**: `wait_for_element` or `wait_for_text` instead of sleeping\n8. **Verify**: `get_text` again to confirm the page changed as expected\n\n## Actions Reference\n\n### Navigation\n\n#### goto\n\nNavigates to a URL. This is how you load pages.\n\n```json\n{\"action\": \"goto\", \"url\": \"https://example.com\"}\n{\"action\": \"goto\", \"url\": \"https://example.com\", \"wait_until\": \"networkidle\"}\n```\n\n**Parameters:**\n- `url` (required): The URL to navigate to.\n- `wait_until` (optional, default `\"domcontentloaded\"`): When to consider the page loaded. Options: `\"domcontentloaded\"` (DOM parsed, fast), `\"load\"` (all resources loaded), `\"networkidle\"` (no network activity for 500ms, slowest but most complete).\n\n**Response data:** `{\"url\": \"https://example.com/\", \"title\": \"Example Domain\"}`\n\n**Note:** If a page loader matches the URL (see Page Loaders section), the loader's steps execute instead of the default navigation. The response will include `\"loader\": \"loader name\"` when this happens.\n\n#### back / forward / refresh\n\nStandard browser navigation. No parameters needed.\n\n```json\n{\"action\": \"back\"}\n{\"action\": \"forward\"}\n{\"action\": \"refresh\"}\n```\n\n### System Input (Undetectable)\n\n#### system_click\n\nMoves the mouse to viewport coordinates with a human-like curve (random jitter, eased acceleration), then clicks. This is the primary way to click things stealthily.\n\n```json\n{\"action\": \"system_click\", \"x\": 500, \"y\": 300}\n{\"action\": \"system_click\", \"x\": 500, \"y\": 300, \"duration\": 0.5}\n```\n\n**Parameters:**\n- `x`, `y` (required): Viewport coordinates — get these from `get_interactive_elements`.\n- `duration` (optional): How long the mouse movement takes in seconds. If omitted, a random duration between 0.2-0.6s is used for realism.\n\n**Response data:** `{\"system_clicked\": {\"x\": 500, \"y\": 300}}`\n\n**How it differs from `mouse_click`:** `system_click` always moves the mouse first (smooth human-like path), then clicks. `mouse_click` can click at a position instantly without the smooth movement, or click wherever the mouse currently is.\n\n#### mouse_move\n\nMoves the mouse to viewport coordinates with human-like movement (jitter, eased curve) but does NOT click. Use this to hover over elements (to trigger hover menus, tooltips) or to simulate natural mouse behavior between actions.\n\n```json\n{\"action\": \"mouse_move\", \"x\": 500, \"y\": 300}\n{\"action\": \"mouse_move\", \"x\": 500, \"y\": 300, \"duration\": 0.4}\n```\n\n**Parameters:**\n- `x`, `y` (required): Viewport coordinates.\n- `duration` (optional): Movement time in seconds. Random 0.2-0.6s if omitted.\n\n**Response data:** `{\"moved_to\": {\"x\": 500, \"y\": 300}}`\n\n#### mouse_click\n\nClicks at a position or at the current mouse location. Unlike `system_click`, this does NOT do a smooth mouse movement first — it's a direct click via PyAutoGUI.\n\n```json\n{\"action\": \"mouse_click\"}\n{\"action\": \"mouse_click\", \"x\": 500, \"y\": 300}\n```\n\n**Parameters:**\n- `x`, `y` (optional): If provided, clicks at that viewport position directly. If omitted, clicks wherever the mouse currently is.\n\n**Response data:** `{\"clicked_at\": {\"x\": 500, \"y\": 300}}` or `{\"clicked_at\": \"current\"}`\n\n**When to use:** After a `mouse_move` when you want to separate the movement and click into two steps. Or when the mouse is already positioned and you just need to click.\n\n#### system_type\n\nTypes text character-by-character via real OS keystrokes. Each keystroke has a randomized delay (jittered around the interval) to mimic human typing speed. Completely undetectable.\n\n```json\n{\"action\": \"system_type\", \"text\": \"hello world\"}\n{\"action\": \"system_type\", \"text\": \"hello world\", \"interval\": 0.12}\n```\n\n**Parameters:**\n- `text` (required): The text to type. Must click/focus an input field first.\n- `interval` (optional, default `0.08`): Base delay between keystrokes in seconds. Actual delay is randomized +-30ms around this value.\n\n**Response data:** `{\"typed_len\": 11}`\n\n**Important:** You must click on the input field first (using `system_click` or `click`) before calling `system_type`. This action types into whatever is currently focused.\n\n#### send_key\n\nSends a single keyboard key or key combination via OS-level input. Use this for pressing Enter to submit forms, Tab to move between fields, Escape to close dialogs, or any key combos like Ctrl+A, Ctrl+C, etc.\n\n```json\n{\"action\": \"send_key\", \"key\": \"enter\"}\n{\"action\": \"send_key\", \"key\": \"tab\"}\n{\"action\": \"send_key\", \"key\": \"escape\"}\n{\"action\": \"send_key\", \"key\": \"ctrl+a\"}\n{\"action\": \"send_key\", \"key\": \"ctrl+shift+t\"}\n```\n\n**Parameters:**\n- `key` (required): Key name or combo with `+` separator. Key names follow PyAutoGUI naming: `enter`, `tab`, `escape`, `backspace`, `delete`, `up`, `down`, `left`, `right`, `home`, `end`, `pageup`, `pagedown`, `f1`-`f12`, `ctrl`, `alt`, `shift`, `space`, etc.\n\n**Response data:** `{\"send_key\": \"enter\"}`\n\n#### scroll\n\nScrolls the page using the mouse scroll wheel. Generates real OS-level scroll events.\n\n```json\n{\"action\": \"scroll\", \"amount\": -3}\n{\"action\": \"scroll\", \"amount\": 5, \"x\": 500, \"y\": 300}\n```\n\n**Parameters:**\n- `amount` (optional, default `-3`): Scroll amount. **Negative = scroll down**, positive = scroll up. Each unit is roughly one \"click\" of a mouse wheel.\n- `x`, `y` (optional): If provided, moves the mouse to these viewport coordinates first, then scrolls. Useful for scrolling inside a specific scrollable element rather than the whole page.\n\n**Response data:** `{\"scrolled\": -3}`\n\n### Playwright Input (Detectable)\n\nThese are faster and more convenient but use Playwright's DOM event injection, which is detectable by sophisticated behavioral analysis.\n\n#### click\n\nClicks an element by CSS selector or XPath. Playwright finds the element in the DOM, scrolls it into view if needed, and dispatches click events.\n\n```json\n{\"action\": \"click\", \"selector\": \"#submit-btn\"}\n{\"action\": \"click\", \"selector\": \"button.primary\"}\n{\"action\": \"click\", \"selector\": \"xpath=//button[@id='submit-btn']\"}\n```\n\n**Parameters:**\n- `selector` (required): CSS selector or XPath (prefix with `xpath=`).\n\n**Response data:** `{\"clicked\": \"#submit-btn\"}`\n\n**When to use over system_click:** When you have a selector but don't want to bother getting coordinates. When the element might move around and coordinates aren't reliable. When stealth isn't critical.\n\n#### fill\n\nFills an input field by selector. Clears any existing content first, then sets the value. This is the fastest way to fill forms but is detectable because it doesn't generate individual keystroke events.\n\n```json\n{\"action\": \"fill\", \"selector\": \"input[name='email']\", \"value\": \"user@example.com\"}\n```\n\n**Parameters:**\n- `selector` (required): CSS selector or XPath of the input element.\n- `value` (required): Text to fill in.\n\n**Response data:** `{\"filled\": \"input[name='email']\"}`\n\n#### type\n\nTypes text into an element character-by-character via Playwright (NOT the OS). Each keystroke has a configurable delay. This is a middle ground between `fill` (instant but obviously automated) and `system_type` (OS-level, undetectable). The typing pattern is more realistic than `fill` but still comes through Playwright's event system.\n\n```json\n{\"action\": \"type\", \"selector\": \"#search\", \"text\": \"query\", \"delay\": 0.05}\n```\n\n**Parameters:**\n- `selector` (required): CSS selector or XPath of the element.\n- `text` (required): Text to type.\n- `delay` (optional, default `0.05`): Delay between keystrokes in seconds.\n\n**Response data:** `{\"typed\": \"#search\"}`\n\n### Screenshots\n\nScreenshots are GET requests (not POST actions).\n\n#### GET /screenshot/browser\n\nCaptures the browser viewport as a PNG image. This is what the page looks like to a user.\n\n```bash\ncurl -s \"$STEALTHY_AUTO_BROWSE_URL/screenshot/browser?whLargest=512\" -o screenshot.png\n```\n\n**Always resize screenshots** to avoid huge images. Resize query parameters (all optional):\n\n| Parameter | What it does |\n|-----------|-------------|\n| `whLargest=512` | Scales so the largest dimension is 512px, keeps aspect ratio. **Use this by default.** |\n| `width=800` | Scales to 800px wide, keeps aspect ratio |\n| `height=300` | Scales to 300px tall, keeps aspect ratio |\n| `width=400&height=400` | Forces exact 400x400 dimensions |\n\n#### GET /screenshot/desktop\n\nCaptures the entire virtual desktop (including window chrome, taskbar, etc.) using `scrot`. Same resize parameters as above. Useful when you need to see things outside the browser viewport.\n\n```bash\ncurl -s \"$STEALTHY_AUTO_BROWSE_URL/screenshot/desktop?whLargest=512\" -o desktop.png\n```\n\n### Page Inspection\n\n#### get_interactive_elements\n\nScans the page and returns every interactive element (buttons, links, inputs, selects, textareas, etc.) with their viewport coordinates. This is how you find what to click and where.\n\n```json\n{\"action\": \"get_interactive_elements\"}\n{\"action\": \"get_interactive_elements\", \"visible_only\": true}\n```\n\n**Parameters:**\n- `visible_only` (optional, default `true`): Only return elements that are currently visible on screen.\n\n**Response data:**\n```json\n{\n  \"count\": 5,\n  \"elements\": [\n    {\n      \"tag\": \"button\",\n      \"text\": \"Submit\",\n      \"selector\": \"#submit-btn\",\n      \"x\": 400,\n      \"y\": 250,\n      \"w\": 120,\n      \"h\": 40,\n      \"visible\": true\n    },\n    {\n      \"tag\": \"input\",\n      \"text\": \"\",\n      \"selector\": \"input[name='email']\",\n      \"x\": 300,\n      \"y\": 180,\n      \"w\": 250,\n      \"h\": 35,\n      \"visible\": true\n    }\n  ]\n}\n```\n\nThe `x`, `y` are the center of the element — pass these directly to `system_click`. The `selector` can be used with Playwright actions like `click` or `fill`. The `w`, `h` give you the element dimensions.\n\n**This is your primary tool for understanding what you can interact with on a page.** Call this before clicking anything.\n\n#### get_text\n\nReturns all visible text content of the page body. Text is truncated to 10,000 characters.\n\n```json\n{\"action\": \"get_text\"}\n```\n\n**Response data:** `{\"text\": \"Page title\\nSome content here...\", \"length\": 1234}`\n\nThis is usually the first thing to call after navigating — it tells you what's on the page without needing a screenshot.\n\n#### get_html\n\nReturns the full HTML source of the current page.\n\n```json\n{\"action\": \"get_html\"}\n```\n\n**Response data:** `{\"html\": \"<!DOCTYPE html>...\", \"length\": 45678}`\n\nUse when `get_text` doesn't give enough structure to understand the page layout, or when you need to find specific elements in the DOM.\n\n#### eval\n\nExecutes arbitrary JavaScript in the page context and returns the result. The expression is evaluated via `page.evaluate()`.\n\n```json\n{\"action\": \"eval\", \"expression\": \"document.title\"}\n{\"action\": \"eval\", \"expression\": \"document.querySelectorAll('a').length\"}\n{\"action\": \"eval\", \"expression\": \"JSON.stringify(performance.timing)\"}\n```\n\n**Parameters:**\n- `expression` (required): JavaScript expression to evaluate. Must return a JSON-serializable value.\n\n**Response data:** `{\"result\": \"Example Domain\"}` — the result is whatever the expression returns.\n\n### Wait Conditions\n\nUse these instead of `sleep` to wait for page content. They're more reliable because they wait for the exact condition rather than an arbitrary time.\n\n#### wait_for_element\n\nWaits for an element matching a CSS selector or XPath to reach a certain state (visible, hidden, attached to DOM, detached).\n\n```json\n{\"action\": \"wait_for_element\", \"selector\": \"#results\", \"timeout\": 10}\n{\"action\": \"wait_for_element\", \"selector\": \"xpath=//div[@class='loaded']\", \"timeout\": 15}\n{\"action\": \"wait_for_element\", \"selector\": \".spinner\", \"state\": \"hidden\", \"timeout\": 10}\n```\n\n**Parameters:**\n- `selector` (required): CSS selector or XPath (prefix with `xpath=`).\n- `state` (optional, default `\"visible\"`): What state to wait for. Options: `\"visible\"` (rendered and not hidden), `\"hidden\"` (not visible), `\"attached\"` (in DOM regardless of visibility), `\"detached\"` (removed from DOM).\n- `timeout` (optional, default `30`): Max wait time in seconds. Throws error if exceeded.\n\n**Response data:** `{\"selector\": \"#results\", \"state\": \"visible\"}`\n\n#### wait_for_text\n\nWaits for specific text to appear anywhere in the page body.\n\n```json\n{\"action\": \"wait_for_text\", \"text\": \"Search results\", \"timeout\": 10}\n```\n\n**Parameters:**\n- `text` (required): Exact text to look for (substring match on `document.body.innerText`).\n- `timeout` (optional, default `30`): Max wait time in seconds.\n\n**Response data:** `{\"text\": \"Search results\", \"found\": true}`\n\n#### wait_for_url\n\nWaits for the page URL to match a pattern. Useful after form submissions or redirects.\n\n```json\n{\"action\": \"wait_for_url\", \"url\": \"**/dashboard\", \"timeout\": 10}\n{\"action\": \"wait_for_url\", \"url\": \"https://example.com/success*\", \"timeout\": 15}\n```\n\n**Parameters:**\n- `url` (required): URL pattern to match. Supports `*` (any chars except `/`) and `**` (any chars including `/`) glob patterns. Can also be a full URL for exact match.\n- `timeout` (optional, default `30`): Max wait time in seconds.\n\n**Response data:** `{\"url\": \"https://example.com/dashboard\"}`\n\n#### wait_for_network_idle\n\nWaits until there are no network requests in flight for 500ms. Useful for pages that load content dynamically after the initial page load.\n\n```json\n{\"action\": \"wait_for_network_idle\", \"timeout\": 30}\n```\n\n**Parameters:**\n- `timeout` (optional, default `30`): Max wait time in seconds.\n\n**Response data:** `{\"idle\": true}`\n\n### Tab Management\n\nThe browser can have multiple tabs open. One tab is \"active\" at a time — all actions operate on the active tab.\n\n#### list_tabs\n\nReturns all open tabs with their URLs and which one is active.\n\n```json\n{\"action\": \"list_tabs\"}\n```\n\n**Response data:**\n```json\n{\n  \"count\": 2,\n  \"tabs\": [\n    {\"index\": 0, \"url\": \"https://example.com/\", \"active\": false},\n    {\"index\": 1, \"url\": \"https://other.com/\", \"active\": true}\n  ]\n}\n```\n\n#### new_tab\n\nOpens a new browser tab. Optionally navigates it to a URL. The new tab becomes the active tab.\n\n```json\n{\"action\": \"new_tab\"}\n{\"action\": \"new_tab\", \"url\": \"https://example.com\"}\n```\n\n**Parameters:**\n- `url` (optional): URL to navigate to in the new tab.\n- `wait_until` (optional, default `\"domcontentloaded\"`): Same as `goto`.\n\n**Response data:** `{\"index\": 1, \"url\": \"https://example.com/\"}`\n\n#### switch_tab\n\nSwitches the active tab by index (0-based). All subsequent actions will operate on this tab.\n\n```json\n{\"action\": \"switch_tab\", \"index\": 0}\n```\n\n**Parameters:**\n- `index` (required): Tab index from `list_tabs`.\n\n**Response data:** `{\"index\": 0, \"url\": \"https://example.com/\"}`\n\n#### close_tab\n\nCloses a tab. After closing, the last remaining tab becomes active.\n\n```json\n{\"action\": \"close_tab\"}\n{\"action\": \"close_tab\", \"index\": 1}\n```\n\n**Parameters:**\n- `index` (optional): Tab index to close. If omitted, closes the currently active tab.\n\n**Response data:** `{\"closed\": true, \"remaining\": 1}`\n\n### Dialog Handling\n\nBrowsers have modal dialogs (alert, confirm, prompt) that block the page until dismissed. You must handle these proactively.\n\n#### handle_dialog\n\n**Must be called BEFORE the action that triggers the dialog.** This pre-configures how the next dialog will be handled. If you don't call this before a dialog appears, the page will hang.\n\n```json\n{\"action\": \"handle_dialog\", \"accept\": true}\n{\"action\": \"handle_dialog\", \"accept\": false}\n{\"action\": \"handle_dialog\", \"accept\": true, \"text\": \"my response\"}\n```\n\n**Parameters:**\n- `accept` (optional, default `true`): `true` clicks OK/Accept, `false` clicks Cancel/Dismiss.\n- `text` (optional): Response text for prompt dialogs. Ignored for alert/confirm.\n\n**Response data:** `{\"configured\": {\"accept\": true, \"text\": null}}`\n\n**Example — handling a confirm dialog:**\n```bash\n# Step 1: Tell the browser to accept the next dialog\ncurl -X POST $API -H 'Content-Type: application/json' -d '{\"action\": \"handle_dialog\", \"accept\": true}'\n# Step 2: Now click the button that triggers the confirm\ncurl -X POST $API -H 'Content-Type: application/json' -d '{\"action\": \"system_click\", \"x\": 300, \"y\": 200}'\n```\n\n#### get_last_dialog\n\nReturns information about the most recent dialog that appeared.\n\n```json\n{\"action\": \"get_last_dialog\"}\n```\n\n**Response data:**\n```json\n{\n  \"dialog\": {\n    \"type\": \"confirm\",\n    \"message\": \"Are you sure you want to delete this?\",\n    \"default_value\": \"\",\n    \"buttons\": [\"ok\", \"cancel\"]\n  }\n}\n```\n\nReturns `{\"dialog\": null}` if no dialog has appeared yet. The `type` field is one of: `\"alert\"`, `\"confirm\"`, `\"prompt\"`, `\"beforeunload\"`.\n\n### Cookies\n\n#### get_cookies\n\nReturns all cookies for the browser context, or cookies for specific URLs.\n\n```json\n{\"action\": \"get_cookies\"}\n{\"action\": \"get_cookies\", \"urls\": [\"https://example.com\"]}\n```\n\n**Parameters:**\n- `urls` (optional): Array of URLs to filter cookies by. If omitted, returns all cookies.\n\n**Response data:**\n```json\n{\n  \"count\": 3,\n  \"cookies\": [\n    {\"name\": \"session\", \"value\": \"abc123\", \"domain\": \".example.com\", \"path\": \"/\", \"httpOnly\": true, \"secure\": true, ...}\n  ]\n}\n```\n\n#### set_cookie\n\nSets a cookie in the browser context.\n\n```json\n{\"action\": \"set_cookie\", \"name\": \"session\", \"value\": \"abc123\", \"url\": \"https://example.com\"}\n{\"action\": \"set_cookie\", \"name\": \"pref\", \"value\": \"dark\", \"domain\": \".example.com\", \"path\": \"/\", \"httpOnly\": false, \"secure\": true}\n```\n\n**Parameters:** Any standard cookie fields — `name`, `value`, `url`, `domain`, `path`, `httpOnly`, `secure`, `sameSite`, `expires`. At minimum you need `name`, `value`, and either `url` or `domain`.\n\n**Response data:** `{\"set\": \"session\"}`\n\n#### delete_cookies\n\nClears all cookies from the browser context.\n\n```json\n{\"action\": \"delete_cookies\"}\n```\n\n**Response data:** `{\"cleared\": true}`\n\n### Storage\n\nAccess the page's localStorage and sessionStorage. These are per-origin — you must be on the right page for the storage to be accessible.\n\n#### get_storage\n\nReturns all items from localStorage or sessionStorage as a key-value object.\n\n```json\n{\"action\": \"get_storage\", \"type\": \"local\"}\n{\"action\": \"get_storage\", \"type\": \"session\"}\n```\n\n**Parameters:**\n- `type` (optional, default `\"local\"`): `\"local\"` for localStorage, `\"session\"` for sessionStorage.\n\n**Response data:** `{\"items\": {\"theme\": \"dark\", \"lang\": \"en\"}, \"type\": \"local\"}`\n\n#### set_storage\n\nSets a single key-value pair in localStorage or sessionStorage.\n\n```json\n{\"action\": \"set_storage\", \"type\": \"local\", \"key\": \"theme\", \"value\": \"dark\"}\n```\n\n**Parameters:**\n- `type` (optional, default `\"local\"`): `\"local\"` or `\"session\"`.\n- `key` (required): Storage key.\n- `value` (required): Storage value (string).\n\n**Response data:** `{\"set\": \"theme\", \"type\": \"local\"}`\n\n#### clear_storage\n\nClears all items from localStorage or sessionStorage.\n\n```json\n{\"action\": \"clear_storage\", \"type\": \"local\"}\n{\"action\": \"clear_storage\", \"type\": \"session\"}\n```\n\n**Response data:** `{\"cleared\": \"local\"}`\n\n### Downloads\n\nThe browser automatically tracks file downloads triggered by page interactions (clicking download links, form submissions that return files, etc.).\n\n#### get_last_download\n\nReturns information about the most recently downloaded file.\n\n```json\n{\"action\": \"get_last_download\"}\n```\n\n**Response data:**\n```json\n{\n  \"download\": {\n    \"url\": \"https://example.com/file.pdf\",\n    \"filename\": \"file.pdf\",\n    \"path\": \"/tmp/playwright-downloads/abc123/file.pdf\"\n  }\n}\n```\n\nReturns `{\"download\": null}` if nothing has been downloaded yet. The `path` is the local path inside the container where the file was saved. The `filename` is what the server suggested as the download name.\n\n### Uploads\n\n#### upload_file\n\nProgrammatically sets a file on an `<input type=\"file\">` element without opening the OS file picker. The file must exist inside the container — use `docker cp` to copy files in if needed.\n\n```json\n{\"action\": \"upload_file\", \"selector\": \"#file-input\", \"file_path\": \"/tmp/document.pdf\"}\n```\n\n**Parameters:**\n- `selector` (required): CSS selector of the file input element.\n- `file_path` (required): Absolute path to the file inside the container.\n\n**Response data:** `{\"selector\": \"#file-input\", \"file\": \"document.pdf\", \"size\": 12345}`\n\n**Note:** After setting the file, you still need to submit the form (click the submit button) for the upload to actually happen.\n\n### Network Logging\n\nCapture all HTTP requests and responses the page makes. Useful for debugging, finding API endpoints the page calls, or verifying that certain resources loaded.\n\n#### enable_network_log\n\nStarts recording all HTTP requests and responses from the active page.\n\n```json\n{\"action\": \"enable_network_log\"}\n```\n\n**Response data:** `{\"enabled\": true}`\n\n#### disable_network_log\n\nStops recording network activity. Already-captured entries remain.\n\n```json\n{\"action\": \"disable_network_log\"}\n```\n\n**Response data:** `{\"enabled\": false}`\n\n#### get_network_log\n\nReturns all captured network entries since logging was enabled (or last cleared).\n\n```json\n{\"action\": \"get_network_log\"}\n```\n\n**Response data:**\n```json\n{\n  \"count\": 4,\n  \"log\": [\n    {\"type\": \"request\", \"url\": \"https://api.example.com/data\", \"method\": \"GET\", \"resource_type\": \"fetch\", \"timestamp\": 1234567890.123},\n    {\"type\": \"response\", \"url\": \"https://api.example.com/data\", \"status\": 200, \"timestamp\": 1234567890.456},\n    {\"type\": \"request\", \"url\": \"https://cdn.example.com/style.css\", \"method\": \"GET\", \"resource_type\": \"stylesheet\", \"timestamp\": 1234567890.789},\n    {\"type\": \"response\", \"url\": \"https://cdn.example.com/style.css\", \"status\": 200, \"timestamp\": 1234567890.999}\n  ]\n}\n```\n\nEach entry is either a `\"request\"` or `\"response\"`. Requests include `method` and `resource_type` (fetch, document, stylesheet, script, image, etc.). Responses include `status` code.\n\n#### clear_network_log\n\nDeletes all captured network entries but keeps logging enabled if it was on.\n\n```json\n{\"action\": \"clear_network_log\"}\n```\n\n**Response data:** `{\"cleared\": true}`\n\n### Scrolling\n\n#### scroll_to_bottom\n\nScrolls the entire page from top to bottom using JavaScript `window.scrollBy()`. Scrolls one viewport height at a time with a fixed delay between scrolls. When it reaches the bottom (scroll position stops changing), it scrolls back to the top. Useful for triggering lazy-loaded content.\n\n```json\n{\"action\": \"scroll_to_bottom\"}\n{\"action\": \"scroll_to_bottom\", \"delay\": 0.6}\n```\n\n**Parameters:**\n- `delay` (optional, default `0.4`): Seconds to wait between each scroll step.\n\n**Response data:** `{\"scrolled\": \"bottom\"}`\n\n#### scroll_to_bottom_humanized\n\nSame as `scroll_to_bottom` but uses real OS-level mouse wheel scrolling (via PyAutoGUI) with randomized scroll amounts and jittered delays to look like a human scrolling. Undetectable by behavioral analysis.\n\n```json\n{\"action\": \"scroll_to_bottom_humanized\"}\n{\"action\": \"scroll_to_bottom_humanized\", \"min_clicks\": 3, \"max_clicks\": 8, \"delay\": 0.7}\n```\n\n**Parameters:**\n- `min_clicks` (optional, default `2`): Minimum mouse wheel clicks per scroll step.\n- `max_clicks` (optional, default `6`): Maximum mouse wheel clicks per scroll step. A random value between min and max is chosen each time.\n- `delay` (optional, default `0.5`): Base delay between scroll steps. Actual delay is jittered +-30%.\n\n**Response data:** `{\"scrolled\": \"bottom_humanized\"}`\n\n### Display\n\n#### calibrate\n\nRecalculates the mapping between viewport coordinates (what `get_interactive_elements` returns) and screen coordinates (what PyAutoGUI uses). The browser has window chrome (title bar, address bar) that offsets the viewport from the screen origin.\n\n```json\n{\"action\": \"calibrate\"}\n```\n\n**Response data:** `{\"window_offset\": {\"x\": 0, \"y\": 74}}`\n\n**When to call this:** After entering/exiting fullscreen, after the browser window is resized, or if `system_click` coordinates seem off. The offset is auto-calculated at startup, so you rarely need this.\n\n#### get_resolution\n\nReturns the virtual display resolution (from the XVFB_RESOLUTION environment variable).\n\n```json\n{\"action\": \"get_resolution\"}\n```\n\n**Response data:** `{\"width\": 1920, \"height\": 1080}`\n\n#### enter_fullscreen / exit_fullscreen\n\nToggles browser fullscreen mode (hides address bar and window chrome). In fullscreen, the viewport takes up the entire screen, so coordinates map differently.\n\n```json\n{\"action\": \"enter_fullscreen\"}\n{\"action\": \"exit_fullscreen\"}\n```\n\n**Response data:** `{\"fullscreen\": true, \"changed\": true}` — `changed` is `false` if already in the requested state.\n\n**Important:** Call `calibrate` after entering/exiting fullscreen to update the coordinate mapping.\n\n### Utility\n\n#### ping\n\nHealth check that returns the current page URL. Use to verify the API is responding and the browser is alive.\n\n```json\n{\"action\": \"ping\"}\n```\n\n**Response data:** `{\"message\": \"pong\", \"url\": \"https://example.com/\"}`\n\n#### sleep\n\nPauses execution for a specified duration. Prefer `wait_for_element` or `wait_for_text` when waiting for page content — use `sleep` only for fixed timing needs.\n\n```json\n{\"action\": \"sleep\", \"duration\": 2}\n```\n\n**Parameters:**\n- `duration` (optional, default `1`): Seconds to sleep.\n\n**Response data:** `{\"slept\": 2}`\n\n#### close\n\nShuts down the browser. The container will stop after this.\n\n```json\n{\"action\": \"close\"}\n```\n\n**Response data:** `{\"message\": \"closing\"}`\n\n### State Endpoints (GET)\n\n#### GET /state\n\nReturns the current browser state.\n\n```bash\ncurl -s \"$STEALTHY_AUTO_BROWSE_URL/state\"\n```\n\n**Response:**\n```json\n{\n  \"status\": \"ready\",\n  \"url\": \"https://example.com/\",\n  \"title\": \"Example Domain\",\n  \"window_offset\": {\"x\": 0, \"y\": 74}\n}\n```\n\n#### GET /health\n\nSimple health check. Returns `ok` as plain text when the API is ready.\n\n```bash\ncurl -s \"$STEALTHY_AUTO_BROWSE_URL/health\"\n```\n\n## Container Options\n\n```bash\n# Custom display resolution\ndocker run -d -p 8080:8080 -e XVFB_RESOLUTION=1280x720 psyb0t/stealthy-auto-browse\n\n# Match timezone to your IP's geographic location (important for stealth — mismatched\n# timezone is a common bot detection signal)\ndocker run -d -p 8080:8080 -e TZ=Europe/Bucharest psyb0t/stealthy-auto-browse\n\n# Route browser traffic through an HTTP proxy\ndocker run -d -p 8080:8080 -e PROXY_URL=http://user:pass@proxy:8888 psyb0t/stealthy-auto-browse\n\n# Persistent browser profile — cookies, sessions, and fingerprint survive container restarts\ndocker run -d -p 8080:8080 -v ./profile:/userdata psyb0t/stealthy-auto-browse\n\n# Open a URL automatically on startup\ndocker run -d -p 8080:8080 psyb0t/stealthy-auto-browse https://example.com\n```\n\n## Page Loaders\n\nLoaders let you define automated sequences that run when `goto` navigates to a matching URL. They're YAML files mounted to `/loaders`. Think of them like Greasemonkey scripts — when the URL matches, the loader's steps execute instead of the default navigation.\n\n```bash\ndocker run -d -p 8080:8080 -v ./my-loaders:/loaders psyb0t/stealthy-auto-browse\n```\n\n### Loader format\n\n```yaml\nname: Clean Up Example.com\nmatch:\n  domain: example.com       # Exact hostname match (www. is stripped automatically)\n  path_prefix: /articles    # URL path must start with this\n  regex: \"article/\\\\d+\"     # Full URL must match this regex\nsteps:\n  - action: goto\n    url: \"${url}\"            # ${url} is replaced with the original URL\n    wait_until: networkidle\n  - action: sleep\n    duration: 1\n  - action: eval\n    expression: \"document.querySelector('.cookie-banner')?.remove()\"\n  - action: eval\n    expression: \"document.querySelector('.popup-overlay')?.remove()\"\n```\n\n### Match rules\n\nAll match fields are optional, but at least one is required. If multiple fields are specified, ALL must match for the loader to trigger:\n\n- **`domain`**: Exact hostname match. `www.` is stripped from both the loader's domain and the URL's hostname before comparing.\n- **`path_prefix`**: The URL path must start with this string.\n- **`regex`**: The full URL must match this regular expression.\n\n### What happens when a loader matches\n\nWhen `goto` is called and a loader matches the URL, the loader's steps execute sequentially as regular API actions. Each step is a JSON command (same as what you'd POST to the API). The `${url}` placeholder in any string value is replaced with the original URL.\n\nThe response from a loader-matched `goto` includes the loader name:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"loader\": \"Clean Up Example.com\",\n    \"steps_executed\": 4,\n    \"last_result\": { ... }\n  }\n}\n```\n\n## Pre-installed Extensions\n\nThe browser comes with these extensions pre-installed:\n\n- **uBlock Origin**: Ad and tracker blocking\n- **LocalCDN**: Serves common CDN resources locally to prevent tracking\n- **ClearURLs**: Strips tracking parameters from URLs\n- **Consent-O-Matic**: Automatically handles cookie consent popups (clicks \"reject all\" or minimal consent)\n\n## Example: Full Login Flow (Undetectable)\n\n```bash\nAPI=$STEALTHY_AUTO_BROWSE_URL\n\n# Navigate to login page\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"goto\", \"url\": \"https://example.com/login\"}'\n\n# See what's on the page\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"get_text\"}'\n\n# Find all interactive elements and their coordinates\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"get_interactive_elements\"}'\n\n# Click the email field (coordinates from get_interactive_elements)\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"system_click\", \"x\": 400, \"y\": 200}'\n\n# Type email with human-like keystrokes\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"system_type\", \"text\": \"user@example.com\"}'\n\n# Tab to password field\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"send_key\", \"key\": \"tab\"}'\n\n# Type password\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"system_type\", \"text\": \"secretpassword\"}'\n\n# Press Enter to submit\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"send_key\", \"key\": \"enter\"}'\n\n# Wait for redirect to dashboard\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"wait_for_url\", \"url\": \"**/dashboard\", \"timeout\": 15}'\n\n# Verify we're logged in\ncurl -s -X POST $API -H 'Content-Type: application/json' \\\n  -d '{\"action\": \"get_text\"}'\n```\n\n## Tips\n\n1. **Always call `get_interactive_elements` before clicking** — don't guess coordinates\n2. **Use system methods for stealth** — `system_click`, `system_type`, `send_key` are undetectable\n3. **Use `get_text` first, screenshots second** — text is faster and smaller\n4. **Match TZ to your IP location** — timezone mismatch is a common bot detection signal\n5. **Resize screenshots with `?whLargest=512`** — full resolution is unnecessarily large\n6. **Mount `/userdata`** for persistent sessions — cookies, fingerprint, and profile survive restarts\n7. **Use wait conditions instead of `sleep`** — `wait_for_element`, `wait_for_text`, `wait_for_url`\n8. **Call `handle_dialog` BEFORE the action that triggers it** — or the page hangs\n9. **Call `calibrate` after fullscreen changes** — coordinate mapping shifts\n10. **Add slight delays between actions for realism** — `sleep` with 0.5-1.5s between clicks looks more human\n\nFile v1.2.1:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"stealthy-auto-browse\",\n  \"version\": \"1.2.1\",\n  \"publishedAt\": 1770673712930\n}","readmeExcerpt":"Skill: stealthy-auto-browse Owner: psyb0t Summary: Browser automation that passes CreepJS, BrowserScan, Pixelscan, and Cloudflare — zero CDP exposure, OS-level input, persistent fingerprints. Use when standard browser skills get 403s or CAPTCHAs. Tags: latest:1.3.0 Version history: v1.3.0 | 2026-02-10T08:03:05.571Z | user better wording v1.2.1 | 2026-02-09T21:48:32.930Z | user Rewrite SKILL.md as a proper usage guide","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"docker run -d -p 8080:8080 -p 5900:5900 psyb0t/stealthy-auto-browse"},{"language":"bash","snippet":"export STEALTHY_AUTO_BROWSE_URL=http://localhost:8080"},{"language":"json","snippet":"{\n  \"skills\": {\n    \"entries\": {\n      \"stealthy-auto-browse\": {\n        \"env\": {\n          \"STEALTHY_AUTO_BROWSE_URL\": \"http://localhost:8080\"\n        }\n      }\n    }\n  }\n}"},{"language":"json","snippet":"{\n  \"success\": true,\n  \"timestamp\": 1234567890.123,\n  \"data\": { ... },\n  \"error\": \"only present when success is false\"\n}"},{"language":"json","snippet":"{\"action\": \"goto\", \"url\": \"https://example.com\"}\n{\"action\": \"goto\", \"url\": \"https://example.com\", \"wait_until\": \"networkidle\"}"},{"language":"json","snippet":"{\"action\": \"refresh\"}\n{\"action\": \"refresh\", \"wait_until\": \"networkidle\"}"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: stealthy-auto-browse\ndescription: Browser automation that passes CreepJS, BrowserScan, Pixelscan, and Cloudflare — zero CDP exposure, OS-level input, persistent fingerprints. Use when standard browser skills get 403s or CAPTCHAs.\nhomepage: https://github.com/psyb0t/docker-stealthy-auto-browse\nuser-invocable: true\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🕵️\", \"primaryEnv\": \"STEALTHY_AUTO_BROWSE_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# stealthy-auto-browse\n\nA stealth browser running in Docker. It uses Camoufox (a custom Firefox fork) instead of Chromium, so there are zero Chrome DevTools Protocol (CDP) signals for bot detectors to find. Mouse and keyboard input happens at the OS level via PyAutoGUI — the browser itself doesn't know it's being automated, which means behavioral analysis can't detect it either.\n\n## Why This Exists\n\nStandard browser automation (Playwright + Chromium, Puppeteer, Selenium) exposes CDP signals that bot detection services (Cloudflare, DataDome, PerimeterX, Akamai) catch instantly. Even with stealth plugins, the CDP protocol is still there and detectable. This skill eliminates that entirely by using Firefox (no CDP at all) and generating input events at the OS level rather than through the browser's automation API.\n\n## When To Use This Skill\n\n- Site has bot detection (Cloudflare challenge pages, DataDome, PerimeterX, Akamai)\n- Site blocks headless browsers or serves CAPTCHAs\n- You need a logged-in session that doesn't get banned\n- Another browser skill is getting 403s or empty/blocked responses\n- You're scraping a site that actively fights automation\n\n## When NOT To Use This Skill\n\n- Simple fetches with no bot protection — use `curl` or `WebFetch`\n- Sites that don't care about automation — use a regular browser skill, it's faster to set up\n- You only need static HTML — use `curl`\n\n## Setup\n\n**1. Start the container:**\n\n```bash\ndocker run -d -p 8080:8080 -p 5900:5900 psyb0t/stealthy-auto-browse\n```\n\nPort 8080 is the HTTP API. Port 5900 is a noVNC web viewer where you can watch the browser in real time.\n\n**2. Set the environment variable:**\n\n```bash\nexport STEALTHY_AUTO_BROWSE_URL=http://localhost:8080\n```\n\nOr via OpenClaw config (`~/.openclaw/openclaw.json`):\n\n```json\n{\n  \"skills\": {\n    \"entries\": {\n      \"stealthy-auto-browse\": {\n        \"env\": {\n          \"STEALTHY_AUTO_BROWSE_URL\": \"http://localhost:8080\"\n        }\n      }\n    }\n  }\n}\n```\n\n**3. Verify:** `curl $STEALTHY_AUTO_BROWSE_URL/health` returns `ok` when the browser is ready.\n\n## How It Works\n\nThe container runs a virtual X display (Xvfb at 1920x1080), the Camoufox browser, and an HTTP API server. You send JSON commands to the API and get JSON responses back. All commands go to `POST $STEALTHY_AUTO_BROWSE_URL/` with `{\"action\": \"<name>\", ...params}`.\n\nEvery response has this shape:\n\n```json\n{\n  \"success\": true,\n  \"timestamp\": 1234567890.123,\n  \"data\": { ... },\n  \"error\": \"only present when success is false\"\n}\n```\n\nThe `data` field conten"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"stealthy-auto-browse\",\n  \"version\": \"1.3.0\",\n  \"publishedAt\": 1770710585571\n}"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Browser automation that passes CreepJS, BrowserScan, Pixelscan, and Cloudflare — zero CDP exposure, OS-level input, persistent fingerprints. Use when standard browser skills get 403s or CAPTCHAs. Skill: stealthy-auto-browse Owner: psyb0t Summary: Browser automation that passes CreepJS, BrowserScan, Pixelscan, and Cloudflare — zero CDP exposure, OS-level input, persistent fingerprints. Use when standard browser skills get 403s or CAPTCHAs. Tags: latest:1.3.0 Version history: v1.3.0 | 2026-02-10T08:03:05.571Z | user better wording v1.2.1 | 2026-02-09T21:48:32.930Z | user Rewrite SKILL.md as a proper usage guide","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1199,"uniquenessScore":53,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-09T21:21:09.458Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}