Layer 4 · renderer
Reading a page is a ladder, and the top rung handles most of the web.
Two lines of configuration — [renderer], mode = "auto" —
select escalation rather than a fixed strategy. Cheap fetch first; a browser only when the cheap
fetch fails. The expensive rung is opt-in, and the repository's own measurements say it rarely
helps.
The interesting thing about this layer is not the technology on each rung, which is upstream fastCRW's. It is the ordering, and the fact that the ordering was tested rather than assumed — including the test that produced an answer the author did not want.
MechanismThe escalation ladder
Rung one is the surprising one. A plain HTTP fetch is not a naive fetch: crw's ordinary path already does proper TLS fingerprinting, which is the check most anti-bot layers apply before they ever look at a DOM. That is why escalation is rarely needed, and why adding a heavier browser further down buys less than intuition suggests.
MeasurementThe Camoufox result
Camoufox is a Firefox fork with anti-detection patches at the C++ level, run here through the camofox-browser REST sidecar. crw supports it as an opt-in renderer tier. The repository tested it against the sites people actually complain about, and published what happened:
| site | plain HTTP | camoufox | verdict |
|---|---|---|---|
| g2.com | junk ("Please enable JS") | bot challenge detected | tie — neither returned content |
| indeed.com | junk ("Security Check") | bot challenge detected | tie — neither returned content |
| zillow | 9,149 chars | failed | loss |
| yahoo finance | 48,971 chars | 7,574 chars | loss — 85% less content |
| blocked | blocked | tie |
It lost or tied everywhere.
README.md — Camoufox stealth tier
Two reasons are given, and both are structural rather than fixable by configuration. First, crw's plain HTTP path already does the TLS fingerprinting that a stealth browser would otherwise contribute. Second — and this is the general lesson — these sites block on IP and rate, not on browser fingerprint. A better disguise does not help when the thing being recognised is the address, not the face.
Because failing honestly has value that beating the bot check does not. Plain HTTP hands your
model a CAPTCHA page scored as a successful fetch; Camoufox reports bot challenge
detected. Feeding a model a challenge page and calling it a source is worse than a clean
failure — the model will summarise it, cite it, and reason from it. And because
include_in_auto = true places the tier at the end of the ladder, it only
ever runs on pages the cheap rungs already failed. It cannot make working pages worse.
OperationTurning the tier on
Three things have to be true, and the first one is not a configuration change: the published
ghcr.io/us/crw image does not contain the feature, so you have to compile it.
$ git clone https://github.com/us/crw.git ./vendor/crw $ ./scripts/build-crw-camoufox.sh # the build it runs — note the capped job count docker build \ --build-arg CARGO_PKGS="-p crw-server --features cdp,camoufox -p crw-mcp -p crw-cli" \ --build-arg CARGO_BUILD_JOBS="6" \ -t crw-local:camoufox ./vendor/crw Built crw-local:camoufox. Now: 1. set CRW_IMAGE=crw-local:camoufox in .env 2. uncomment [renderer.camoufox] in config/crw.toml 3. build + run the sidecar: git clone https://github.com/jo-inc/camofox-browser.git && cd camofox-browser && make build 4. docker compose --profile stealth up -d
CARGO_BUILD_JOBS is capped at 6
Straight from the script's comment: a 524-crate build at full parallelism will OOM a default 8 GB Docker Desktop VM. This is the kind of number that only exists in a repository because somebody's build died at 3am.
| setting | value | why |
|---|---|---|
| profiles | ["stealth"] | The service does not start unless you ask for it by name |
| shm_size | 2gb | A real Firefox needs real shared memory; this is why the stealth profile roughly doubles the stack's RAM footprint |
| ports | 127.0.0.1:9377:9377 | camofox-browser's own make up publishes on 0.0.0.0. This compose file does not. |
| base_url | http://host.docker.internal:9377 | crw reaches it through the host gateway, not the compose network |
| camoufox_timeout_ms | 60000 | A full minute — acceptable only because this rung runs last, on pages that already failed |
VerificationAsking crw what it can render
Whether the feature is actually compiled in is not something you should have to infer from a
failed scrape. crw reports it, and health.sh goes looking — with a deliberately loose
search, because the exact shape of the capabilities document depends on your build:
$ curl -s --max-time 10 "http://127.0.0.1:3002/v1/capabilities" \ | python3 -c ' import sys,json def walk(o): if isinstance(o,dict): for k,v in o.items(): if "render" in k.lower(): print(" ",k+":",json.dumps(v)); return walk(v) elif isinstance(o,list): for i in o: walk(i) walk(json.load(sys.stdin))' (whichever renderer key your build reports, dumped as JSON)
That walk() is a recursive search for the first key whose name contains
render, anywhere in the document. It is a small piece of defensive scripting: the
check keeps working when upstream reorganises its capabilities schema, and prints
(unavailable) rather than failing the whole health run if the endpoint is missing —
note that unlike the other three checks, this one never sets fail=1. Renderer
introspection is diagnostic, not a pass/fail condition.