docker-stealthy-auto-browse: Der Browser, Der Nicht Weiß, Dass Er Automatisiert Wird

Ich automatisiere seit Jahren Browser. Selenium, Puppeteer, Playwright, alle benutzt, alle auffliegen sehen. Das Wettrüsten zwischen Bot-Erkennung und Browser-Automatisierung läuft seit den Anfängen des Scrapings, und rate mal, wer verliert? Ausnahmslos jedes Chromium-basierte Automatisierungswerkzeug auf diesem verfluchten Planeten.
Das Problem sind nicht die Werkzeuge. Playwright ist gute Software. Puppeteer funktioniert prima. Das Problem ist Chrome DevTools Protocol, der Mechanismus, über den sie alle mit dem Browser reden. CDP ist die Art, wie dein Automatisierungsframework sagt “klick diesen Button” oder “tipp in dieses Feld”. Es ist auch die Art, wie Cloudflare, DataDome, PerimeterX und jeder andere Bot-Erkennungsdienst der Erde wissen, dass du kein Mensch bist. Du kannst Stealth-Plugins installieren, navigator.webdriver patchen, Fingerabdrücke fälschen, bis dir die Augen bluten, CDP ist immer noch da, und sie werden es finden.
Also habe ich docker-stealthy-auto-browse gebaut. Damit das klar ist, ich habe von dem Stealth-Scheiß nichts erfunden. Die Schwerstarbeit machen Camoufox, Playwright, PyAutoGUI und browserforge. Das sind brillante Projekte, gebaut von Leuten, die klüger sind als ich. Was ich getan habe: den ganzen Scheiß genommen, in einem Docker-Container zusammengelötet und eine HTTP-API draufgeklatscht, damit du das ganze Ding aus der Ferne mit curl-Kommandos steuern kannst. Ein Container, ein Endpoint, null Setup-Gefrickel.

Das Grundproblem Aller Anderen Ansätze

Das hier macht jedes Chromium-basierte Automatisierungswerkzeug: es öffnet Chrome, verbindet sich per CDP und schickt Kommandos durch dieses Protokoll. Der Browser weiß es. Das JavaScript, das auf der Seite läuft, weiß es. Das Skript des Bot-Erkennungsdienstes, das vor deinem Seiteninhalt geladen wurde? Das weiß es garantiert.
Du kannst versuchen, es zu verstecken:

  • navigator.webdriver patchen, damit es false liefert, die Detektoren prüfen, ob gepatcht wurde
  • Stealth-Plugins installieren, die Detektoren suchen nach den Nebenwirkungen dieser Plugins
  • Fingerabdrücke fälschen, die Detektoren vergleichen den Fingerabdruck des Hauptkontexts mit dem der Web Worker und finden Widersprüche
  • Headless-Modus nutzen, die Detektoren suchen nach Headless-Signalen

Es ist ein Katz-und-Maus-Spiel, in dem die Katze jeden Vorteil hat. CDP hinterlässt überall Spuren, in der JavaScript-Laufzeit, in der Art, wie Events verteilt werden, in Timing-Mustern, im internen Zustand des Browsers. Du versuchst so zu tun, als wäre eine Marionette keine Marionette, während die Fäden gut sichtbar hängen.

Der Ansatz: Überhaupt Keine Fäden

docker-stealthy-auto-browse versteckt keine Automatisierungssignale. Es beseitigt sie komplett.
Camoufox statt Chromium. Ein maßgeschneiderter Firefox-Fork. Es gibt kein Chrome DevTools Protocol, weil Firefox es nicht benutzt. Bot-Detektoren, die nach CDP-Signalen suchen, finden nichts, nicht weil wir sie versteckt hätten, sondern weil es sie nicht gibt. navigator.webdriver ist false, nicht auf false gepatcht, sondern echt falsch, weil Camoufox es gar nicht erst setzt.
Playwright für die Browsersteuerung. Es übernimmt das DOM-Niveau, Navigation, Elementauswahl, Seiteninspektion. Der bequeme, aber erkennbare Eingabemodus läuft über Playwright. Zusammen mit Camoufox leckt es die üblichen CDP-Automatisierungssignale nicht, die Chromium-basierte Aufbauten lecken.
PyAutoGUI statt DOM-Events. Wenn du Stealth brauchst, bewegt sich die Maus physisch über den virtuellen Bildschirm, mit menschlichen Kurven, zufälligem Zittern und weich beschleunigt. Wenn du tippst, werden echte Tastenanschläge auf Betriebssystemebene erzeugt, mit zufälligen Abständen zwischen den Zeichen. Der Browser bekommt sie als echte Nutzereingabe. Kein JavaScript der Welt kann PyAutoGUI-Eingaben von einem echten Menschen an der Tastatur unterscheiden.
Echte Fingerabdrücke über browserforge. Der Fingerabdruck wird einmal erzeugt und konsistent auf Hauptkontext und Web Worker angewandt. Kein Fälschen heißt keine Widersprüche, ein verbreiteter Erkennungsvektor, der die meisten Fingerprint-Fälscher erwischt.
Xvfb für ein echtes Display. Der Browser läuft mit vollem grafischem Display im Container, über einen virtuellen Framebuffer. Kein Headless-Modus, keine Headless-Signale. Für den Browser und jedes Erkennungsskript läuft das Ganze auf einem normalen Desktop.
Mein Beitrag ist der Kleber: eine HTTP-API in Python, die all das verdrahtet, der Docker-Container, der alles in ein einziges docker run packt, das Page-Loader-System für URL-getriggerte Automatisierung, die Abstraktion der beiden Eingabemodi (system und playwright) und die noVNC-Einbindung zum Livezuschauen. Die Stealth-Technik ist die Genialität anderer Leute. Verpackung und API sind von mir.

Wie Es Funktioniert

Du startest den Container, er stellt eine HTTP-API auf Port 8080 bereit. Du schickst JSON-Kommandos, du bekommst JSON-Antworten. Das ist die ganze Schnittstelle.

docker run -d --name browser 
  -p 8080:8080 
  -p 5900:5900 
  psyb0t/stealthy-auto-browse

Port 8080 ist die API. Port 5900 ist ein noVNC-Viewer, damit du dem Browser in Echtzeit aus deinem eigenen Browser zusehen kannst: du öffnest http://localhost:5900/ und siehst genau das, was der automatisierte Browser sieht.
Navigier irgendwohin:

curl -X POST https://ciprian.51k.eu80 
  -H "Content-Type: application/json" 
  -d '{"action": "goto", "url": "https://example.com"}'

Seit v2.6.0 nehmen goto (und refresh und new_tab) optionale Navigationsregler pro Aufruf: timeout in Sekunden (Standard 30), retry_count für begrenzte Wiederholungen, wenn ein Laden ins Timeout läuft (Standard 1), und retry_delay zwischen diesen Wiederholungen (Standard 1s). Dieselben drei Regler, egal ob du es über HTTP aufrufst, über das MCP-Tool oder als Schritt innerhalb eines run_script.
Lies die Seite:

curl -X POST https://ciprian.51k.eu80 
  -H "Content-Type: application/json" 
  -d '{"action": "get_text"}'

Finde alles Anklickbare auf der Seite:

curl -X POST https://ciprian.51k.eu80 
  -H "Content-Type: application/json" 
  -d '{"action": "get_interactive_elements"}'

Das liefert dir jeden Button, Link und jedes Eingabefeld mit Viewport-Koordinaten, Text und CSS-Selektoren. Jetzt klick eins davon mit einer echten Mausbewegung an:

curl -X POST https://ciprian.51k.eu80 
  -H "Content-Type: application/json" 
  -d '{"action": "system_click", "x": 500, "y": 300}'

Tipp mit echten Tastenanschlägen:

curl -X POST https://ciprian.51k.eu80 
  -H "Content-Type: application/json" 
  -d '{"action": "system_type", "text": "hello world"}'

Mach einen Screenshot:

curl https://ciprian.51k.eu80/screenshot/browser?whLargest=512 -o screenshot.png

Fahr mehrstufige Skripte in einer einzigen Anfrage mit run_script, ohne pro Aktion ein curl zu schicken:

curl -X POST https://ciprian.51k.eu80 
  -H "Content-Type: application/json" 
  -d '{
    "action": "run_script",
    "steps": [
      {"action": "goto", "url": "https://example.com", "wait_until": "domcontentloaded"},
      {"action": "sleep", "duration": 2},
      {"action": "get_text", "output_id": "text"},
      {"action": "eval", "expression": "document.title", "output_id": "title"}
    ]
  }'

Nimmt auch "yaml": "...", im selben Format wie im Skriptmodus. Im Einzelinstanz-Modus werden Anfragen automatisch serialisiert, schick mehrere Skripte parallel und sie stellen sich an, statt sich gegenseitig umzurennen.

Zwei Eingabemodi, Und Das Ist Wichtig

Der Container gibt dir zwei Wege, mit Seiten umzugehen, und den richtigen zu wählen ist der Unterschied zwischen Durchkommen und Blockiertwerden.

Systemeingabe: Nicht Erkennbar

system_click, mouse_move, system_type, send_key, scroll, die nutzen alle PyAutoGUI, um echte Events auf Betriebssystemebene zu erzeugen. Die Maus bewegt sich in menschlichen Kurven. Tastenanschläge haben zufälliges Timing. Der Browser hat keinerlei Möglichkeit zu wissen, dass die nicht von einer echten Person kommen.
Du arbeitest mit Viewport-Koordinaten, die holst du dir aus get_interactive_elements.

Playwright-Eingabe: Erkennbar, Aber Bequem

click, fill, type, die nutzen Playwrights DOM-Automatisierung mit CSS-Selektoren oder XPath. Schneller, einfacher, keine Koordinatenrechnerei. Aber die Muster der Event-Injektion sind theoretisch von einer ernsthaften Verhaltensanalyse erkennbar.
Die Regel ist simpel: hat die Seite Bot-Erkennung? Systemeingabe. Immer. Du scrapest nur etwas, das sich nicht wehrt? Playwright-Eingabe reicht.

Ein Echter Login-Ablauf

So sieht ein nicht erkennbarer Login aus, jede Interaktion läuft über Eingaben auf Betriebssystemebene:

API=https://ciprian.51k.eu80
# Navigate to login
curl -X POST $API -H 'Content-Type: application/json' 
  -d '{"action": "goto", "url": "https://example.com/login"}'
# Find all interactive elements
curl -X POST $API -H 'Content-Type: application/json' 
  -d '{"action": "get_interactive_elements"}'
# Click the email field (coordinates from above)
curl -X POST $API -H 'Content-Type: application/json' 
  -d '{"action": "system_click", "x": 400, "y": 200}'
# Type email with human-like keystrokes
curl -X POST $API -H 'Content-Type: application/json' 
  -d '{"action": "system_type", "text": "[email protected]"}'
