Post

Camoufox Tutorial: Setting Up a Fingerprint-Resistant Firefox

Camoufox Tutorial: Setting Up a Fingerprint-Resistant Firefox

Camoufox is not a browser extension or a set of JavaScript patches layered on top of Firefox. It modifies Firefox at the C++ engine level, rewriting the source code that produces browser fingerprints so that no detectable shim exists for anti-bot systems to find. When a detection script renders a canvas, probes WebGL parameters, or enumerates font metrics, the responses come from compiled native code rather than from JavaScript overrides that can be discovered and flagged. This tutorial walks through installation, configuration, and practical usage patterns so you can start using Camoufox in your own projects.

How Camoufox Differs from Patching

Most stealth approaches work by running a stock browser and patching its behavior from the outside. Selenium stealth plugins override navigator.webdriver. Puppeteer-stealth hooks into JavaScript APIs. These patches exist in JavaScript, and detection scripts can probe for inconsistencies between the patched surface and the underlying engine. Our overview of stealth browsers in 2026 covers how Camoufox fits into the broader anti-detection landscape.

Camoufox ships a custom-compiled Firefox binary where the fingerprint-producing code has been modified at the C++ level. There is no JavaScript layer to discover.

graph TD
    subgraph "Patch Approach"
        A["Stock Browser"] --> B["JS Patches<br>(stealth plugins)"]
        B --> C["Modified Surface<br>(detectable gaps)"]
        C --> D["Anti-Bot Probes<br>for inconsistencies"]
        D --> E["Detected"]
    end

    style A fill:#ffcccc
    style B fill:#ffcccc
    style C fill:#ffcccc
    style E fill:#ff9999
graph TD
    subgraph "Camoufox Approach"
        F["Custom Firefox Build<br>(C++ engine changes)"] --> G["Native Fingerprints<br>(no JS shim layer)"]
        G --> H["Anti-Bot Probes<br>find consistent data"]
        H --> I["Passes"]
    end

    style F fill:#ccffcc
    style G fill:#ccffcc
    style I fill:#99ff99

The canvas hash, WebGL renderer string, and font metrics all come from the same modified engine, so cross-referencing them produces a consistent profile that passes checks which trip up JavaScript-based stealth solutions. For a direct comparison of the two approaches, see Camoufox vs Selenium anti-detection approaches compared.

Installation

Camoufox is distributed as a Python package. The recommended install includes the GeoIP database for IP-based geolocation spoofing:

1
pip install camoufox[geoip]

On the first run, Camoufox automatically downloads its custom Firefox build. This is a one-time operation. The binary is cached locally and reused for subsequent runs. If you only need the core library without GeoIP support, use pip install camoufox instead.

Requirements:

  • Python 3.8+
  • No separate Firefox installation needed — Camoufox downloads and manages its own custom build
  • Playwright is bundled — Camoufox uses Playwright internally but packages its own compatible version

Verify the installation:

1
2
3
4
5
6
from camoufox.sync_api import Camoufox

with Camoufox() as browser:
    page = browser.new_page()
    page.goto("https://example.com")
    print(page.title())

If this prints Example Domain, your setup is working.

Basic Usage with the Sync API

Camoufox provides a synchronous API that wraps Playwright’s async internals. For scripts that do not need concurrency, this is the simplest way to get started.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from camoufox.sync_api import Camoufox


def scrape_page(url):
    with Camoufox(headless=True) as browser:
        page = browser.new_page()
        page.goto(url, wait_until="networkidle")

        title = page.title()
        links = page.eval_on_selector_all(
            "a[href]",
            "elements => elements.map(el => ({text: el.textContent.trim(), href: el.href}))"
        )
        return {"title": title, "links": links}


result = scrape_page("https://example.com")
print(result)

The with statement ensures the browser closes cleanly when the block exits. Since Camoufox uses Playwright internally, the page object supports the full Playwright API: page.locator(), page.query_selector(), page.evaluate(), and everything else you would expect. You can also use Camoufox with JavaScript for browser automation if Python is not your primary language.

Async API for Concurrent Scraping

When you need to scrape multiple pages simultaneously, the async API lets you run browser operations concurrently:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import asyncio
from camoufox.async_api import AsyncCamoufox


async def scrape_url(browser, url):
    page = await browser.new_page()
    try:
        await page.goto(url, wait_until="networkidle", timeout=30000)
        title = await page.title()
        content = await page.text_content("body")
        return {"url": url, "title": title, "length": len(content or "")}
    finally:
        await page.close()


async def main():
    urls = [
        "https://example.com",
        "https://httpbin.org/get",
        "https://quotes.toscrape.com",
    ]

    async with AsyncCamoufox(headless=True) as browser:
        tasks = [scrape_url(browser, url) for url in urls]
        results = await asyncio.gather(*tasks, return_exceptions=True)

        for result in results:
            if isinstance(result, Exception):
                print(f"Error: {result}")
            else:
                print(f"{result['url']} - {result['title']} ({result['length']} chars)")


asyncio.run(main())

