Your AI Coding Agent Needs to Search the Web. Here’s the Problem.
You’re mid-session with Claude Code. The agent needs to check a library’s changelog, look up an obscure iptables flag, or verify whether some package still exists. It reaches for its built-in WebSearch tool, fires off the query, and you get a clean structured result with citations. Lovely.
Except: that query just went to Anthropic’s search vendor. The query had your internal project name in it. Or your company’s proprietary tooling stack. Or you’re just the kind of person who doesn’t love the idea of every research lookup an AI agent makes on your behalf being routed through a third-party black box. No judgment — I’ve been running a self-hosted SearXNG instance for over a year and wiring it into my local tooling one way or another.
This article covers how to give Claude Code (or any agent that can shell out via Bash — Aider, Cline, whatever) a private web search fallback using a small Bash wrapper around SearXNG’s JSON API. And critically: when to use it versus the built-in tool, because this is a complement story, not a “ditch the built-in” story.
Full example: Grab the wrapper, Compose file, and SearXNG settings snippet at github.com/KingPin/sumguy-examples/productivity/claude-code-searxng-search
Two Options, One Job
Let’s get the lay of the land before we go deep.
Built-in WebSearch is Claude Code’s native tool. Anthropic wires it up, it returns structured/cited results, it just works. It’s the right default for interactive “look this up for me” tasks mid-session. Downside: you have no control over which engines it hits, the query routing is opaque, and it’s been subject to regional availability gaps.
The SearXNG wrapper is a plain Bash script sitting on your PATH. When the agent shells out via its Bash tool and runs websearch "your query", it hits your local SearXNG instance, parses the JSON results, and prints numbered title / URL / snippet tuples back to the agent’s context. No MCP server required, no pip installs, no API keys. Just a shell script.
You could wrap this as an MCP server for a cleaner integration — but a CLI on your PATH is the lower-friction path and it works fine for most use cases.
How the Wrapper Works
The script lives at ~/.claude/bin/websearch (or wherever you keep local binaries on your PATH). It’s pure Bash with an embedded Python snippet that uses only stdlib (urllib, json) — no dependencies to install.
Basic usage
# Simple query, default 10 resultswebsearch "searxng docker compose setup"
# Limit resultswebsearch -n 5 "caddy reverse proxy tls"
# Target a specific category and engine setwebsearch --category it --engine brave,duckduckgo "rust async runtime comparison"
# Time-filter to recent resultswebsearch --time week "openai api rate limits"
# Raw JSON for pipingwebsearch --json "grafana loki query syntax" | jq -r '.results[].url'All the flags
| Flag | Default | What it does |
|---|---|---|
-n / --num N | 10 | Number of results |
-c / --category | (SearXNG default) | general, images, news, videos, music, files, science, it, map |
-e / --engine | (SearXNG default) | Comma list: brave,duckduckgo,mojeek |
-t / --time | (none) | day, week, month, year |
-l / --lang | (SearXNG default) | Language code: en, en-US, de, etc. |
-s / --safe | (SearXNG default) | 0 off, 1 moderate, 2 strict |
-j / --json | off | Raw JSON output for piping |
-h / --help | — | Usage |
Environment variables
SEARXNG_URL=http://localhost:8383 # defaultWEBSEARCH_TIMEOUT=15 # seconds, defaultIf your SearXNG is on a different host or Tailscale IP, just export SEARXNG_URL in your shell profile and the script picks it up.
Security note worth keeping
This one matters when you’re pointing an autonomous agent at a script: all user-supplied values (the query string, engine list, etc.) are passed to the embedded Python as environment variables, not interpolated into the source string. That means a query containing $HOME, backticks, semicolons, or any other shell metacharacter cannot break out or inject. The agent can construct arbitrarily weird queries and the script stays safe.
Setting Up SearXNG
If you’re not already running SearXNG, the fastest path is Compose:
services: searxng: image: searxng/searxng:latest container_name: searxng ports: - "8383:8080" volumes: - ./searxng:/etc/searxng environment: - SEARXNG_BASE_URL=http://localhost:8383/ - SEARXNG_SECRET_KEY=changeme_use_openssl_rand_hex_32 restart: unless-stoppedmkdir searxngdocker compose up -dThe gotcha that will get you
SearXNG serves HTML only by default. Request the JSON API without enabling it and SearXNG hands back an HTTP 403 Forbidden — so the wrapper bails with ERROR: cannot reach SearXNG at http://localhost:8383 (Forbidden). (If your instance ever returns a 200 with non-JSON instead, the wrapper gets more specific: ERROR: SearXNG did not return JSON. Is the JSON format enabled in settings.yml?) Either way, the fix is the same.
Fix it by adding json to search.formats in searxng/settings.yml:
search: formats: - html - jsonRestart the container. One line. Easy to miss, annoying when you do.
While you’re in settings.yml, if this instance is private (localhost only, or behind a Tailscale ACL), you can also disable the rate limiter — it’s designed to protect public instances from abuse and will throttle an agent that’s hammering queries in a tight loop:
server: limiter: falseDon’t disable this on a public-facing instance. For a private box that only you and your agent can reach, it’s fine.
Wiring It Into Claude Code
Drop the script at ~/.claude/bin/websearch (or anywhere on your PATH) and make it executable:
chmod +x ~/.claude/bin/websearchwebsearch "test query" # verify it works before the agent tries itFrom Claude Code’s perspective, websearch is just a Bash command. The agent calls it via its Bash tool the same way it would call git, curl, or jq. You don’t need to configure anything special — if it’s on PATH and the agent’s Bash tool is allowed to run it, it works.
To reduce permission prompts, add it to your allowed commands in .claude/settings.json:
{ "permissions": { "allow": [ "Bash(websearch:*)" ] }}If you want the agent to reach for it automatically rather than waiting to be asked, you can add a note in your CLAUDE.md project file describing when to use it:
## SearchFor privacy-sensitive queries or when you need engine/time filtering, use the`websearch` CLI instead of the built-in WebSearch tool. Use `websearch --json`when you need to pipe results into other tools.That’s it. No MCP server, no npm packages, no OAuth dance.
The Engine / CAPTCHA Reality
Here’s the thing nobody mentions in SearXNG tutorials: when an agent is doing research and fires off 15 queries in 10 minutes, all from the same IP, Google and Bing notice. They start returning CAPTCHAs or silently degrading results. You won’t always see it — the results just quietly get worse, or your logs show 429s.
Practical mitigations:
- Lean on Brave, DuckDuckGo, and Mojeek. These are more API-friendly and handle automated traffic better. Set them as defaults in
settings.ymlor use-e brave,duckduckgoin your queries. - Disable Google as a default engine on your instance. You can still enable it per-query if you want, but having it active for every search will get your instance soft-blocked sooner rather than later.
- Put a proxy in front if you’re doing serious volume. SearXNG can route outgoing requests through Tor — set
using_tor_proxy: trueand aproxies:block underoutgoing:insettings.yml— but latency goes up and some engines block Tor exits anyway. - Use
--timeand--categoryflags to narrow queries. Fewer, more targeted queries means less engine churn and better results anyway.
For normal coding-agent workloads — a few searches per session — you’ll be fine with the default setup. It’s only batch/research workflows where this becomes a real issue.
The Honest Token Trade-Off
Let’s not pretend there’s no cost here, because there is.
The built-in WebSearch tool returns structured, cited results that the Claude API knows how to handle. The token overhead is relatively low and the citations integrate cleanly into the response. It’s a first-class tool with first-class integration.
The wrapper’s output is plain text that lands in the agent’s context as raw Bash output: numbered title/URL/snippet tuples. 10 results at 300 chars per snippet is around 3,000 characters of context before the agent has even started reasoning about the results. That’s the real downside.
The fix is to distill the output before it hits the conversation. A few patterns that work:
Option 1: Limit results aggressively. websearch -n 3 "query" instead of the default 10. The agent usually only needs the top few results anyway.
Option 2: Use --json and pipe to jq to extract only what you need.
websearch --json "prometheus alertmanager config" | jq '.results[0:3][] | {title, url}'Now only the title and URL of the top 3 results enter context — no snippets, minimal tokens.
Option 3: Have the agent summarize in a separate step. Ask it to search, then distill the results into a single-sentence summary before continuing. Keeps the conversation clean.
None of these eliminate the token overhead completely, but they bring it close to parity with the built-in tool for most practical workloads.
When to Use Which
Here’s the recommendation without the hand-wraving:
| Situation | Use |
|---|---|
| Normal “look this up” mid-task | Built-in — lighter, cited, no dependency |
| Privacy-sensitive queries | Wrapper — stays on your infra |
| Scripted/batch search, piping to other tools | Wrapper — --json mode, no rate limits |
| Engine/time/category control needed | Wrapper — built-in doesn’t expose this |
| Built-in unavailable (offline, LAN, Tailscale) | Wrapper as fallback |
| Deep multi-source research | Either — but a dedicated research orchestration beats both raw tools |
The default should still be the built-in tool for interactive work. It’s lower friction, no moving parts to maintain, and the citations are handled cleanly. The wrapper earns its place for privacy-sensitive work, batch use, and as the fallback you’re glad you set up when the built-in isn’t available.
Should You Bother?
If you’re already running a SearXNG instance for personal browsing — yes, absolutely. The incremental cost of dropping a small Bash script on your PATH is essentially zero and you get private search for your coding agent immediately. The “is the JSON format enabled” gotcha aside, it works on first setup.
If you’re not running SearXNG and you’d need to spin it up just for this — the calculation changes a bit. The Compose setup takes maybe 15 minutes, but you’re now maintaining a container and occasionally fighting engine CAPTCHAs. Worth it if privacy is genuinely a concern, or if you do enough batch/scripted search work that the --json pipeline mode earns its keep. Not worth it if you’re just looking for a way to avoid the built-in tool you already have.
Honestly, the sweet spot is treating this as infrastructure you run anyway (SearXNG is useful for browser search too) and letting the agent benefit from it as a side effect. Running it behind Tailscale or on your homelab box means it’s always reachable from wherever you’re working, private by construction, and available as a fallback when the built-in WebSearch has a bad day.
Your 2 AM self debugging a production incident will appreciate not having to think about which search path the agent is taking.