# Tab to password field
curl -X POST $API -H 'Content-Type: application/json' 
  -d '{"action": "send_key", "key": "tab"}'
# Type password
curl -X POST $API -H 'Content-Type: application/json' 
  -d '{"action": "system_type", "text": "secretpassword"}'
# Submit
curl -X POST $API -H 'Content-Type: application/json' 
  -d '{"action": "send_key", "key": "enter"}'
# Wait for redirect
curl -X POST $API -H 'Content-Type: application/json' 
  -d '{"action": "wait_for_url", "url": "**/dashboard", "timeout": 15}'

Die Seite sieht einen echten Menschen, der in natürlichem Tempo mit zufälligen Verzögerungen tippt. Keine CDP-Signale. Keine Automatisierungs-Fingerabdrücke. Nichts.

Page Loader: Automatisierung auf Autopilot

Page Loader sind wie Greasemonkey-Userscripts, nur für die HTTP-API. Du schreibst eine YAML-Datei, die sagt “immer wenn der Browser diese Domain besucht, führ diese Schritte automatisch aus”. Du mountest sie in den Container und vergisst sie.

# loaders/news_site.yaml
name: News Site Cleanup
match:
  domain: news-site.com
steps:
  - action: goto
    url: "${url}"
    wait_until: networkidle
  - action: wait_for_element
    selector: "article"
    timeout: 10
  - action: eval
    expression: "document.querySelector('.cookie-consent')?.remove()"
  - action: eval
    expression: "document.querySelector('.newsletter-overlay')?.remove()"
  - action: scroll_to_bottom
    delay: 0.3

Jetzt wartet jedes goto auf news-site.com automatisch auf den Inhalt, killt das Cookie-Popup, killt das Newsletter-Modal und scrollt, um faul geladene Bilder auszulösen. Schluss damit, nach jeder Navigation 5 Kommandos zu schicken.

Die Komplette API

Die HTTP-API deckt alles ab, was du bräuchtest:

  • Navigation: goto, refresh, mit konfigurierbaren Wartebedingungen
  • Systemeingabe: system_click, mouse_move, system_type, send_key, scroll, alles auf Betriebssystemebene, alles nicht erkennbar
  • Seiteninspektion: get_interactive_elements, get_text, get_html, eval
  • Wartebedingungen: wait_for_element, wait_for_text, wait_for_url, wait_for_network_idle, weil sleep etwas für Amateure ist
  • Tab-Verwaltung: list_tabs, new_tab, switch_tab, close_tab
  • Cookies und Storage: volles CRUD für Cookies, localStorage, sessionStorage
  • Downloads und Uploads: Dateidownloads und programmatische Datei-Inputs handhaben
  • Netzwerk-Logging: alle HTTP-Anfragen der Seite aufzeichnen, API-Endpoints finden, debuggen, verifizieren
  • Screenshots: Browser-Viewport oder der ganze Desktop, mit Größenparametern
  • Dialoge: automatisches Bestätigen oder konfigurierte Antworten auf alert, confirm und prompt
  • Bildschirmaufnahme: start_recording, stop_recording, recording_status, MP4 der tatsächlich gerenderten Pixel

Zwei Screenshot-Endpoints geben dir den Browser-Viewport (wie die Seite aussieht) oder den ganzen virtuellen Desktop (Browser-Oberfläche inklusive). Beide unterstützen Größenparameter, damit du nicht jedes Mal PNGs in 1920×1080 herunterlädst:

# Resize longest side to 512px
curl https://ciprian.51k.eu80/screenshot/browser?whLargest=512 -o shot.png
# Full desktop including browser chrome
curl https://ciprian.51k.eu80/screenshot/desktop?whLargest=512 -o desktop.png

Virtuelle Kamera und Mikrofon

Du legst eine Video- und/oder Audiodatei in ein gemountetes /media-Verzeichnis, und Seiten bekommen Kamera- und Mikrofonspuren aus diesen Dateien, über navigator.mediaDevices.getUserMedia(). Die Pfade werden beim Start validiert, Symlinks aufgelöst, damit alles im konfigurierten Medienverzeichnis bleibt. Frag eine Art an, die nicht konfiguriert ist, und die Anfrage scheitert, sie fällt nicht still auf ein echtes Gerät zurück, was genau das Verhalten ist, das du willst, wenn der ganze Sinn darin besteht, dass es kein echtes Gerät gibt.
Statische Dateien waren die erste Version. Setz VIRTUAL_MEDIA_DYNAMIC=true und du kannst die Quelle zur Laufzeit tauschen: set_virtual_media_source wählt eine vorhandene enthaltene Datei, upload_virtual_media nimmt eine begrenzte base64-Nutzlast. Beide erhalten die Identitäten der Kamera- und Mikrofonspuren, die eine Seite bereits bekommen hat, eine Seite kann also ändern, was sie sieht, ohne getUserMedia() neu anzufragen und ohne zu merken, dass etwas passiert ist.
Uploads sind kein Loch: kollisionssicher generierte Dateinamen statt Überschreiben einer benannten Quelle, strikte base64-Dekodierung, eine konfigurierbare Obergrenze VIRTUAL_MEDIA_UPLOAD_MAX_BYTES (Standard 50 MiB) und eine ffprobe-Prüfung auf den angeforderten Stream, bevor irgendetwas gespeichert oder aktiviert wird. Die Quellenauswahl nimmt nur reguläre Dateien an, die in VIRTUAL_MEDIA_DIR liegen, keine entfernten URLs, keine WebSocket-Feeds, keine beliebigen Hostpfade, keinen anderen lebenden Eingang. get_virtual_media_state meldet den Zustand des dynamischen Modus, den Basisnamen der aktiven Quelle und einen Revisionszähler, ohne Quellpfade oder hochgeladene Bytes preiszugeben.

