Skip to content
Go back

Claude Code + SearXNG: Private Web Search

By SumGuy 10 min read
Claude Code + SearXNG: Private Web Search

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

Terminal window
# Simple query, default 10 results
websearch "searxng docker compose setup"
# Limit results
websearch -n 5 "caddy reverse proxy tls"
# Target a specific category and engine set
websearch --category it --engine brave,duckduckgo "rust async runtime comparison"
# Time-filter to recent results
websearch --time week "openai api rate limits"
# Raw JSON for piping
websearch --json "grafana loki query syntax" | jq -r '.results[].url'

All the flags

FlagDefaultWhat it does
-n / --num N10Number 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 / --jsonoffRaw JSON output for piping
-h / --helpUsage

Environment variables

SEARXNG_URL=http://localhost:8383 # default
WEBSEARCH_TIMEOUT=15 # seconds, default

If 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:

docker-compose.yml
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-stopped
Terminal window
mkdir searxng
docker compose up -d

The 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:

searxng/settings.yml
search:
formats:
- html
- json

Restart 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:

searxng/settings.yml
server:
limiter: false

Don’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:

Terminal window
chmod +x ~/.claude/bin/websearch
websearch "test query" # verify it works before the agent tries it

From 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:

.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:

## Search
For 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:

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.

Terminal window
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:

SituationUse
Normal “look this up” mid-taskBuilt-in — lighter, cited, no dependency
Privacy-sensitive queriesWrapper — stays on your infra
Scripted/batch search, piping to other toolsWrapper — --json mode, no rate limits
Engine/time/category control neededWrapper — built-in doesn’t expose this
Built-in unavailable (offline, LAN, Tailscale)Wrapper as fallback
Deep multi-source researchEither — 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.



Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
BirdNET-Pi for Self-Hosted Bird Identification
Next Post
fd vs find: Rust Speed vs POSIX Power

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts