Skip to content

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:

  1. Variable cadence — you speed up on common bigrams ("th", "ing") and slow down on rare ones.
  2. Inter-keystroke variance — a long tail of slow keystrokes when you "think."
  3. Occasional typos + corrections — a long input with zero Backspaces is suspicious.
  4. 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 00.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:

# In .env or your client's env block
GHOSTVAULT_HUMANIZE_POLICY=recommended

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:

{"filled": "Email", "humanize": true, "typing_speed_wpm": 85}

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:

  1. With probability mistake_rate, types a random QWERTY-adjacent key (not a random character — real typos hit physically neighboring keys).
  2. Pauses 200–500 ms (the "oh" moment).
  3. Presses Backspace.
  4. 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.