Scrapen Ohne JavaScript Zu Schreiben

Vier Aktionen, die abdecken, was du sonst jedes Mal von Hand in einen evaluate-Aufruf schreiben würdest: get_page_info, get_element, get_elements und get_computed_style. Seiten- und CSS-Daten direkt heraus, kein eigenes JS, kein Anführungszeichen-Albtraum, um es durch JSON zu bekommen. get_elements liefert jetzt standardmäßig 20 Ergebnisse, und zwar einheitlich, in der HTTP-API, in der MCP-Dokumentation und in der Test-Fixture, was nicht immer der Fall war.

Bildschirmaufnahme

Screenshots sagen dir, wie eine Seite aussah. Sie sagen dir nicht, was passiert ist. Also nimmt der Browser sich jetzt selbst auf: ffmpeg mit x11grab gegen das Xvfb-Display, das MP4 in ein gemountetes /recordings-Volume schreibt. Die tatsächlich gerenderten Pixel, inklusive des Mauszeigers auf Betriebssystemebene, der herumfährt, denn in diesem Ding ist der Zeiger echt.
Drei Modi: window nimmt das ganze Camoufox-Fenster, viewport schneidet die Browser-Oberfläche anhand der kalibrierten mozInnerScreenX/Y-Offsets weg (keine hartkodierten Schätzungen), und desktop nimmt den ganzen Xvfb-Bildschirm.

mkdir -p ./recordings
docker run -d -p 8080:8080 -v ./recordings:/recordings psyb0t/stealthy-auto-browse
# start
curl -X POST https://ciprian.51k.eu80/action 
  -d '{"action": "start_recording", "mode": "viewport", "fps": 20}'
# ... drive the browser ...
# stop — you name the file at stop time, after you know how it went
curl -X POST https://ciprian.51k.eu80/action 
  -d '{"action": "stop_recording", "slug": "my-flow"}'
# → ./recordings/my-flow.mp4

Der Slug wird absichtlich beim Stoppen mitgegeben, du benennst die Aufnahme nach dem Lauf, wenn du tatsächlich weißt, ob es login-success oder login-broke-again ist. Slugs werden gegen Pfad-Traversal bereinigt, und Kollisionen benennen sich selbst um. Eine aktive Aufnahme pro Container. Absturzsicher: sauberes Herunterfahren bei SIGINT plus ein Startdurchlauf nach verwaisten Temp-Dateien.
show_cursor ist standardmäßig true, aber schalt es aus, wenn du visuelle Regressionsaufnahmen machst und Zeigerpixel den Diff vergiften würden. Der Antwort-Deskriptor gibt das Flag zurück, damit Aufrufer bestätigen können, was sie bekommen haben.
Läuft über die HTTP-API und über MCP. Im Clustermodus müssen Start und Stopp im selben run_script-Aufruf liegen, damit beide dieselbe Instanz treffen, sonst sagst du einem Container, er soll eine Aufnahme stoppen, die ein anderer Container gestartet hat.
Auflösung. Die Aufnahme ging über 1920×1080 kaputt, weil der Entrypoint Xvfb in dieser Größe startete und xrandr den Root-Framebuffer nachträglich nicht vergrößern kann, ffmpeg hat also fröhlich außerhalb des echten Bildschirms aufgenommen. Der Framebuffer wird jetzt von vornherein mit XVFB_RESOLUTION angelegt, und das Nachskalieren ist weg. Quadratische und hohe Auflösungen funktionieren, die Grenze bei 1920×1080 gibt es nicht mehr.

Wenn Camoufox Stirbt, Kommt Es Zurück

Ein Nutzer lief nach einer Handvoll n8n-Läufen gegen Facebook in “Connection closed while reading from the driver”. Die Ursache war hässlich: der Browser hatte ein totes Page-Objekt im Cache, nachdem Camoufox selbst gestorben war, jede weitere Anfrage versuchte also, mit einer Leiche zu reden.
Jetzt gibt es einen echten Health Check. is_healthy() macht einen Roundtrip zum Driver, statt dem Cache-Zustand zu vertrauen, und ensure_healthy() startet den persistenten Kontext neu, wenn der Roundtrip scheitert, das Profil überlebt, Cookies und Fingerabdruck kommen also mit. Sowohl der interne Page-Getter als auch der Zugriff auf die aktive Seite heilen zuerst und benutzen danach. Die Anfrage, die die Wiederherstellung auslöst, frisst etwa 4-5 Sekunden, alles danach läuft mit voller Geschwindigkeit.
Jede Wiederherstellung wirft außerdem ein Postmortem auf WARNING-Ebene raus: die OOM-Zeilen aus dmesg, meminfo, loadavg und die Liste der überlebenden camoufox-bin-Prozesse. Wenn es stirbt, bekommst du also die echte Ursache im JSON-Log statt eines mysteriösen Container-Stopps.

Die Stealth-Konfiguration, Die Wirklich Zählt