Each URL gets its own page (tab) within a single browser instance, which is more efficient than launching separate browsers. The return_exceptions=True flag prevents one failed page from crashing the entire batch.

Configuration Options

Camoufox exposes several configuration parameters that control how the browser presents itself. These are passed to the Camoufox or AsyncCamoufox constructor.

OS Spoofing and Screen Size

1
2
3
4
5
6
7
8
9
from camoufox.sync_api import Camoufox

with Camoufox(
    headless=True,
    os="windows",                              # "windows", "macos", or "linux"
    screen={"width": 1920, "height": 1080},    # Match a plausible resolution
) as browser:
    page = browser.new_page()
    page.goto("https://example.com")

When you set os, Camoufox adjusts not just the User-Agent but also navigator properties, platform strings, and font lists to match the target OS. This consistency is what separates engine-level spoofing from header-only changes.

Human-Like Behavior and GeoIP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from camoufox.sync_api import Camoufox

with Camoufox(
    headless=True,
    humanize=True,    # Realistic delays and mouse movement patterns
    geoip=True,       # Match locale/timezone to proxy IP location
    proxy={
        "server": "http://proxy.example.com:8080",
        "username": "user",
        "password": "pass",
    },
) as browser:
    page = browser.new_page()
    page.goto("https://example.com")

When humanize is enabled, clicks include slight position jitter and timing variation. The geoip=True flag eliminates a common detection vector: a browser claiming US locale and timezone while connecting from a German IP address.

Fingerprint Verification

After configuring Camoufox, verify what anti-bot systems actually see:

1
2
3
4
5
6
7
8
9
10
11
12
from camoufox.sync_api import Camoufox

with Camoufox(headless=False, os="windows") as browser:
    page = browser.new_page()

    page.goto("https://browserleaks.com/canvas")
    page.wait_for_timeout(5000)
    page.screenshot(path="canvas_fingerprint.png")

    page.goto("https://creepjs.com")
    page.wait_for_timeout(10000)
    page.screenshot(path="creepjs_report.png")

What to look for: the canvas hash should be consistent with the claimed OS and browser version. The CreepJS trust score should be high, indicating no detectable inconsistencies. The navigator.webdriver property should be undefined or false.

Headless vs Headed Mode

Headless mode uses less memory, runs faster, and works on servers without a display. Camoufox’s headless implementation avoids the fingerprint leaks that plague stock Firefox headless — properties like window.outerHeight return plausible values rather than the zeros that typically betray headless browsers.

Headed mode is essential for debugging. When a site blocks your scraper, switching to headed lets you see CAPTCHA challenges, watch page loads, and inspect the DOM interactively. If you encounter blocks in headless that do not appear in headed mode, the target site likely has headless-specific detection.

Firefox's architecture offers unique advantages for fingerprint resistance.
Firefox's architecture offers unique advantages for fingerprint resistance. Photo by Caio / Pexels

Using with Proxies

Camoufox accepts proxy configuration through the same format Playwright uses:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from camoufox.sync_api import Camoufox

# HTTP proxy
with Camoufox(
    headless=True,
    proxy={"server": "http://proxy.example.com:8080", "username": "user", "password": "pass"},
) as browser:
    page = browser.new_page()
    page.goto("https://httpbin.org/ip")
    print(page.text_content("body"))

# SOCKS5 proxy
with Camoufox(
    headless=True,
    proxy={"server": "socks5://proxy.example.com:1080"},
) as browser:
    page = browser.new_page()
    page.goto("https://httpbin.org/ip")

Combining proxies with geoip=True automatically sets the browser locale and timezone to match the proxy’s geographic location.

Persistent Profiles

By default, Camoufox creates a fresh profile for each session. For tasks requiring logged-in state across runs, specify a persistent profile directory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import os
from camoufox.sync_api import Camoufox

PROFILE_DIR = os.path.expanduser("~/.camoufox_profiles/my_session")

# First run: log in and save the session
with Camoufox(headless=False, persistent_context=True, user_data_dir=PROFILE_DIR) as browser:
    page = browser.new_page()
    page.goto("https://example.com/login")
    page.fill("#username", "my_user")
    page.fill("#password", "my_password")
    page.click("#login-button")
    page.wait_for_url("**/dashboard**")

# Later runs: reuse the saved session
with Camoufox(headless=True, persistent_context=True, user_data_dir=PROFILE_DIR) as browser:
    page = browser.new_page()
    page.goto("https://example.com/dashboard")
    print(page.text_content(".dashboard-data"))

Be careful with persistent profiles when using fingerprint spoofing. Each profile should use a consistent set of fingerprint parameters — logging in with one spoofed OS and resuming with a different one creates inconsistencies that detection systems can flag.

Scraping Example: Extracting Data from a Protected Site

A complete example combining multiple Camoufox features:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import json
from camoufox.sync_api import Camoufox


