Configuration and glue · MIT · Docker Compose
Web search your agent can call, running entirely on your own machine.
Six layers, two containers by default, no API key and no per-query bill. SearXNG resolves a
query into candidate URLs; fastCRW turns those URLs into clean markdown; an MCP endpoint on
127.0.0.1:3002 makes both available to any agent that speaks the protocol. Built to
replace Tavily, Exa, Serper and Firecrawl for personal use.
The diagram below is the site map. Every slab is a layer of the running system and
a section of this page; each section links to the layer's own page. Nothing here is a metaphor —
the boundaries drawn between slabs are real process, container and network boundaries, and the
annotations are the values that appear in docker-compose.yml and
config/crw.toml.
PremiseBuying search per query, or hosting it
Every hosted search API for agents — Tavily, Exa, Serper, Firecrawl — sells the same two primitives: find me pages about X, and give me the readable text of this URL. Both have mature open-source implementations. SearXNG has been aggregating around 250 search engines since long before agents existed, and fastCRW does HTML-to-markdown extraction with a browser-rendering fallback. localbrowser is the compose file, configuration and glue that puts them behind one MCP endpoint on loopback.
What that buys you: no key to rotate, no per-query bill, no third party keeping a log of every question your agent asks. What it costs you: you now operate a scraper, which means you inherit its failure mode. Search engines rate-limit and CAPTCHA the IP doing the scraping, and that IP is now yours. That failure is documented in detail rather than papered over, because it will happen.
This is configuration and glue. The hard parts are other people's work.
README.md — Credits
layer 0agentThe model that holds the tools
The top layer is not something this repository ships — it is your agent session, and the stack's security design is mostly about what that session is allowed to see. An agent with shell access, your credentials in its environment, and the ability to read arbitrary web pages is the combination Simon Willison calls the lethal trifecta: private data, untrusted content, and a way to send data out.
localbrowser's answer is not to make page content safe. It is to offer a second path in which the privileged model never reads raw page text at all — a small summariser model reads it instead, and that model has no tools and cannot act. The repository is explicit that this is defence in depth and not a guarantee.
- You supply
- Any MCP client — Claude Code, Cline, opencode, or your own
- Registered with
claude mcp add --transport http --scope user crw http://127.0.0.1:3002/mcp- Risk model
- Page content is attacker-controlled input to a model that holds tools
layer 1MCP surfaceSix tools, two shapes of answer
crw publishes its own MCP endpoint over HTTP with five tools: crw_search,
crw_scrape, crw_crawl, crw_map and
crw_extract. These return raw page markdown — fast, lossless, and expensive: 50–100k
tokens of prefill for a single search, all of it text a stranger wrote.
The repository adds a sixth tool from a different direction. mcp/websearch-mcp.py
is a 173-line stdio MCP server with no dependencies outside the standard library. It exposes
search_answer, which posts to crw's /v1/search with
"answer": true and returns the synthesised answer plus source URLs — never the page
bodies. Both sets stay registered on purpose: summaries drop exact syntax, code blocks and
tables, so the raw tools remain the right call when you need those.
- Raw path
- crw's built-in MCP server, HTTP transport,
/mcp - Synthesised path
websearch-mcp.py, stdio transport, one tool- Protocol version
2024-11-05, hand-rolled JSON-RPC over stdin/stdout
layer 2crwThe engine that fans out and joins back
fastCRW is the load-bearing component: it owns the MCP endpoint, calls SearXNG for candidate
URLs, drives the renderer ladder over each result, converts HTML to markdown, and — if a
summariser is configured — performs answer synthesis. One POST /v1/search becomes
one SearXNG query plus n page fetches plus one LLM call.
Its configuration in this repository is deliberately small: a SearXNG URL, multi-query
expansion turned on, renderer mode auto, and two commented blocks for the optional
tiers. The container itself is where most of the hardening lives — read_only,
cap_drop: ALL, no-new-privileges, 2 GB of memory and 512 PIDs — because
this is the process that parses bytes written by strangers.
- Image
ghcr.io/us/crw:latest, overridable viaCRW_IMAGE- Config
./config/crw.tomlmounted read-only at/app/config.docker.toml- Query expansion
query_expand = true— unions an entity-rewrite's results with the original
layer 3SearXNGQuery in, candidate URLs out
SearXNG queries every enabled engine in parallel and merges the results. There is no sequential fallback, which has a consequence people usually discover from a bill: a second paid API key is not a backup, it is a second charge on every search.
The instance here runs with the JSON output format explicitly enabled — without it, SearXNG
answers ?format=json with a 403 and every downstream call fails silently — and with
six independent crawlers switched on alongside the defaults: mojeek, marginalia, stract, mwmbl,
right dao and yep. They have their own indexes, so they do not share the big providers'
rate limits. That is the difference between fewer results and zero results.
- Image
searxng/searxng:2026.5.9-0cba32c15— pinned, never:latest- Formats
htmlfor the debug UI,jsonfor everything else- Limiter
false— it needs Valkey/Redis, and this instance is private
layer 4rendererURL in, clean markdown out
Reading a page is a ladder, not a single strategy. mode = "auto" starts with a
plain HTTP fetch — cheap, and already doing proper TLS fingerprinting — and escalates only when
that fails, ending, if you have built it in, at a Camoufox stealth browser running as a REST
sidecar.
The repository's own measurements of that stealth tier are unflattering and published as
such: it lost or tied on every site tested. The reason is that hard bot protection blocks on IP
and request rate, not browser fingerprint. What it is still worth is honest failure —
bot challenge detected instead of a CAPTCHA page handed to your model and scored as
a successful fetch.
- Tier 1
- Plain HTTP with TLS fingerprinting — handles most of the web
- Tier 2
- JS rendering for pages that need a DOM
- Tier 3
- Camoufox, opt-in at build time,
include_in_autoputs it last
layer 5hostThree containers, all on loopback
The substrate is one docker-compose.yml. crw depends on SearXNG being
healthy, not merely started; the health check is wget -qO- /healthz every
ten seconds. Every published port binds 127.0.0.1, including the optional Camoufox
sidecar, whose own upstream make up publishes on 0.0.0.0.
That is not a stylistic preference. crw fetches arbitrary URLs on request and has no authentication; anything that can reach it has a server-side request forgery primitive against your internal network. Reach it from another machine with an SSH tunnel, not by editing the bind address.
- Bring-up
./scripts/setup.sh— generates.env, starts the stack, verifies each layer- Verification
./scripts/health.sh— four independent checks, exits non-zero on failure- Requirements
- Docker and Compose; ~2 GB RAM base, ~4 GB with the stealth profile
Network shapeWhat actually leaves the machine
Self-hosting search changes who sees your queries, but it does not make the traffic disappear. Exactly two kinds of request cross the boundary: SearXNG asking search engines a question, and the renderer fetching a page. Both leave from your IP address. Nothing crosses inbound, because nothing is published anywhere but loopback.
host.docker.internal rather than the compose
network — they are loopback services on the host, and extra_hosts is what makes
them addressable from inside the container.
If you want the shortest path to a working system, go to
Operating the stack — two scripts and one claude mcp add
line. If you want to know whether this is safe to point at your agent, go to
Trust boundaries first.