Ein paar Umgebungsvariablen, die tatsächlich beeinflussen, ob du auffliegst:
Zeitzonen-Abgleich. Bot-Detektoren vergleichen die Zeitzone deines Browsers mit der Geolokalisierung deiner IP. Wenn deine IP Rumänien sagt und deine Zeitzone UTC, ist das eine rote Flagge. Setz TZ=Europe/Bucharest (oder was zu deiner IP passt) und dieser Vektor verschwindet.
Proxy-Unterstützung. Leite den gesamten Verkehr über einen beliebigen Ausgang mit PROXY_URL, entweder http://user:pass@host:port oder socks5://host:port, was du eben hast. Zusammen mit dem Zeitzonen-Abgleich siehst du aus wie ein echter Nutzer vom Standort dieses Ausgangs, weshalb die Doku unverblümt sagt, dass es ein autorisierter Ausgang sein sollte, dessen Standort zum getesteten Fingerabdruck passt. Das Repo dokumentiert jetzt auch einen Aufbau, bei dem der Ausgang dir gehört statt gemietet zu sein: eine wegwerfbare pr0xteus-WireGuard-Zelle. Diese Anleitung wurde für pr0xteus v0.11.0 neu geschrieben, das pro Lease zwei URLs zurückgibt statt einer, und der Browser nimmt jetzt die HTTP-Variante: jq -er '.proxies.http'. Keine Stilfrage. Firefox macht authentifiziertes SOCKS5 nicht zuverlässig, Camoufox auf die SOCKS-URL zu zeigen ist also der Weg zu einem Proxy, der genau so lange funktioniert, bis er Zugangsdaten braucht. Der Rest folgt daraus: du erreichst den Controller über --network host, statt einem Egress-Netz beizutreten, hältst die Control-API auf 127.0.0.1:8000 und den HTTP-Proxy auf 127.0.0.1:8080, und verschiebst die API des Browsers mit HTTP_LISTEN_PORT=8090 auf 8090, damit sich die beiden nicht um 8080 streiten. Nichts an diesem Aufbau ist von außerhalb des Hosts erreichbar. Vollständige Anleitung in docs/configuration.md.
Persistente Profile. Mounte ein Verzeichnis nach /userdata und deine Cookies, localStorage, Sessions und der Fingerabdruck überleben Container-Neustarts. Ohne das ist jeder Neustart eine frische Identität, was manchmal genau das ist, was du willst, und manchmal verdammt verdächtig.

docker run -d 
  -e TZ=Europe/Bucharest 
  -e PROXY_URL=http://user:pass@proxy:8888 
  -v ./my-profile:/userdata 
  -p 8080:8080 
  -p 5900:5900 
  psyb0t/stealthy-auto-browse

Vorinstallierte Erweiterungen

Jeder Container kommt mit fertig konfigurierten Privatsphäre-Erweiterungen:

  • uBlock Origin, blockt Werbung, Tracker und Nervereien. Weniger Lärm, weniger laufende Tracking-Skripte
  • LocalCDN, fängt CDN-Anfragen ab und liefert die Ressourcen lokal aus. Google und Cloudflare können dich nicht mehr seitenübergreifend verfolgen
  • ClearURLs, entfernt Tracking-Parameter (utm_source, fbclid, gclid) aus URLs
  • Consent-O-Matic, lehnt Cookie-Consent-Popups automatisch ab, damit du dich mit dem Scheiß nicht befassen musst

Du willst mehr? Mounte ein persistentes Profil, öffne VNC, geh auf about:addons und installier, was du willst. Sie überleben Neustarts.

Ergebnisse der Bot-Erkennungstests

Gegen alles Relevante getestet und überall durchgekommen:

  • CreepJS, Konsistenz von Canvas- und WebGL-Fingerabdruck, Lügenerkennung, Worker-Vergleich: bestanden
  • BrowserScan, WebDriver-Flag, CDP-Signale, navigator-Eigenschaften: bestanden
  • Pixelscan, Kohärenz des Fingerabdrucks, Übereinstimmung von Zeitzone und IP, WebRTC-Lecks: bestanden
  • Cloudflare, Challenge-Seiten, Turnstile, Bot Management: bestanden
  • SannySoft, Intoli-Tests plus Fingerprint-Scanner: bestanden
  • Incolumitas, moderne Erkennungstechniken: bestanden
  • Rebrowser, Erkennung von CDP-Lecks, webdriver, Viewport-Analyse: bestanden
  • BrowserLeaks WebRTC, Erkennung von IP-Lecks über WebRTC: bestanden
  • DeviceAndBrowserInfo, 19 Prüfungen, alle grün, “You are human!”: bestanden
  • IpHey, Bewertung “Trustworthy”: bestanden
  • Fingerprint.com, als normaler Firefox erkannt, keine Bot-Flags: bestanden

Es besteht, weil es nichts zu erkennen gibt. Kein CDP zu finden, weil Firefox keines hat. Keine gefälschten Fingerabdrücke, weil der Fingerabdruck echt und konsistent ist. Keine Automatisierungs-Flags, weil navigator.webdriver wirklich falsch ist. Keine gefakten Eingabe-Events, weil PyAutoGUI echte auf Betriebssystemebene erzeugt.

Es Sagt Dir, Dass Da ein CAPTCHA Ist, Und Sonst Nichts

