{"id":25498,"date":"2026-09-03T07:40:55","date_gmt":"2026-09-03T07:40:55","guid":{"rendered":"https:\/\/capskip.com\/?p=25498"},"modified":"2026-09-03T07:40:55","modified_gmt":"2026-09-03T07:40:55","slug":"camoufox-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/camoufox-captcha\/","title":{"rendered":"\u5982\u4f55\u5728 Camoufox \u4e2d\u4f7f\u7528 Main World Eval \u8bc6\u522b\u9a8c\u8bc1\u7801"},"content":{"rendered":"<p>A Camoufox captcha step looks like every other one: read the sitekey, send it to a solver, write the token into the page. The third move is where Camoufox is different. It runs the JavaScript you hand to evaluate in an isolated scope that the page cannot see, and an isolated scope cannot change the page&#8217;s DOM. Your write returns without an error, the textarea stays empty, and the form fails validation. The fix is one launch option and a two-character prefix.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>Python 3.10 or newer. Camoufox 0.5 pins Playwright itself, so let pip resolve it.<\/li>\n<li>The Camoufox browser downloaded once with the fetch command. It is a Firefox build, not Chromium.<\/li>\n<li>The page URL of the protected form. The sitekey is read at runtime.<\/li>\n<li>CapSkip running in Local mode when the script and the solver share a machine, or in Server mode when they do not. Both are described under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>.<\/li>\n<\/ul>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># The geoip extra is optional and worth having if you use proxies.\npip install -U capskip &quot;camoufox[geoip]&quot;\n\n# Downloads the browser itself. Run once per machine.\ncamoufox fetch<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Why the usual token injection does nothing here<\/h2>\n<p>Camoufox is a patched Firefox with a thin Python wrapper around Playwright. The wrapper class is a Playwright context manager, so what you get back from it is an ordinary Playwright browser object and every locator, click and navigation you already know works unchanged. That is the good news and it is most of the library.<\/p>\n<p>The exception is script execution. Camoufox runs all JavaScript in an isolated scope that is invisible to the page, which is the whole reason it exists: a site cannot see the automation poking at it. Reading is unaffected, because Playwright&#8217;s own locator methods travel over the browser protocol rather than through that scope. Writing is affected, and this is the sentence to remember: an isolated scope cannot modify the DOM. A token assignment there is discarded quietly.<\/p>\n<p>Camoufox&#8217;s answer is a main world escape hatch. Pass main_world_eval when you launch, then prefix any script that has to touch the real page with mw: and it runs in the page&#8217;s own scope. Two things come with that. The site can detect code running there, so use it for the injection and nothing else. And you cannot return element references out of the main world, only values that survive as JSON.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: launch with the main world enabled<\/h2>\n<p>The option is off by default and it has to be set at launch time. There is no way to switch it on for a single call later.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install camoufox[geoip]\nfrom camoufox.sync_api import Camoufox\n\nwith Camoufox(main_world_eval=True, headless=True) as browser:\n    page = browser.new_page()\n    page.goto(&quot;https:\/\/example.com\/page-with-recaptcha&quot;)\n\n    # browser is a normal Playwright Browser from here on.\n    print(page.title())<\/pre>\n<\/div>\n<p>Two neighbouring options are worth knowing before you go further. humanize moves the cursor along a human-looking path, taking up to about 1.5 seconds to cross the window, which matters if you click the widget yourself rather than injecting a token. And disable_coop drops the Cross-Origin-Opener-Policy so that elements inside cross-origin iframes, the Cloudflare Turnstile checkbox among them, can be clicked at all.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: read the sitekey off the page<\/h2>\n<p>The sitekey sits on the host document, not inside the widget iframe. Google&#8217;s markup puts it on a container as a data-sitekey attribute, and a plain Playwright locator reads it. No main world prefix is needed, because this path never goes through the isolated scope.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Locators wait by default, so this doubles as a wait\n# condition for a widget that renders late.\nholder = page.locator(&quot;div.g-recaptcha&quot;)\nholder.wait_for(state=&quot;attached&quot;, timeout=15000)\n\nsitekey = holder.get_attribute(&quot;data-sitekey&quot;)\nprint(sitekey)   # 6Lxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<\/pre>\n<\/div>\n<p>Some sites never expose the sitekey on the host page and pass it only in the widget iframe URL. Read it out of the query string in that case.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Fallback: the k= parameter on the anchor iframe.\nfrom urllib.parse import urlparse, parse_qs\n\nsrc = page.locator(&quot;iframe[src*='recaptcha\/api2\/anchor']&quot;).get_attribute(&quot;src&quot;)\nsitekey = parse_qs(urlparse(src).query)[&quot;k&quot;][0]<\/pre>\n<\/div>\n<p>Check the value before you spend a solve on it. An empty sitekey travels all the way to the solver and comes back as <a href=\"https:\/\/capskip.com\/error-googlekey-pageurl\/\">ERROR_GOOGLEKEY<\/a>, a long way from the locator that actually failed.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: solve it on your own machine<\/h2>\n<p>The Python SDK talks to CapSkip on port 8080 and returns the token as a plain string. One method covers reCAPTCHA v2, Invisible, Enterprise and v3, with the variants passed as options rather than as separate calls.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip\nfrom capskip import CapSkip\n\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n\n# Invisible v2 takes invisible=1, Enterprise takes enterprise=1,\n# and v3 takes version=&quot;v3&quot; with an action.\nresult = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)\n\ntoken = result[&quot;code&quot;]   # the g-recaptcha-response value<\/pre>\n<\/div>\n<p>Turnstile and GeeTest have their own methods and take the same shape. Turnstile additionally returns the user agent the solve was made with, and a challenge page will reject the token unless you send that user agent back with it. Full parameter lists live in <a href=\"https:\/\/capskip.com\/api-docs\/\">the CapSkip API documentation<\/a>.<\/p>\n<p>Camoufox also ships an async class, and the Python SDK&#8217;s AsyncCapSkip is a genuine asyncio implementation rather than an alias for the synchronous one. Pair them when you drive several contexts at once, so a solve on one page does not stall the others.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: inject the token in the main world<\/h2>\n<p>Here is the part that is specific to this browser. The response textarea is hidden with display:none, so no automation tool can type into it and you have to assign the value with JavaScript. Prefix the script with mw: so it runs in the page&#8217;s own scope, and build the string with json.dumps rather than an f-string, because a JSON string literal is also a valid JavaScript string literal, quoting and escaping included.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># The mw: prefix is what makes this write land.\nimport json\n\npage.evaluate(\n    &quot;mw:document.getElementById('g-recaptcha-response').value = &quot;\n    + json.dumps(token)\n)\n\npage.click(&quot;button[type=submit]&quot;)<\/pre>\n<\/div>\n<p>Verifying that it landed has one wrinkle. You cannot hand an element back out of the main world, so return a value instead of the node. A length is fine and it tells you exactly what you want to know.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Return a number, never the element itself.\nlength = page.evaluate(\n    &quot;mw:document.getElementById('g-recaptcha-response').value.length&quot;\n)\nprint(length)   # 0 means the injection did not land<\/pre>\n<\/div>\n<p>If the site defines a success callback instead of reading the textarea when the form submits, call that callback after setting the value. This is the case where the main world is not merely convenient but required, because a function the page defined does not exist in the isolated scope at all. Read the function name out of the page&#8217;s own markup rather than guessing it. Either submission style is still ordinary reCAPTCHA v2 underneath, and both of them are written up in detail on <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">the reCAPTCHA v2 solver page<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Full working example<\/h2>\n<p>Everything above in one script. The solver is created once, the browser closes itself when the block ends, and the sitekey is checked before a solve is spent on it.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip camoufox[geoip]\nimport json\nfrom camoufox.sync_api import Camoufox\nfrom capskip import CapSkip\n\nPAGE_URL = &quot;https:\/\/example.com\/page-with-recaptcha&quot;\n\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n\nwith Camoufox(main_world_eval=True, headless=True) as browser:\n    page = browser.new_page()\n    page.goto(PAGE_URL)\n\n    holder = page.locator(&quot;div.g-recaptcha&quot;)\n    holder.wait_for(state=&quot;attached&quot;, timeout=15000)\n    sitekey = holder.get_attribute(&quot;data-sitekey&quot;)\n    if not sitekey:\n        raise RuntimeError(&quot;Widget found but data-sitekey was empty.&quot;)\n\n    result = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)\n    page.evaluate(\n        &quot;mw:document.getElementById('g-recaptcha-response').value = &quot;\n        + json.dumps(result[&quot;code&quot;])\n    )\n\n    page.click(&quot;button[type=submit]&quot;)\n    page.wait_for_load_state(&quot;networkidle&quot;)\n    print(page.url)   # where you land after submitting<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running the solver on another machine<\/h2>\n<p>Camoufox tends to end up on a bigger box than the one you wrote the script on, and there is a platform detail to get right when it does. Passing headless as the string virtual starts an Xvfb display, which is a Linux-only feature: on Windows and macOS that value raises a not-supported error instead. Plain headless works everywhere, so use the boolean unless you are deliberately on Linux.<\/p>\n<p>The solver does not have to travel with the browser. CapSkip has two connection modes. Local binds to 127.0.0.1 and answers that device only, which is the right setting while you are writing the script on the machine the app runs on. Server binds to your network or public IP, so a scraping VM, a second workstation or a Linux box running Camoufox calls the same Windows machine over the API. A static public IP keeps that address stable. Nothing in the code changes except the host, and nothing about the cost changes either, because it is still your hardware.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Same SDK, same call. Only the host moves.\nsolver = CapSkip(host=&quot;10.0.0.12&quot;, port=8080, apiKey=&quot;YOUR_API_KEY&quot;)<\/pre>\n<\/div>\n<p>Turn on key validation once the solver listens on a network address, and give each machine its own key so one can be revoked without touching the others. Both modes are walked through in the <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">CapSkip setup guide<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors and what they mean<\/h2>\n<table>\n<thead>\n<tr>\n<th>What you see<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>The token is set but the form still fails<\/td>\n<td>The write ran in the isolated scope and was discarded<\/td>\n<td>Launch with main_world_eval and prefix the script with mw:<\/td>\n<\/tr>\n<tr>\n<td>Evaluate raises about an unsupported return<\/td>\n<td>A node reference was returned out of the main world<\/td>\n<td>Return a length or a string instead of the element<\/td>\n<\/tr>\n<tr>\n<td>The callback function is undefined<\/td>\n<td>Page globals do not exist in the isolated scope<\/td>\n<td>Call it from the main world with the same prefix<\/td>\n<\/tr>\n<tr>\n<td>The Turnstile checkbox cannot be clicked<\/td>\n<td>It sits in a cross-origin iframe<\/td>\n<td>Launch with disable_coop, or inject a token instead of clicking<\/td>\n<\/tr>\n<tr>\n<td>ERROR_GOOGLEKEY<\/td>\n<td>An empty sitekey reached the solver<\/td>\n<td>Assert the value before calling recaptcha<\/td>\n<\/tr>\n<tr>\n<td>NetworkException<\/td>\n<td>CapSkip is not running, or the host is wrong<\/td>\n<td>Start the app, or point host at the server address<\/td>\n<\/tr>\n<tr>\n<td>TimeoutException<\/td>\n<td>The solve outlasted recaptchaTimeout<\/td>\n<td>Raise it above the default of 300 seconds<\/td>\n<\/tr>\n<tr>\n<td>Virtual display not supported<\/td>\n<td>headless was set to virtual off Linux<\/td>\n<td>Use headless=True instead<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Does main world eval make me easier to detect?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Anything running in the main world is visible to the page, so yes, in principle. In practice the exposure is one assignment that lasts microseconds and looks identical to what the widget&#8217;s own script does when a human passes the challenge. Keep every other script in the isolated scope, do the injection in one call rather than several, and you are not handing over much.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can I reuse my existing Playwright code?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Almost all of it. The launcher hands back a real Playwright browser, so locators, contexts, routes and waits behave as they always did. The two things to revisit are any call that writes to the DOM through evaluate, which needs the prefix, and anything that assumed Chromium, since this is Firefox. The wider picture for that engine is on <a href=\"https:\/\/capskip.com\/playwright-captcha-solver\/\">the Playwright CAPTCHA solver page<\/a>.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Should I click the widget instead of injecting a token?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Only for Turnstile, and only sometimes. A Turnstile checkbox in managed mode can pass on its own if the browser looks convincing, which is what Camoufox is for, and clicking it needs the Cross-Origin-Opener-Policy dropped first. A reCAPTCHA checkbox click just opens an image challenge, so there is nothing to gain there. Details on the widget side are on <a href=\"https:\/\/capskip.com\/cloudflare-turnstile-solver\/\">the Cloudflare Turnstile solver page<\/a>.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">My crawler runs on a Linux VPS. Where does CapSkip go?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">On a Windows machine you control, with Server mode switched on. The VPS then calls it over the API exactly as it would any internal service, so Camoufox and the solver do not need to share an operating system or even a network segment. Point the host argument at that address, enable key validation, and give the VPS its own key.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>Launch Camoufox with main_world_eval switched on, read the sitekey with an ordinary locator, solve it against CapSkip on 127.0.0.1:8080, then inject the token through an evaluate call prefixed with mw: and submit. The prefix is the whole trick, because without it your write lands in a scope the page never sees. For the rest of the Python landscape, including Selenium and Playwright, see <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">the Python CAPTCHA solver page<\/a>.<\/p>\n<p>One more thing is worth knowing before you scale a crawl up. CapSkip is a <a href=\"https:\/\/capskip.com\/\">local captcha solver<\/a> that runs on hardware you already own, so a run that retries a thousand pages costs exactly what a run that retries ten costs.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Camoufox \u4f1a\u628a\u4f60\u4f20\u7ed9 evaluate \u7684\u6bcf\u4e00\u6bb5\u811a\u672c\u653e\u8fdb\u4e00\u4e2a\u9694\u79bb\u4f5c\u7528\u57df\u91cc\u6267\u884c\uff0c\u6240\u4ee5\u5e38\u89c4\u7684 token \u6ce8\u5165\u6beb\u65e0\u6548\u679c\u3002\u6253\u5f00 main world eval\uff0c\u7ed9\u811a\u672c\u52a0\u4e0a\u524d\u7f00\uff0c\u8868\u5355\u5c31\u80fd\u63d0\u4ea4\u4e86\u3002<\/p>","protected":false},"author":1,"featured_media":25497,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Camoufox CAPTCHA: Inject the Token That Sticks | CapSkip","rank_math_description":"A Camoufox captcha token never lands: Camoufox runs your JavaScript in an isolated scope. Enable main_world_eval, prefix the script with mw:, then submit.","rank_math_focus_keyword":"camoufox captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25498","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25498","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=25498"}],"version-history":[{"count":1,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25498\/revisions"}],"predecessor-version":[{"id":25501,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25498\/revisions\/25501"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25497"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25498"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25498"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25498"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}