humanization-2026
npx skills add https://github.com/krishamaze/skills --skill humanization-2026
Agent 安装分布
Skill 文档
Humanization 2026 â Full Reference
[!CAUTION] This skill documents legitimate HCI research (Bézier curves, Fitts’s Law, Gaussian keystroke dynamics) for use in authorized browser automation and accessibility testing. Do not use these techniques to circumvent terms of service, impersonate real users, or conduct unauthorized automated access. You are responsible for ensuring your use complies with applicable laws and platform policies.
Why This Skill Exists
In 2026, anti-bot systems use ML behavioral biometric analysis â not just fingerprinting. They analyze: mouse trajectory curvature, velocity profiles, keystroke dwell+flight time distributions, scroll rhythm, idle behavior, and inter-action timing entropy.
Camoufox’s humanize=True handles C++-level fingerprint patching.
It does NOT handle behavioral patterns â that is entirely your code’s responsibility.
See references/detection-signals.md for what ML detectors measure.
See references/math.md for algorithm derivations.
Layer 1: Mouse Movement
The Science
Humans move mice via two-phase motor control (Woodworth, 1899):
- Initial ballistic phase â fast, distance-driven, imprecise
- Final homing phase â slow, target-size-driven, precise
Fitts’s Law predicts movement time: MT = a + b à logâ(D/W + 1)
- D = distance to target, W = target width
- Large buttons nearby â fast. Small buttons far away â slow.
- Detectors verify that your timing matches the D/W ratio of each element.
Algorithm: Cubic Bézier + Jitter + Overshoot
import asyncio
import random
import math
import numpy as np
def _bezier_curve(start: tuple, end: tuple, num_points: int = 50) -> list[tuple]:
"""
Cubic Bézier curve between two points with randomized control points.
Control points are placed on ONE side of the line (not both) â humans
don't make S-curves; they arc to one side.
"""
x1, y1 = start
x2, y2 = end
# Midpoint
mx, my = (x1 + x2) / 2, (y1 + y2) / 2
# Perpendicular offset â control points arc to one side
dx, dy = x2 - x1, y2 - y1
dist = math.hypot(dx, dy)
if dist < 1:
return [start, end]
# Perpendicular direction
px, py = -dy / dist, dx / dist
# Arc magnitude: 10-25% of distance, always same side (not random side per point)
arc_side = random.choice([-1, 1]) # chosen once per movement
arc1 = arc_side * random.uniform(0.1, 0.25) * dist
arc2 = arc_side * random.uniform(0.05, 0.2) * dist
# Two control points
cp1 = (x1 + dx * 0.3 + px * arc1, y1 + dy * 0.3 + py * arc1)
cp2 = (x1 + dx * 0.7 + px * arc2, y1 + dy * 0.7 + py * arc2)
# Generate curve points
points = []
for i in range(num_points):
t = i / (num_points - 1)
# Cubic Bézier formula
bx = (1-t)**3 * x1 + 3*(1-t)**2*t * cp1[0] + 3*(1-t)*t**2 * cp2[0] + t**3 * x2
by = (1-t)**3 * y1 + 3*(1-t)**2*t * cp1[1] + 3*(1-t)*t**2 * cp2[1] + t**3 * y2
points.append((bx, by))
return points
def _fitts_duration(dist: float, target_w: float) -> float:
"""
Fitts's Law: MT = a + b à logâ(D/W + 1)
Returns movement duration in seconds.
Constants calibrated for mouse (~200-800ms range).
"""
a = 0.05 # motor initiation constant
b = 0.12 # motor control constant
id_ = math.log2(dist / max(target_w, 10) + 1)
mt = a + b * id_
# Add human variance (±15%)
return mt * random.uniform(0.85, 1.15)
def _apply_jitter(points: list[tuple], sigma: float = 0.5) -> list[tuple]:
"""
Add Gaussian micro-jitter to path â simulates natural hand tremor.
μ=0, Ï=0.5 pixels: subtle, statistically identical to human data.
"""
return [
(x + random.gauss(0, sigma), y + random.gauss(0, sigma))
for x, y in points
]
async def human_move(page, target_x: float, target_y: float,
target_w: float = 20, overshoot: bool = True):
"""
Move mouse to target with:
- Cubic Bézier curved path
- Fitts's Law timing
- Gaussian jitter
- Overshoot + correction (for distant targets)
"""
# Get current position
curr = await page.evaluate("() => ({x: window.mouseX || 0, y: window.mouseY || 0})")
start = (curr.get("x", 0), curr.get("y", 0))
end = (target_x, target_y)
dist = math.hypot(end[0] - start[0], end[1] - start[1])
duration = _fitts_duration(dist, target_w)
# Overshoot for distant targets (>300px) â humans overshoot then correct
if overshoot and dist > 300:
# Overshoot 3-8% past target
overshoot_factor = random.uniform(1.03, 1.08)
ox = end[0] + (end[0] - start[0]) * (overshoot_factor - 1)
oy = end[1] + (end[1] - start[1]) * (overshoot_factor - 1)
# Phase 1: move to overshoot point
path1 = _bezier_curve(start, (ox, oy), num_points=40)
path1 = _apply_jitter(path1)
step_time = duration * 0.7 / len(path1)
for x, y in path1:
await page.mouse.move(x, y)
await asyncio.sleep(step_time)
# Phase 2: correct back to target (shorter, slower)
path2 = _bezier_curve((ox, oy), end, num_points=15)
path2 = _apply_jitter(path2, sigma=0.3)
step_time = duration * 0.3 / len(path2)
for x, y in path2:
await page.mouse.move(x, y)
await asyncio.sleep(step_time)
else:
# Straight Bézier move
num_points = max(20, int(dist / 10))
path = _bezier_curve(start, end, num_points=num_points)
path = _apply_jitter(path)
# Non-uniform timing: accelerate then decelerate (ease-in-out)
for i, (x, y) in enumerate(path):
await page.mouse.move(x, y)
t = i / len(path)
# Ease-in-out timing: slower at start and end
speed_factor = 1 - abs(2 * t - 1) ** 2 * 0.5
await asyncio.sleep((duration / len(path)) / speed_factor)
# Hover pause before click (humans aim before clicking)
await asyncio.sleep(random.uniform(0.05, 0.18))
Layer 2: Clicking
async def human_click(page, selector: str, button: str = "left"):
"""
Click with:
- Random point within element bounding box (not center)
- Mouse move to element first
- Variable mousedown â mouseup duration
"""
elem = page.locator(selector)
await elem.wait_for(state="visible")
box = await elem.bounding_box()
if not box:
await elem.click()
return
# Random point in bounding box (±30% from center)
cx = box["x"] + box["width"] * random.uniform(0.3, 0.7)
cy = box["y"] + box["height"] * random.uniform(0.3, 0.7)
await human_move(page, cx, cy, target_w=box["width"])
# Variable press duration (50-180ms) â humans don't click instantly
await page.mouse.down(button=button)
await asyncio.sleep(random.uniform(0.05, 0.18))
await page.mouse.up(button=button)
# Post-click micro-pause
await asyncio.sleep(random.uniform(0.08, 0.25))
Layer 3: Keystroke Dynamics
The Science
Anti-bot systems measure two metrics per keystroke pair:
- Dwell time (DT): duration key is held down (key_press â key_release)
- Flight time (FT): time between key_release and next key_press
Human distributions follow Gaussian (normal) patterns:
- Dwell: μ â 80ms, Ï â 20ms
- Flight: μ â 120ms, Ï â 40ms
- Familiar character sequences (common digraphs) are 20-40% faster
Mechanical bots: perfectly uniform timing â instantly flagged. Uniform random: wrong distribution shape â flagged. Gaussian: matches real human data.
async def human_type(page, selector: str, text: str,
wpm: float = 65, error_rate: float = 0.02):
"""
Type text with:
- Gaussian dwell + flight time per character
- Digraph speed boost for common pairs
- Realistic typo + correction simulation
- Pre-type focus behavior
"""
# Common fast digraphs (muscle memory â 30% faster)
fast_digraphs = {
"th", "he", "in", "er", "an", "re", "on", "en",
"at", "es", "or", "te", "of", "ed", "is", "it",
"al", "ar", "st", "to", "nt", "ng", "se", "ha",
}
# Base timing from WPM (1 word = 5 chars)
base_flight = 60 / (wpm * 5) * 1000 # ms per character
elem = page.locator(selector)
await elem.click()
await asyncio.sleep(random.uniform(0.2, 0.5)) # focus pause
i = 0
while i < len(text):
char = text[i]
# Typo simulation
if random.random() < error_rate and char.isalpha():
typo_char = random.choice("qwertyuiop")
# Type wrong char
await page.keyboard.down(typo_char)
dwell = random.gauss(80, 20)
await asyncio.sleep(max(30, dwell) / 1000)
await page.keyboard.up(typo_char)
# Pause (noticing error)
await asyncio.sleep(random.uniform(0.15, 0.5))
# Backspace
await page.keyboard.press("Backspace")
await asyncio.sleep(random.uniform(0.08, 0.2))
# Digraph speed boost
if i > 0:
digraph = text[i-1:i+1].lower()
speed_mult = 0.7 if digraph in fast_digraphs else 1.0
else:
speed_mult = 1.0
# Press key
await page.keyboard.down(char)
# Dwell time: Gaussian μ=80ms Ï=20ms
dwell = random.gauss(80, 20) * speed_mult
await asyncio.sleep(max(30, dwell) / 1000)
await page.keyboard.up(char)
# Flight time: Gaussian μ=base_flight Ï=base_flight*0.35
flight = random.gauss(base_flight, base_flight * 0.35) * speed_mult
await asyncio.sleep(max(20, flight) / 1000)
i += 1
# Post-type pause (reviewing what was typed)
await asyncio.sleep(random.uniform(0.3, 0.8))
Layer 4: Scrolling
async def human_scroll(page, direction: str = "down",
distance: int = 400, read_pause: bool = True):
"""
Scroll with:
- Variable speed (not constant)
- Micro-pauses (reading behavior)
- Occasional scroll reversal (re-reading)
- Jitter in scroll amount per step
"""
steps = random.randint(4, 8)
total = 0
sign = 1 if direction == "down" else -1
for i in range(steps):
# Variable scroll amount per step with jitter
step_dist = (distance / steps) * random.uniform(0.7, 1.3)
await page.evaluate(f"window.scrollBy(0, {int(sign * step_dist)})")
total += step_dist
# Pause between scroll steps (reading rhythm)
await asyncio.sleep(random.uniform(0.08, 0.25))
# Occasional scroll-back (re-reading â very human)
if read_pause and random.random() < 0.2:
back = random.uniform(50, 150)
await asyncio.sleep(random.uniform(0.3, 0.8))
await page.evaluate(f"window.scrollBy(0, {int(-sign * back)})")
await asyncio.sleep(random.uniform(0.4, 1.2))
# Reading pause after scroll
if read_pause:
await asyncio.sleep(random.uniform(0.8, 3.0))
Layer 5: Session-Level Behavior
What ML Detectors Measure at Session Level
- Action entropy: humans vary their actions; bots repeat identical sequences
- Inter-action timing: humans have irregular pacing; bots are metronomic
- Idle periods: humans pause to read, think, distract; bots don’t idle
- Navigation patterns: humans backtrack, re-read; bots move linearly
- Session duration: too short = suspicious; too regular = suspicious
import random
import asyncio
async def session_pause(context: str = "thinking"):
"""
Context-aware pauses that match human cognitive patterns.
Context: 'thinking' | 'reading' | 'loading' | 'distracted'
"""
pauses = {
"thinking": (0.5, 2.5), # deciding what to do next
"reading": (1.5, 6.0), # reading content
"loading": (0.3, 1.0), # waiting for page response
"distracted": (3.0, 12.0), # tab switching, phone check
}
lo, hi = pauses.get(context, (0.5, 2.0))
await asyncio.sleep(random.uniform(lo, hi))
async def session_warmup(page):
"""
Warmup behavior after page load before taking action.
Simulates: page scan, scroll to orient, mouse wander.
DO NOT skip â jumping straight to target element is a strong bot signal.
"""
# Initial scan pause
await asyncio.sleep(random.uniform(0.8, 2.0))
# Light scroll to scan content
scroll_amt = random.randint(100, 300)
await human_scroll(page, "down", scroll_amt, read_pause=False)
await asyncio.sleep(random.uniform(0.4, 1.2))
# Random mouse wander (looking around)
vp = await page.evaluate("() => ({w: window.innerWidth, h: window.innerHeight})")
for _ in range(random.randint(1, 3)):
rx = random.uniform(0.2, 0.8) * vp["w"]
ry = random.uniform(0.2, 0.8) * vp["h"]
await human_move(page, rx, ry, target_w=50, overshoot=False)
await asyncio.sleep(random.uniform(0.3, 1.0))
async def random_idle_action(page):
"""
Occasionally inject idle-like micro-behaviors between real actions.
Call this randomly during long sessions (20% chance per major action).
"""
action = random.choice([
"scroll_small", "mouse_wander", "pause_long"
])
if action == "scroll_small":
d = random.choice(["up", "down"])
amt = random.randint(30, 100)
await human_scroll(page, d, amt, read_pause=False)
elif action == "mouse_wander":
vp = await page.evaluate("() => ({w: window.innerWidth, h: window.innerHeight})")
rx = random.uniform(0.1, 0.9) * vp["w"]
ry = random.uniform(0.1, 0.9) * vp["h"]
await human_move(page, rx, ry, overshoot=False)
elif action == "pause_long":
await session_pause("distracted")
Layer 6: Complete Interaction Pattern
This is the correct order for any interaction:
async def interact_with_form(page, form_data: dict):
"""
Example: full humanized form submission flow.
Every step uses the layers above.
Replace selectors with your application's actual elements.
"""
# 1. Warmup after navigation
await session_warmup(page)
# 2. Find target element â scroll to locate if needed
target_btn = page.get_by_role("button", name="Submit")
await session_pause("thinking")
# 3. Move + click with full humanization
box = await target_btn.bounding_box()
await human_move(page, box["x"] + box["width"]/2, box["y"] + box["height"]/2,
target_w=box["width"])
await asyncio.sleep(random.uniform(0.05, 0.15))
await target_btn.click()
# 4. Pause after interaction (reading response)
await session_pause("reading")
# 5. Type into input with keystroke dynamics
text_input = page.get_by_placeholder("Enter your text")
await human_type(page, text_input, form_data.get("text", ""), wpm=55)
# 6. Review pause (re-reading what was typed)
await session_pause("reading")
# 7. Occasional idle action before submitting
if random.random() < 0.3:
await random_idle_action(page)
# 8. Submit â thinking pause before committing
await session_pause("thinking")
submit_btn = page.get_by_role("button", name="Confirm")
box2 = await submit_btn.bounding_box()
await human_move(page, box2["x"] + box2["width"]/2, box2["y"] + box2["height"]/2,
target_w=box2["width"])
await asyncio.sleep(random.uniform(0.1, 0.3))
await submit_btn.click()
# 9. Post-action pause (watching result)
await session_pause("loading")
What NOT to Do
# â Instant teleport â strongest bot signal
await page.mouse.move(500, 300)
# â Uniform timing â wrong statistical distribution
for char in text:
await page.keyboard.type(char)
await asyncio.sleep(0.1) # constant = detected
# â Click center every time
await elem.click() # Playwright defaults to center â real humans don't
# â time.sleep() in async code
import time; time.sleep(1) # blocks event loop
# â No warmup â jumping straight to target
await page.goto(url)
await page.get_by_role("button", name="Submit").click() # immediate = bot
# â Identical sessions â same sequence every run
# Vary your scroll amounts, pause durations, idle behaviors each run
# â random.uniform() for all timing â flat distribution â human
# Use random.gauss() for timing â humans have Gaussian timing, not uniform
Dependencies
uv add numpy # only for advanced jitter (can replace with random.gauss)
No external humanization library needed â this skill IS the implementation.
Read Next
references/detection-signals.mdâ what 2026 ML detectors measure (know your enemy)references/math.mdâ Bézier, Fitts’s Law, and Gaussian derivations