detect_challenge meldet begrenzte, datenminimierte Belege für dokumentierte Integrationen von Turnstile, reCAPTCHA, hCaptcha, Friendly Captcha, ALTCHA, Arkose, AWS WAF und GeeTest, dazu vorsichtige Hinweise auf sichtbare generische. Verfügbar über HTTP, im Skriptmodus, in run_script und über MCP.
Lies die Verben genau, denn die Auslassungen sind das Design: es klickt nie, löst nie, betritt nie den Challenge-Frame und gibt nie Query-Strings, Site Keys oder Antwort-Tokens preis. Bei Arkose-Ressourcenbelegen werden schlüsseltragende Pfadsegmente geschwärzt, bevor irgendeine API-, Skript- oder MCP-Antwort sie zurückgeben kann. Das sagt dir, dass eine Challenge da ist. Es bringt dich nicht vorbei, und es versucht es auch nicht.
Der Begleiter ist scroll_into_view: true, das den ersten gerenderten erkannten Frame oder das erste Widget in den Viewport holt, ohne es anzuklicken, zu fokussieren, zu lösen, abzuschicken oder zu betreten. Das ist für die Übergabe: deine Automatisierung läuft gegen eine Wand, scrollt die Wand ins Bild, und ein Mensch übernimmt über die noVNC-Sitzung, die die ganze Zeit offen war. Der Browser war immer zuschaubar, jetzt kann er dir auch sagen, wann du hinschauen sollst.

MCP-Server

KI-Agenten können den Browser über das Model Context Protocol steuern, per Streamable HTTP unter /mcp auf Port 8080. Alle Browseraktionen sind als MCP-Tools verfügbar: Navigation, Screenshots, Klicken, Tippen, JavaScript-Auswertung, Cookies, alles.
Häng einen beliebigen MCP-fähigen Client dran, Claude Desktop, Claude Code, eigene Agenten, auf https://ciprian.51k.eu80/mcp/, und los. Läuft im Standalone- wie im Clustermodus, HAProxy routet MCP-Verkehr mit denselben Sticky Sessions wie die HTTP-API.
Im Clustermodus stellt der MCP-Server nur run_script (plus ping und sleep) als Tools bereit. Einzelaktionen wie goto, get_text, screenshot und so weiter stehen hinter einem Cluster nicht als separate MCP-Tools zur Verfügung. Das ist Absicht, warum, steht unten im Abschnitt zum Clustermodus.
Das ist getrennt vom unten erwähnten Ansatz mit dem .agents/.skills/-Verzeichnis. Skills bringen der KI bei, die HTTP-API mit curl zu benutzen. MCP gibt der KI nativen Tool-Zugriff, kein curl, kein HTTP, die Browseraktionen tauchen direkt als aufrufbare Tools auf. Nimm, was zu deinem Aufbau passt.

Authentifizierung

Setz AUTH_TOKEN, um bei allen Anfragen einen Bearer Token zu verlangen (außer /health):

docker run -d -p 8080:8080 -e AUTH_TOKEN=mysecretkey psyb0t/stealthy-auto-browse

Gib den Token im Authorization-Header mit:

curl -H "Authorization: Bearer mysecretkey" https://ciprian.51k.eu80 ...

v2.0.0 hat die Query-Param-Form getötet. Früher wurde auch ?auth_token=mysecretkey akzeptiert, was für MCP-Clients bequem war, die keine Header setzen konnten, und für alles andere furchtbar, Tokens an dieser Stelle lecken in Zugriffslogs, in den Browserverlauf und in Referer-Header. Der Header ist jetzt die einzige akzeptierte Form, und allein das Vorhandensein eines auth_token-Query-Params ist ein sofortiger 401.
Beim Migrieren wissenswert: diese Query-Prüfung läuft vor der Header-Prüfung, ein Client, der einen völlig korrekten Authorization-Header plus ein übrig gebliebenes ?auth_token= schickt, frisst also trotzdem einen 401. Entferne den Query-Param, häng nicht nur den Header dran und halte es für erledigt. Der Vergleich läuft jetzt außerdem in konstanter Zeit (hmac.compare_digest) statt als schlichtes !=, du kannst einen Token also nicht durch Zeitmessung der Antworten stückweise erraten.
Die Auth bleibt aber freiwillig: lass AUTH_TOKEN ungesetzt, und jeder Endpoint außer /health steht allem offen, was den Port erreicht.

Für KI-Agenten Gebaut

Hier die Sache, über die bei Browser-Automatisierung niemand redet: der beste Anwendungsfall 2026 ist kein Python-Skript, das eine Scraping-Schleife dreht. Es sind KI-Agenten, die wie ein Mensch mit dem Web umgehen müssen.
Ich benutze Claude Code ständig, und die Hälfte von dem, was ich davon brauche, geht über Webseiten: Formulare ausfüllen, Dashboards prüfen, Daten von Seiten ohne API holen, in Admin-Panels herumfummeln. Das Problem, einem LLM einen Browser zu geben, war immer die Schnittstelle. Selenium? Zu komplex. Playwrights API? Zu viele bewegliche Teile. Das LLM schreibt am Ende 50 Zeilen Setup, bevor es einen einzigen Button klicken kann.
docker-stealthy-auto-browse wurde von Grund auf KI-freundlich entworfen. Die ganze Schnittstelle sind curl-Kommandos mit JSON. Das war’s. Ein LLM muss keine Bibliotheken importieren, keine Browserinstanzen verwalten, keine async-Kontexte jonglieren, nichts von dem Mist. Es schickt einfach HTTP-Anfragen.
Überleg dir, was ein KI-Agent zum Surfen braucht:

  1. Irgendwohin navigieren, ein curl auf goto
  2. Verstehen, was auf der Seite ist, ein curl auf get_text. Die KI liest den Text und weiß, was sie vor sich hat. Reicht Text nicht, liefert get_interactive_elements jedes anklickbare Ding mit Koordinaten und Beschriftungen. Ist sie immer noch verwirrt, ein Screenshot, Claude kann Bilder lesen
  3. Mit Elementen interagieren, ein curl auf system_click mit x,y-Koordinaten, ein curl auf system_type für Texteingabe
  4. Auf Ergebnisse warten, ein curl auf wait_for_text oder wait_for_element
  5. Das Ergebnis prüfen, noch ein curl auf get_text