def scrape_protected_site():
    with Camoufox(
        headless=True,
        os="windows",
        humanize=True,
        geoip=True,
        proxy={"server": "http://us-proxy.example.com:8080", "username": "user", "password": "pass"},
    ) as browser:
        page = browser.new_page()
        page.goto("https://protected-site.example.com/products", wait_until="networkidle")

        # Wait for anti-bot check to clear
        page.wait_for_selector(".product-list", timeout=15000)

        # Scroll to trigger lazy-loaded content
        for _ in range(3):
            page.evaluate("window.scrollBy(0, window.innerHeight)")
            page.wait_for_timeout(1500)

        # Extract product data
        products = page.evaluate("""
            () => {
                const items = document.querySelectorAll('.product-card');
                return Array.from(items).map(item => ({
                    name: item.querySelector('.product-name')?.textContent?.trim(),
                    price: item.querySelector('.product-price')?.textContent?.trim(),
                    rating: item.querySelector('.product-rating')?.getAttribute('data-score'),
                    url: item.querySelector('a')?.href,
                }));
            }
        """)

        return products


products = scrape_protected_site()
with open("products.json", "w") as f:
    json.dump(products, f, indent=2)

Common Issues: Firefox vs Chrome Differences

Camoufox is built on Firefox, not Chrome. If you are coming from Playwright with Chromium or from Puppeteer, there are important differences.

Chrome DevTools Protocol is unavailable. CDP is a Chrome-specific protocol. Any code that relies on new_cdp_session() or CDP events needs to be rewritten using Playwright’s standard cross-browser API. If you are coming from a Playwright-based workflow, our Playwright vs Camoufox stealth comparison covers the key differences.

Selector edge cases. Most CSS selectors work identically, but Chrome-specific pseudo-elements may behave differently. Always test your selectors against Firefox. Standard selectors and Playwright text selectors (text=Add to cart) work everywhere.

Network interception. Route handlers work, but request and response objects may have different properties available compared to Chromium:

1
2
3
4
5
6
7
8
9
10
from camoufox.sync_api import Camoufox

with Camoufox(headless=True) as browser:
    page = browser.new_page()

    # Block images and tracking scripts to speed up scraping
    page.route("**/*.{png,jpg,jpeg,gif,svg,webp}", lambda route: route.abort())
    page.route("**/analytics.js", lambda route: route.abort())

    page.goto("https://example.com")

Performance Tips

Camoufox launches a full browser, which is heavier than HTTP-only scraping libraries. These practices help manage resource consumption.

Reuse browser instances. Open multiple pages within one browser rather than creating a new browser per page. Close pages when done with page.close().

Block unnecessary resources. Images, fonts, and media consume bandwidth without contributing to data extraction:

1
2
3
4
5
6
page.route(
    "**/*",
    lambda route: route.abort()
    if route.request.resource_type in ["image", "font", "media"]
    else route.continue_(),
)

Use domcontentloaded instead of networkidle. The networkidle wait stalls on sites with continuous background requests. Wait for domcontentloaded and then use wait_for_selector for the specific element you need:

1
2
page.goto(url, wait_until="domcontentloaded")
page.wait_for_selector(".product-list")

Architecture Overview

graph TD
    A["Python Script"] --> B["Camoufox Library"]
    B -->|"Configures fingerprint<br>OS, screen, locale"| C["Custom Firefox Build"]
    C -->|"Engine-level spoofing<br>Canvas, WebGL, fonts"| D["TLS Handshake<br>(Firefox fingerprint)"]
    D --> E["Proxy Server<br>(optional)"]
    E --> F["Target Website"]
    F -->|"Anti-bot checks"| G{"Fingerprint<br>Consistent?"}
    G -->|"Yes"| H["Content Served"]
    G -->|"No"| I["Blocked / CAPTCHA"]

    style A fill:#cce5ff
    style B fill:#cce5ff
    style C fill:#cce5ff
    style D fill:#d4edda
    style E fill:#f0f0f0
    style F fill:#f0f0f0
    style H fill:#99ff99
    style I fill:#ff9999

Each layer contributes to stealth: the Camoufox library configures fingerprint parameters, the custom Firefox build produces consistent fingerprints from native code, the TLS handshake uses Firefox’s legitimate JA3/JA4 fingerprint, and the optional proxy routes traffic through a clean IP with locale matching. These layers reflect the broader evolution of web scraping detection methods that have driven the need for engine-level stealth.

Quick Reference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from camoufox.sync_api import Camoufox

with Camoufox(
    headless=True,                    # Run without visible window
    os="windows",                     # Spoof as Windows/macOS/Linux
    humanize=True,                    # Human-like interaction patterns
    geoip=True,                       # Match locale to proxy IP
    screen={"width": 1920, "height": 1080},
    proxy={
        "server": "http://proxy:8080",
        "username": "user",
        "password": "pass",
    },
    persistent_context=True,
    user_data_dir="/path/to/profile",
) as browser:
    page = browser.new_page()
    page.goto("https://example.com")

Camoufox handles the hard part of stealth — making fingerprints consistent at the engine level — so you can focus on the scraping logic itself. Start with the defaults, verify your fingerprint with the tools described above, and add configuration as needed based on what the target site’s detection system is actually checking.

Contact Arman for Complex Problems
This post is licensed under CC BY 4.0 by the author.