Browser Automation with Anti-Detection
Use this skill when building any browser automation system that needs to evade bot detection. Covers the core patterns: stealth Playwright setup, fingerprinting, human-like behavior, React form workarounds, and multi-module architecture.
Support files (core):
references/playwright-ubuntu-patch.md— Step-by-step for patching Playwright on unsupported distrosreferences/browser-use-setup.md— browser-use LLM agent framework: WSL setup, API surface, OpenRouter integration, MCP serverreferences/reddit-post-submission.md— Reddit CDP posting: right Chrome profile, title custom element, flair blockertemplates/bu_config.py— Reusable browser-use config: OpenRouter LLM + headless Chromium with WSL args
Related skills for extracted domains:
proxy-management— proxy pools, ProviderEmpire SID rotation, health trackingsms-verification— SMS provider evaluation, 5sim/SMSPool/sms-activate APIsemail-verification— mail.tm REST API, IMAP polling, code extractionfacebook-automation— FB Marketplace GraphQL, account creation, signup selectorsplatform-automation— Skool, Reddit anti-bot, curl_cffi TLS impersonation
Core Stack
- Playwright (Python) — primary browser automation engine
- playwright-stealth — pre-built stealth patches (webdriver, chrome.runtime, plugins)
- fake-useragent — rotating user agents per profile
- Custom
context.add_init_script()for additional fingerprint overrides - curl_cffi — TLS impersonation for headless HTTP (see
platform-automationskill)
Proxy Architecture
EXTRACTED to proxy-management skill. Load with skill_view(name='proxy-management') for:
- Sticky residential IP assignment patterns
- Provider comparison (Bright Data, IPRoyal, Oxylabs, Proxy-Cheap)
- ProxyEmpire SID rotation
- Health tracking and failure handling
- Geo consistency rules
Browser Fingerprinting
Each profile needs a unique, consistent fingerprint. The key dimensions:
context_options = {
"user_agent": unique_per_profile,
"viewport": {"width": 1920, "height": 1080}, # vary slightly
"timezone_id": "America/New_York", # match proxy geo
"locale": "en-US",
"geolocation": {"latitude": 40.7, "longitude": -74.0}, # match proxy
"permissions": ["geolocation"],
}
Additional stealth via init_script (runs before every page load):
Object.defineProperty(navigator, 'webdriver', { get: () => false });
window.chrome = { runtime: {}, loadTimes: function() {}, csi: function() {}, app: {} };
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
Launch args for Chromium:
--no-sandbox
--disable-blink-features=AutomationControlled
--disable-dev-shm-usage
--disable-features=IsolateOrigins,site-per-process
Human-Like Behavior
The difference between getting banned and staying alive is behavior simulation:
- Typing: character-by-character with random delays (30-150ms per char), occasional typos + correction (10% chance)
- Clicking: move mouse to random position within element bounds, brief hover, then click
- Scrolling: random distance (200-800px), smooth behavior
- Delays: random.uniform(0.3, 2.0) between actions; longer delays (1-3s) before navigation
- Variance: no two profiles should behave identically — vary post timing ±30min, engagement volume, scroll distances
Account Warming Protocol
New accounts need 3-4 weeks of human-like activity before aggressive posting:
Week 1: Browse only. Follow 10-20/day. Like posts. No original content.
Week 2: Reply to others. 1 post/day.
Week 3: 2 posts/day. Start using hashtags.
Week 4+: Full cadence.
Playwright on Unsupported Distros
When Playwright's host platform check rejects your OS (e.g., new Ubuntu releases), the fix is patching the bundled coreBundle.js. Three locations may need patching:
- Python venv:
venv/lib/python*/site-packages/playwright/driver/package/lib/coreBundle.js - Local npm:
node_modules/playwright-core/lib/coreBundle.js - Global npm cache:
/tmp/node_modules/playwright-core/lib/coreBundle.js
The version check logic (~line 7749) maps Ubuntu major versions to supported host platforms. When your version falls through to the default case (which returns an unrecognized platform string like ubuntu26.04-x64), the browser download fails. Patch the condition to treat your version as the closest supported one:
// BEFORE (fails on Ubuntu 26.04+):
if (major < 24)
return { hostPlatform: "ubuntu22.04" + archSuffix, ... };
if (major < 26)
return { hostPlatform: "ubuntu24.04" + archSuffix, ... };
return { hostPlatform: "ubuntu" + distroInfo.version + archSuffix, isOfficiallySupportedPlatform: false };
// AFTER (maps 24.x+ to ubuntu24.04 which has compatible binaries):
if (major < 24)
return { hostPlatform: "ubuntu22.04" + archSuffix, ... };
return { hostPlatform: "ubuntu24.04" + archSuffix, isOfficiallySupportedPlatform: false };
Then set PLAYWRIGHT_BROWSERS_PATH=./browsers and install:
PLAYWRIGHT_BROWSERS_PATH=./browsers npx playwright install chromium
Live-Platform Selector Discovery
When automating a site you haven't tested live, the CSS selectors in your code are GUESSES. The correct approach is a two-phase discovery:
Phase 1: Headless reconnaissance — Launch headless, navigate to the target page, dump all input/button attributes:
page.goto("https://target.com/signup", wait_until="networkidle")
time.sleep(3)
# Get ALL attributes from every input
inputs = page.locator("input").all()
for i in inputs:
attrs = page.evaluate('''(el) => {
const r = {};
for (const a of el.attributes) r[a.name] = a.value;
return r;
}''', i.element_handle())
print(attrs)
Phase 2: Flow discovery — Modern SPAs often hide forms behind CTAs. Click buttons, wait for modals, re-check the DOM:
# Click the CTA that opens the real form
page.locator("button:has-text('Get Started')").first.click()
time.sleep(3)
# Now check for inputs again — they may appear in a modal
For platform-specific verified selectors (Skool, Reddit, Facebook), load skill_view(name='platform-automation').
Email Verification
EXTRACTED to email-verification skill. Load skill_view(name='email-verification') for mail.tm REST API patterns, IMAP polling for Gmail/custom domains, and code/link extraction from verification emails.
Multi-Module Orchestrator Architecture
For complex automation systems (account creation → community setup → content seeding → monitoring), use this module structure:
skool_automation/
├── config.py # Pydantic models, YAML persistence, env vars
├── proxy_manager.py # Pool with sticky assignment, health tracking, provider abstraction
├── browser_manager.py # Playwright launch, fingerprinting, stealth, human-like I/O
├── [platform]_account.py # Page-specific flows (signup, login, verification)
├── [platform]_manager.py # Higher-level operations (create, configure, query)
├── content_seeder.py # Posting, replying, liking with calendar + state tracking
├── profile_store.py # SQLite CRUD for profiles, seeding state, stats
└── orchestrator.py # Single entry point tying all modules together
Each module is a class instantiated once. The orchestrator composes them. Profiles flow through the system: proxy assigned → browser launched with fingerprint → account created → community built → content seeded. SQLite tracks progress so interrupted runs can resume.
context.storage_state(path=f"profiles/{profile_id}/cookies.json")
# Restore on next launch:
context_options["storage_state"] = f"profiles/{profile_id}/cookies.json"
Use SQLite for structured profile data (email, proxy assignment, community state, seeding progress). JSON/YAML for configuration.
React Form Submission (CRITICAL PITFALL)
React-based forms often silently ignore mouse clicks on submit buttons because React uses event delegation with synthetic events. <span> overlays inside <button> elements intercept clicks, and element.click() / page.mouse.click() produce events lacking isTrusted: true.
The fix: Keyboard Tab + Enter
# Fill fields using keyboard navigation, not click+fill
page.locator("#first_name").first.click()
page.keyboard.type("James", delay=50) # delay adds human-like typing
page.keyboard.press("Tab")
page.keyboard.type("Anderson", delay=50)
page.keyboard.press("Tab")
page.keyboard.type("email@domain.com", delay=50)
page.keyboard.press("Tab")
page.keyboard.type("password", delay=50)
# Tab to the submit button and press Enter
page.keyboard.press("Tab")
time.sleep(0.5)
page.keyboard.press("Enter") # This triggers React's synthetic onSubmit
DO NOT use: page.locator("button[type='submit']").click(), click(force=True), page.evaluate("...click()"), page.mouse.click(x, y), or form.submit() — all fail silently on React.
Email Verification Code Polling
EXTRACTED to email-verification skill.
Facebook Automation
EXTRACTED to facebook-automation skill. Load skill_view(name='facebook-automation') for Marketplace GraphQL scraping (no browser), account creation with SMS verification, signup form selectors, and doc ID rotation.
Platform-Specific Patterns
EXTRACTED to platform-automation skill. Load skill_view(name='platform-automation') for Skool.com signup flow, Reddit anti-bot strategy (CDP piercing, headless detection), curl_cffi TLS impersonation, and live selector discovery techniques.
SMS Provider Evaluation & Selection
EXTRACTED to sms-verification skill. When choosing an SMS provider for automated account creation, load skill_view(name='sms-verification') for the full evaluation framework, 4-layer root cause analysis, provider catalog, and integration patterns.
ProxyEmpire SID Rotation
EXTRACTED to proxy-management skill.
Pitfalls
- Try HTTP before browser: Before reaching for Playwright or browser-use, test if the target site's login is a simple form POST. Use
curl_cffiwith TLS impersonation for sites that don't require JavaScript for their login flow. It's 100x lighter and avoids headless-detection entirely. Check withre.findall(r'<form', html)— if no forms exist in raw HTML, the login is JS-rendered and you need a real browser. - Headless detection by target sites: Some sites (Reddit, Cloudflare-protected) detect and block headless Chromium even with stealth flags. Always confirm it's not an IP block first:
curl -sI https://target.com. If curl gets 200/301 but headless Chromium gets blocked, the block is fingerprint-level. Try--headless=newflag, a residential proxy, or fall back tocurl_cffiif no JS needed. networkidlehangs on certain sites: Reddit and other SPA-heavy sites maintain constant websocket/polling connections, sowait_until="networkidle"never resolves. Usewait_until="domcontentloaded"orwait_until="commit"with a fixed wait instead.- Consistent fingerprints matter more than perfect ones: A profile that changes user agents or screen sizes between sessions triggers more flags than a mildly suspicious but consistent fingerprint.
- Rate limiting: Don't create accounts or communities back-to-back. Add 10-30 second cooldowns with jitter between operations.
- Don't over-automate the platform itself: Bot the funnel (YouTube, X) to drive real users. Inside the community, real members should drive engagement. Amplifier profiles are for launch seeding only — taper them off as real members join.