Kein SDK. Kein Treiber-Setup. Kein Verwalten des Browser-Lebenszyklus. Das erledigt alles der Container. Die KI redet einfach mit einem HTTP-Endpoint.
Ich habe Claude Code Sachen machen lassen wie:

  • Sich in Web-Dashboards einloggen, zu bestimmten Seiten navigieren, Daten extrahieren und zusammenfassen
  • Mehrstufige Formulare auf Seiten ausfüllen, die JavaScript-Rendering verlangen
  • Seiten auf Änderungen überwachen und mich benachrichtigen, wenn sich etwas aktualisiert
  • In Admin-Panels ohne API herumfummeln, Buttons klicken, Einstellungen ändern, Exporte herunterladen
  • Zeug auf Seiten recherchieren, die normale HTTP-Anfragen hinter Cloudflare blocken

Das Repo liefert ein .agents/.skills/-Verzeichnis mit einer vollständigen Skill-Definition für KI-Coding-Agenten. Klon das Repo (oder nur das .agents/-Verzeichnis) in dein Projekt, und Claude Code findet es von allein. Setz STEALTHY_AUTO_BROWSE_URL=https://ciprian.51k.eu80 und der Agent hat die komplette API-Referenz, beide Eingabemodi, typische Abläufe und Beispiele, alles, was er zum Surfen aus dem Stand braucht.
Es gibt das auch auf ClawHub. Installier es mit clawhub install psyb0t/stealthy-auto-browse, und jeder OpenClaw-kompatible KI-Agent kann den Browser bei Bedarf nutzen.
Die Kombination aus einer idiotensicher einfachen HTTP-API, vollem Stealth gegen Bot-Erkennung und eingebauten Anweisungen für KI-Agenten macht das zum besten Browser-Automatisierungswerkzeug für LLMs, das ich gefunden habe. Und ich habe gesucht, glaub mir. Alles andere verlangt entweder ein kompliziertes SDK-Setup, das die KI verwirrt, oder fliegt bei Cloudflare in der ersten Anfrage auf, oder beides.

Skriptmodus: Ausführen und Raus

Lass beim Containerstart ein YAML-Skript laufen, es führt die Schritte aus, gibt dir die Ergebnisse als JSON auf stdout, und der Container beendet sich. Kein HTTP-Server, kein Dauerprozess. Gut für CI, Cronjobs, einmaliges Scraping, oder alles, wo du eine Sequenz automatisieren und die Ausgabe haben willst.

# Pipe a script in, get JSON results out
cat my-script.yaml | docker run --rm -i 
  psyb0t/stealthy-auto-browse --script > results.json
# Parameterize with environment variables
cat my-script.yaml | docker run --rm -i 
  -e TARGET_URL=https://example.com 
  psyb0t/stealthy-auto-browse --script

Das Skriptformat sind dieselben Aktionen wie in der HTTP-API, nur in YAML:

name: Scrape Example
on_error: stop  # "stop" (default) or "continue"
steps:
  - action: goto
    url: ${env.TARGET_URL}
    wait_until: networkidle
  - action: save_screenshot
    output_id: page_screenshot
    whLargest: 1024
  - action: get_text
    output_id: page_text
  - action: eval
    expression: "document.title"
    output_id: title

Schritte mit output_id landen im Ausgabe-JSON. Screenshots kommen als base64-kodierte PNGs heraus. ${env.VAR_NAME} wird durch Umgebungsvariablen ersetzt. Logs gehen auf stderr, stdout umleiten gibt dir also sauberes JSON. Exit-Code 0, wenn alle Schritte gelingen, 1, wenn einer scheitert. Page Loader feuern weiterhin bei goto, wenn sie konfiguriert sind.

Skripte Können Jetzt Verzweigen und Schleifen (In Grenzen)

Eine flache Liste von Schritten ist schnell am Ende der Straße. “Klick akzeptieren, wenn das Cookie-Banner da ist.” “Scroll weiter, bis der Weiter-Button verschwindet.” Skriptmodus und run_script können jetzt beides: verschachtelte if-Zweige plus repeat– und while-Schleifen.
Die Bedingungen decken den Zustand von CSS-Elementen ab, sichtbaren Text, URL-Globs, boolesche JavaScript-Ergebnisse und die benannten Ausgaben früherer Schritte, ein späterer Zweig kann also darauf reagieren, was ein früherer Schritt tatsächlich gefunden hat, statt dass du beim Abschicken rätst.
Das Wort, das in dieser Überschrift die Arbeit macht, ist Grenzen. Anzahl der Schleifendurchläufe, gesamte Schleifenarbeit, Bedingungs-Timeout und Verschachtelungstiefe sind alle gedeckelt. Ein eingereichtes Skript muss endlich sein, denn dieses Ding nimmt Skripte über HTTP entgegen, und eine while-Schleife ohne Obergrenze ist ein Denial-of-Service-Primitiv, das du absichtlich verteilt hast.

Clustermodus

