Stealth and input behavior
Why this exists
This image uses Camoufox, a patched Firefox build, through Playwright’s Firefox transport rather than Chromium CDP. That avoids CDP-specific signals, but detection can still use browser fingerprints, network reputation, session history, and behavior. No browser or input mode is universally undetectable.
This container takes a different approach:
- Camoufox (custom Firefox fork) instead of Chromium. There is no CDP to detect because Firefox doesn’t use it
- PyAutoGUI for mouse and keyboard. Input happens at the OS level, not through the browser’s automation API, so the page receives trusted events with no injection trail. That leaves the behavior itself for a detector to judge, so every move, click, keystroke, and wheel notch is timed from human motor data
- Native fingerprint injection generated by Camoufox from a model of real Firefox traffic and applied below the page JavaScript layer. The identity persists with the profile, and its fonts, voices, media devices, GPU, and screen are drawn to fit together.
- Isolated automation. Playwright’s selectors and this image’s own page reads (challenge detection, element scans, input checks) run in a JavaScript world the page cannot see or hook. The
evalaction runs in the page’s world by default so it can read site globals; pass"world": "isolated"to keep a read invisible to the page. - Everything packaged in a single Docker container, run with one command
Two input modes
Understanding the difference between these is key to not getting detected.
System input (OS-level)
Actions: system_click, system_hover, system_double_click, system_right_click, system_mouse_down, system_mouse_up, system_drag, mouse_move, mouse_click, system_type, send_key, scroll, scroll_to_bottom_humanized, human_pause
These use PyAutoGUI to generate real OS-level events, timed from human motor data rather than random jitter:
- Mouse: curved sigma-lognormal paths with Fitts’s-law timing, a speed peak early in the move, overshoot and correction on about a third of moves, and hand tremor, posted at a 125 Hz mouse rate.
- Clicks: a human pause before the press, a ~100 ms button hold, and a landing point scattered inside the target instead of its exact centre.
- Typing: right-skewed key rhythm that depends on the key pair, real key holds, overlapping keys for fast typists, and Shift pressed before capitals. Optional typos are always corrected.
- Wheel: multi-notch gestures sent one notch at a time with a flick or controlled rhythm, reading pauses, and occasional scroll-backs.
Each container behaves like one consistent person. The main timings (speed, path bend, tremor, click and key holds, typing rhythm, typos, wheel spacing) can be tuned per request; see Humanized Input. The test suite scores these sessions with open-source bot detectors (motion-attestation, Gaitcha) and checks that a robotic control session is caught by the same checks. make test-real repeats this against hosted behavior scorers; see Behavioral checks.
human_pause represents visible reading, review, or composition time. It keeps the pointer still most of the time and optionally schedules sparse, irregular drifts through the same fitted mouse planner. It is not a constant jiggle, background movement service, or replacement for sleep. Agents should call it only when the page task naturally includes visible thinking time.
Human handoff does not synthesize activity. begin_handoff pauses mutating automation while a person uses noVNC, and handoff_status reports trusted pointer, keyboard, and wheel events without changing them.
System input uses viewport coordinates (x, y pixel positions). Get these from get_interactive_elements.
Playwright input (detectable but convenient)
Actions: click, fill, type
These use Playwright’s element automation with CSS, XPath, role, or reference targets. They are convenient and avoid manual coordinate handling, but their timing and behavior differ from the fitted OS-level planners. Do not assume every Playwright event is an untrusted JavaScript event; explicit synthetic DOM events are a separate test control.
Which to use
- Default: use
clickwith a CSS selector. It’s fast, reliable, and works for most sites. - Site explicitly detects and blocks DOM event injection? Fall back to system input.
- Pass
wandhwithsystem_click.get_interactive_elementsreturns them; with them the click lands at a human offset inside the element instead of near its centre. - Filling forms on a protected site?
fillfirst; if blocked,system_clickto focus thensystem_type. - Just scraping? Playwright input (
click,fill) is fine.
Bot detection test results
The following table preserves previously recorded observations, not current-release certifications. The original entries do not identify every test image or environment, so do not treat them as reproducible benchmarks or a guarantee. Results change with service version, network exit, timezone, browser build, and tested flow. Run a check against the exact setup you plan to use.
| Service | Result | What They Check |
|---|---|---|
| CreepJS | Pass | Canvas/WebGL fingerprint consistency, lies detection, worker comparison |
| liarjs.dev | 82/100, no critical findings | Font, canvas, worker, GPU, network, and behavioral consistency |
| BrowserScan | Pass | WebDriver flag, CDP signals, navigator properties |
| Pixelscan | Pass | Fingerprint coherence, timezone/IP match, WebRTC leaks |
| Cloudflare | Pass | Challenge pages, Turnstile, bot management |
| SannySoft | Pass | Intoli + fingerprint scanner tests |
| Incolumitas | Pass | Modern detection techniques |
| Rebrowser | Pass | CDP leak detection, webdriver, viewport analysis |
| BrowserLeaks WebRTC | Pass | WebRTC IP leak detection |
| DeviceAndBrowserInfo | Pass | 19 checks, all green, “You are human!” |
| IpHey | Pass | “Trustworthy” rating |
| Fingerprint.com | Pass | Identified as normal Firefox, no bot flags |
The liarjs.dev result was observed with TZ matched to the network exit. Its remaining warnings were empty native media-device enumeration and the human challenge not being taken.
Behavioral checks
These detectors score how the input moves, not what the browser is. Each check runs from one container and one IP, so only the input path differs. The controls vary because hosted detectors change their behavior.
| Check | Humanized system input | Playwright DOM input | Synthetic DOM input | Raw PyAutoGUI |
|---|---|---|---|---|
| DeviceAndBrowserInfo login test | human | bot | not run | bot |
| FCaptcha checkbox | verified | verified | failed | failed |
| FCaptcha invisible risk score (lower is better) | 0.06, allowed | 0.60, blocked | not run | 0.22, allowed |
| motion-attestation and Gaitcha: mouse, typing, scroll | pass | not run | not run | caught |
Raw PyAutoGUI sends the same trusted OS-level events with straight instant moves and zero-length holds, and the hosted checks still flag it or score it riskier. The pass comes from the humanized timing, not from the events being trusted.
make test-real runs the hosted checks above and the liarjs.dev font, canvas, worker, and WebGL checks. It asserts that the humanized run passes and every named negative control fails or scores clearly worse. The offline detectors run on every make test against a local probe page, and each check also plays a robotic session that must be caught. The default suite does not depend on a third-party service.
What the implementation changes
The image combines these measures; none establishes a universal detection result:
- No CDP. Firefox doesn’t have Chrome DevTools Protocol. There’s nothing to hide because it doesn’t exist.
- Native fingerprint injection. Camoufox applies the generated fingerprint below the page JavaScript layer. The persisted configuration keeps the main context, workers, bundled Linux font aliases, and WebGL cohort aligned across restarts.
- Browser-level fingerprint behavior. Camoufox controls navigator properties below page JavaScript. Validate exposed values on the build you deploy rather than assuming a property alone proves a session is human.
- Real input events. PyAutoGUI generates OS-level mouse and keyboard events. No DOM event injection for the browser to detect, and the timing follows human motor data instead of straight lines and fixed intervals.
AI agent integration
This project ships a skill and an MCP server at /mcp/. Install and configure them explicitly using MCP and agent integrations. Do not assume every agent automatically loads a repository skill directory.
Point the agent at a running container:
export STEALTHY_AUTO_BROWSE_URL=http://localhost:8080