Human-like typing & clicks¶
Camoufox's humanize=True flag handles mouse-movement bezier curves at the C++ layer — the cursor travels along a curved path with click jitter, and every event is isTrusted=true. That defeats the cheap bot detectors. But it does not cover the behavioral tells that get accounts flagged on heavily-protected pages like Google login. This guide covers the layer Ghostvault adds on top.
Why this matters¶
The single biggest bot tell on a login form is instant paste. Playwright's default fill() writes the entire string in one input event — zero inter-keystroke delay, zero variance. No human types that way. Even keyboard.type(text, delay=N) uses a constant delay between every character, which is itself a recognizable fingerprint.
Real human typing has four properties Playwright doesn't give you out of the box:
- Variable cadence — you speed up on common bigrams ("th", "ing") and slow down on rare ones.
- Inter-keystroke variance — a long tail of slow keystrokes when you "think."
- Occasional typos + corrections — a long input with zero Backspaces is suspicious.
- Pre-click hover — real users hover for 50–500 ms before clicking; Playwright's
.click()moves and clicks in one gesture.
Ghostvault fills all four gaps at the Playwright layer. The typing model is a log-normal distribution — the standard model in HCI literature (Dhakal et al. 2018) — and the typo engine targets QWERTY-adjacent keys, exactly like a real "fat-finger" mistake.
The per-call params¶
Every interaction tool gains optional humanization parameters:
| Param | Type | Default | Purpose |
|---|---|---|---|
humanize |
bool |
false |
Type char-by-char (fill_input) or hover-then-click (click_element). |
typing_speed_wpm |
int |
90 |
Target words-per-minute (40=slow, 90=average, 150=fast). |
typing_variance |
float |
0.35 |
Cadence spread 0–1 (log-normal sigma). 0=constant, 0.35=realistic, 1=erratic. |
mistake_rate |
float |
0.0 |
Chance per char to typo-then-correct (recommended 0–0.05). Produces realistic Backspace events. |
delay_after_ms |
int |
0 |
Randomized pause after the action (0.7×–1.3× spread). |
Click tools also accept humanize (which enables the pre-click hover + position jitter) and delay_after_ms.
Per-call example: Google login¶
This is the recommended pattern for any Google or SSO login form — it's lifted directly from examples/scripts/human_like_login.py:
# Email — average speed, realistic spread, 2% typo rate
await gv.fill_input(
target="Email",
value="user@example.com",
humanize=True,
typing_speed_wpm=85,
typing_variance=0.4,
mistake_rate=0.02, # ~2% of chars get typo'd then Backspace-corrected
delay_after_ms=800, # pause before clicking Next
)
# Click "Next" with a pre-click hover
await gv.click_element(
target="Next",
humanize=True,
delay_after_ms=1500, # wait for the password page to load
)
# Password — SLOWER than email (people type passwords carefully),
# MORE variance (asterisks mask rhythm anyway), NO typos (would look weird)
await gv.fill_input(
target="Enter your password",
value="correct horse battery staple",
humanize=True,
typing_speed_wpm=65,
typing_variance=0.5,
mistake_rate=0.0, # never typo a password
delay_after_ms=600,
)
# Submit with Enter (more human than clicking the button)
await gv.press_key("Enter", delay_after_ms=2000)
Pick believable speeds
Don't set typing_speed_wpm=200 to save time — that's superhuman and is itself a tell. 85 wpm is a confident touch-typist; 65 wpm is careful password entry. For long forms where you don't know the field, 75–90 wpm with variance=0.35 is the safe default.
Why no typos on passwords?¶
The mistake engine types a QWERTY-adjacent key, pauses 200–500 ms (the "oh" moment), hits Backspace, and retypes. On an email field that looks completely natural. On a password field — where every character is masked — a Backspace event with no visible wrong character is a weird signal. Set mistake_rate=0.0 for password fields.
Per-account defaults¶
Setting these params on every call gets tedious. Lock them in per account and every subsequent call uses them unless you override:
await gv.update_account_config(
account_id,
humanize_typing=True, # all fills on this account are humanized
default_typing_wpm=85, # default speed
# humanize_clicks=True, # also default-hover on clicks
)
Now await gv.fill_input(target="Email", value="...") is humanized automatically — you only need to pass params when you want to deviate.
Resolution order (highest wins) for whether humanize is on for a given call:
1. Per-call `humanize=` argument (explicit on this call)
2. Per-account `humanize_typing`/`clicks` (set via gv_update_account_config)
3. Global GHOSTVAULT_HUMANIZE default (true)
The humanize policy: who decides¶
By default, the AI agent decides per-call whether to enable humanize, based on a decision guide in each tool's docstring. For sensitive accounts you may want to lock this down with a 3-layer policy model:
| Policy | Behavior | Use case |
|---|---|---|
off (default) |
Agent decides every call; per-account config is the fallback when the agent omits the arg. | Trusted environments, fast iteration. |
recommended |
Humanize defaults ON; the agent can turn it OFF explicitly by passing humanize=false. |
Most real-world use — a safety net without rigidity. |
always |
Humanize forced ON at the server. The agent cannot disable it, even by passing humanize=false. |
Sensitive accounts (ads, payments) where one mistake = ban. |
Resolution order (highest wins):
1. Per-account humanize_policy (set via gv_update_account_config)
2. Global GHOSTVAULT_HUMANIZE_POLICY (env var)
3. "off" (default — agent decides)
Set it globally:
Or lock it down per sensitive account (overrides global):
# Force humanize on for this account — agent can't disable it
await gv.update_account_config(account_id, humanize_policy="always")
Every tool result echoes the effective humanize value so you (and the agent) can verify what actually ran:
See Advanced → Humanize policy for the full resolution model.
How the typing engine works¶
Under the hood, humanize=True on gv_fill_input calls into src/ghostvault/humanize.py. The math, for the curious:
mean_cps = wpm * 5 / 60 # chars per second (1 word = 5 chars)
mean_delay = 1000 / mean_cps # average ms per char
mu = ln(mean_delay) - sigma^2 / 2
delay = exp(mu + sigma * z) # where z ~ N(0, 1), sigma = variance
That's a log-normal distribution. For wpm=90, variance=0.35, inter-keystroke delays center around ~130 ms with occasional 300–500 ms outliers — exactly matching observed human typing. Setting variance=0 collapses it to a constant delay (useful for tests, a tell at high precision).
The mistake engine, when mistake_rate > 0, for each character:
- With probability
mistake_rate, types a random QWERTY-adjacent key (not a random character — real typos hit physically neighboring keys). - Pauses 200–500 ms (the "oh" moment).
- Presses Backspace.
- Types the correct key.
reCAPTCHA v3 warm-up pattern¶
reCAPTCHA v3 runs invisibly and assigns a score from 0.0 to 1.0 based on behavioral signals collected from the moment the page loads. The single biggest tell is zero interaction — a page that loads and immediately submits a form is obviously a bot.
The fix is the "warm-up" pattern: scroll, read, click around, then act. This is the full sequence from examples/scripts/captcha_friendly_browsing.py:
import asyncio
await gv.open_url("https://protected-site.com")
# 1. Let the page settle — humans don't act instantly
await asyncio.sleep(3)
# 2. Scroll down in bursts with reading pauses
# (reCAPTCHA v3 weights scroll behavior heavily)
await gv.scroll(direction="down", amount=2, humanize=True, delay_after_ms=1500)
await asyncio.sleep(2)
await gv.scroll(direction="down", amount=2, humanize=True, delay_after_ms=1500)
await asyncio.sleep(2)
# 3. "Read" the content (generates a focus/engagement signal)
await gv.get_page_content(max_chars=2000)
await asyncio.sleep(2)
# 4. Scroll back up — round-trip scrolling looks more human than one-way
await gv.scroll(direction="up", amount=3, humanize=True, delay_after_ms=1200)
# 5. Take a screenshot (generates mouse activity)
await gv.screenshot()
# 6. NOW perform the target action
await gv.fill_input("query", "search term", humanize=True)
await gv.press_key("Enter")
Humanize is necessary but not sufficient
Behavioral warm-up raises your reCAPTCHA score, but it doesn't fix a flagged IP or an inconsistent fingerprint. If you're still getting challenged after warm-up, the problem is usually the IP (datacenter = instant flag) or a fingerprint that doesn't match the locale. See Advanced → Fingerprint consistency and use a residential proxy.
New tools for richer interaction¶
Beyond fill_input and click_element, two more tools round out the behavioral surface:
| Tool | Use case |
|---|---|
gv_press_key |
Submit with Enter, Tab between fields, Escape to dismiss, Ctrl+A to select all. Accepts hold_ms and delay_after_ms. |
gv_scroll |
Human-like scroll bursts with reading pauses — critical for reCAPTCHA v3. Accepts direction, amount, humanize. |
A multi-field form using all of them (condensed from examples/scripts/fill_long_form.py):
# Tab between fields like a keyboard user, not a mouse user
await gv.fill_input("First name", "Jane", humanize=True, typing_speed_wpm=95)
await gv.press_key("Tab")
await gv.fill_input("Last name", "Doe", humanize=True, typing_speed_wpm=95)
await gv.press_key("Tab")
await gv.fill_input("Email", "jane@example.com", humanize=True, mistake_rate=0.02)
# Made a mistake? Shift+Tab to go back
await gv.press_key("Shift+Tab")
await gv.fill_input("First name", "Janet", humanize=True)
await gv.click_element("Submit", humanize=True)
Troubleshooting¶
| Symptom | Fix |
|---|---|
| Typing is too slow | Lower typing_speed_wpm (higher = faster) or typing_variance. Note: wpm is words/min, so higher is faster. |
| Form submits before typing finishes | Add delay_after_ms to fill_input, or await asyncio.sleep(...) between fill and submit. |
Agent keeps passing humanize=false |
Set GHOSTVAULT_HUMANIZE_POLICY=recommended (defaults on) or always (forced on). See policy. |
| Still getting flagged after humanize | The issue is likely IP or fingerprint consistency, not typing. Use a residential proxy and match locale/timezone. See Fingerprint consistency. |
gv_press_key("Ctrl+A") does nothing |
Some pages swallow chord events. Try clicking the field first, then the chord. |
Related guides¶
- Anonymous scraping — enable
humanize=Trueon ephemeral sessions for captcha-protected targets - Reading Gmail — replying to mail: type the response human-like
- Advanced → Humanize policy — the full 3-layer resolution model