Du musst gleichzeitige Anfragen bedienen? Fahr mehrere Browserinstanzen hinter HAProxy, mit einer Anfrage-Warteschlange und Cookie-Sync über Redis. Jeder Browser bedient eine Anfrage nach der anderen, der Proxy stellt den Rest an, bis ein Platz frei wird.

curl -LO https://raw.githubusercontent.com/psyb0t/docker-stealthy-auto-browse/main/docker-compose.cluster.yml
curl -LO https://raw.githubusercontent.com/psyb0t/docker-stealthy-auto-browse/main/haproxy.cfg.template
docker compose -f docker-compose.cluster.yml up -d

Das startet Redis, 5 Browser-Container (über NUM_REPLICAS konfigurierbar) und den HAProxy-Queue-Proxy. Der Einstiegspunkt ist https://ciprian.51k.eu80, dieselbe API wie im Einzelcontainer-Modus. MCP unter /mcp/ läuft ebenfalls durch den Proxy.
Im Clustermodus wird nur run_script angenommen. Einzelaktionen wie goto, get_text, click oder screenshot direkt zu schicken, gibt einen Fehler. Das ist so gewollt: jede Anfrage einer mehrstufigen Sequenz kann auf einer anderen Browserinstanz landen, wenn der Client die Sitzungsbindung nicht sorgfältig verwaltet, und wenn er das nicht tut, bekommst du Bugs mit veraltetem Inhalt, subtil und zum Wahnsinnigwerden. run_script ist atomar. Jeder Schritt des Skripts läuft auf derselben Browserinstanz, in derselben Anfrage. Keine Bindung zu verwalten. Kein Zustand, der zwischen Instanzen ausblutet.
Im Standalone-Modus (ein Container, kein Cluster) funktionieren Einzelaktionen weiterhin gut, Anfragen werden automatisch serialisiert, es gibt also kein Nebenläufigkeitsproblem.
Die Syntax ist identisch zu der im Standalone-Modus. Vollständige Login-Sequenz, Navigation, Extraktion, alles in einem Rutsch:

curl -X POST https://ciprian.51k.eu80 
  -H "Content-Type: application/json" 
  -d '{
    "action": "run_script",
    "steps": [
      {"action": "goto", "url": "https://example.com/login", "wait_until": "domcontentloaded"},
      {"action": "system_click", "x": 400, "y": 200},
      {"action": "system_type", "text": "[email protected]"},
      {"action": "send_key", "key": "tab"},
      {"action": "system_type", "text": "secretpassword"},
      {"action": "send_key", "key": "enter"},
      {"action": "wait_for_url", "url": "**/dashboard", "timeout": 15},
      {"action": "get_text", "output_id": "page"}
    ]
  }'

HAProxy kümmert sich intern ums Routing, es weist eine freie Browserinstanz zu und hält das ganze Skript auf dieser Instanz. Du fasst nie INSTANCEID-Cookies an. Du denkst überhaupt nicht ans Routing.
Cookie-Sync über Redis ist das Killer-Feature. Cookies, die auf einer beliebigen Instanz gesetzt werden, verbreiten sich per Redis PubSub sofort auf alle anderen. Logg dich auf browser1 ein, und browser2 bis browser10 sind sofort authentifiziert. Lass ein einziges Login-Skript auf irgendeiner Instanz laufen, und die ganze Flotte ist eingeloggt, ohne das Login auf jedem Browser zu wiederholen.
HAProxy stellt auf Port 8081 ein Statistik-Dashboard bereit: Live-Verkehr, Warteschlangentiefe, Serverzustand, Anfrageraten pro Instanz.

Skill und Plugin

Das Repo liefert einen Agent-Skill und ein OpenClaw-Plugin unter .agents/, beide von der CI bei Tag-Pushes auf ClawHub veröffentlicht. Richte einen Agenten auf das Plugin und er steuert den MCP-Endpoint einer laufenden Instanz direkt, ohne Glue Code, ohne die Aktionsliste jede Sitzung neu zu erklären.

Unterm Strich

Jedes andere Browser-Automatisierungswerkzeug spielt Verteidigung, versteckt CDP-Signale, patcht Erkennungsvektoren, hofft, dass das nächste Cloudflare-Update sein Stealth-Plugin nicht zerlegt. docker-stealthy-auto-browse spielt dieses Spiel nicht. Es gibt kein CDP zu verstecken. Es gibt keine Automatisierungssignale zu patchen. Der Browser weiß wirklich nicht, dass er automatisiert wird.
Ein Docker-Container. Eine HTTP-API. Er besteht jeden Bot-Detektor, den wir auf ihn geworfen haben.
Hol es dir: github.com/psyb0t/docker-stealthy-auto-browse
Lizenziert unter der WTFPL, Do What The Fuck You Want To Public License. Weil, klar.

Rein Damit In Deinen Agenten

Da der ganze Sinn Agenten sind, die Browser steuern, zählt der Installationsweg hier mehr als sonst wo. Alles unter .agents/ ist in einem einzigen Marketplace katalogisiert, also sind es zwei Befehle:

claude plugin marketplace add psyb0t/agents
claude plugin install stealthy-auto-browse@psyb0t

Codex nutzt denselben Marketplace mit einem anderen Verb, codex plugin add stealthy-auto-browse@psyb0t, weil es kein codex plugin install gibt. Er findet den Skill außerdem von allein in einem Checkout des Repos, da er .agents/skills/ nativ scannt, ganz ohne Installation. Es ist jetzt auch im offiziellen MCP Registry gelistet, ein Client, der seine Server von dort auflöst, kann es also finden, ohne dass man ihm eine URL gibt.