Two docs next to the plugin: README.md for the tool, SEARXNG-SETUP.md for the
Docker deployment behind it. Both are written from measurements against the
running instance, not from a guide.
The setup doc exists because the deployment decisions were only recorded as
comments inside settings.yml, which means they are unfindable unless you
already know to look there. It also records the parts that are not guessable:
why the port is 39217, why limiter is off, why the UI locale is zh-Hans-CN
rather than zh-CN, and which engine is broken how.
Corrections found while writing it:
- The rebuild steps pointed at searxng/searxng-docker, which is superseded. That
repository now holds only a deprecation notice and its docker-compose.yml raw
URL 404s. The templates moved to searxng/master/container/. Verified both
local files are byte-identical to the current templates, so the deployment
was always the current one -- only the documented URL was wrong.
- The plugin README claimed the entrypoint comes from package.json. This
directory has no package.json; the entrypoint falls back to index.ts. Restarted
the service with both .md files in place to confirm they do not affect
resolution.
Documents the two SearXNG behaviours that fail silently rather than loudly,
both verified by reading the engine label on each result:
- An unknown engine name is ignored and the default engine set is searched
instead, with no error. engines=mojeek returns 10 bing results and looks like
a success, as does a typo. The only reliable signal is whether the requested
engine appears in results[].engine.
- /config is not a way to check this. It reported disabled: false for every
engine while baidu and sogou were in fact being dropped, because it reflects
upstream defaults and not the local disabled: true overrides.
Also records that ctx.options is always {} for a directory-discovered plugin,
so the four options in PluginConfig cannot be supplied without moving the
plugin out of plugins/ and registering it in opencode.jsonc. Verified by
writing JSON.stringify(ctx.options) to a file from inside setup().
No secret_key value is in either file; settings.yml stays outside this
repository under ~/Docker.
7.9 KiB
searxng-search
An OpenCode V2 plugin that adds a searxng_search tool backed by a
self-hosted SearXNG instance.
It does not replace OpenCode's built-in websearch provider. It registers
one extra, opt-in tool, so nothing existing changes behaviour. Replacing a
built-in provider means taking over a key the user chose; adding a tool does
not.
- OpenCode V2 plugin docs: https://opencode.ai/v2/docs/build/plugins/
- OpenCode plugin config: https://opencode.ai/v2/docs/plugins
- SearXNG search API: https://docs.searxng.org/dev/search_api.html
- How the paired instance was deployed:
SEARXNG-SETUP.md
Requirements
-
A reachable SearXNG instance with
jsoninsearch.formats(disabled upstream by default). SeeSEARXNG-SETUP.md. -
@opencode/plugininstalled at your exact OpenCode version:opencode --version # e.g. v2.0.24 cd ~/.config/opencode npm i @opencode/plugin@2.0.24 # must match -
A service restart. The running service caches module resolution, so a freshly installed package is invisible until then:
opencode service restart
Check the log if the tool does not appear:
grep -i "loading plugin" ~/.local/share/opencode/log/opencode.log | tail
A line like this is good:
loading plugin id=/home/you/.config/opencode/plugins/searxng-search
A Cannot find package '@opencode/plugin' error means the package is
missing, not that the docs are wrong. See the stale-package trap in
../../skills/opencode-plugins/SKILL.md.
How it is loaded
By directory discovery, not by config. OpenCode loads every immediate
subdirectory of ~/.config/opencode/plugins/ as a plugin package and resolves
an entrypoint inside it. This directory has no package.json, so the entrypoint
falls back to index.ts:
loading plugin id=/home/you/.config/opencode/plugins/searxng-search
entrypoint=file:///.../plugins/searxng-search/index.ts
Sibling files such as this README.md do not affect that resolution — verified
by restarting the service with the docs in place. There is deliberately nothing
to add to opencode.jsonc.
That has a consequence worth knowing before you try to configure it:
ctx.optionsis always{}for a discovered plugin.
Options reach a plugin only through an object entry in the plugins array in
opencode.json(c), which is keyed by package or path. A directory that was
found by scanning has no such entry, so it receives no options at all. This was
verified by writing JSON.stringify(ctx.options) to a file from inside
setup() — the file contained {}.
The baseUrl / engines / timeoutMs / maxSnippetChars options in
PluginConfig are therefore inert as shipped. Two working alternatives:
| Approach | How |
|---|---|
| Environment variable (easiest) | export SEARXNG_URL=http://host:39217 before starting OpenCode |
| Register explicitly | Move this directory out of plugins/, then add a {"package": "<path>", "options": {...}} entry to opencode.jsonc |
To keep the engines default editable without moving anything, change
DEFAULT_ENGINES in index.ts and push — see "Push every change" in
../../skills/opencode-plugins/SKILL.md.
If you do register it explicitly, remove the directory from plugins/
first. Otherwise the plugin loads twice: once by discovery, once by config,
and the tool is registered against the same name twice.
The tool
Effective name: searxng_search (tool name + _ + namespace).
| Argument | Type | Notes |
|---|---|---|
query |
string, required | The search query. |
engines |
string | Comma-separated. Omit for the instance default. |
category |
string | Only sent when you pass it — see the additive trap below. |
language |
string | e.g. zh-CN, en-US. Omit for the instance default. |
safesearch |
0 | 1 | 2 | Omit for the instance setting. |
pageno |
number | From 1. Engines without paging are dropped above page 1. |
time_range |
string | day, week, month, year. Engines without it are dropped entirely. |
At setup() the plugin reads the instance's /config and bakes what the
instance actually supports into each argument's description, so the model is
told which engines lose paging or time filtering instead of discovering it by
returning fewer results.
Two SearXNG behaviours that silently mislead
Both are handled, both are worth understanding because they fail silently.
1. categories is additive, not a filter. If you pass both engines and
category, SearXNG takes the union: the named engines plus every engine
enabled by default in that category. A careful engines=bing,360search becomes
bing,360search + whatever the category contributes. The plugin therefore
sends categories only when the caller explicitly asked for a category, never
alongside an engine list it chose itself.
2. An unknown engine name falls back instead of erroring. If engines=
names something the active engine list does not contain, SearXNG ignores the
name and searches with the default engine set. No error, no
unresponsive_engines entry, and results that look like a success. Verified on
this instance:
engines=bing -> bing x10
engines=360search -> 360search x7
engines=baidu -> 0 results, unresponsive_engines empty
engines=mojeek -> bing x10 <-- fell back, mojeek is not active
engines=duckduckgo -> bing x10 <-- fell back
engines=nosuchengine -> bing x10 <-- typo: also fell back, no warning
Three signatures, and only the third means the engine was really used:
| Observation | Meaning |
|---|---|
0 results and empty unresponsive_engines |
Engine is disabled or not in the active list |
| Results, but none of them from the requested name | Fell back to the default engine set |
| Results including the requested name | Genuinely used |
/config is not a reliable way to check this. On this instance it reports
disabled: false for every engine while baidu and sogou are in fact being
dropped, because the listing reflects upstream defaults rather than the
disabled: true overrides in settings.yml. Diagnose with the table above.
Measured state of the paired instance
SearXNG 2026.10.7-6671d89be, docker.io/searxng/searxng:latest (built
2026-10-07), http://127.0.0.1:39217.
| Engine | Status |
|---|---|
bing |
Active. Needs base_url: https://cn.bing.com; www.bing.com blocks scraping from this network. |
360search |
Active. |
yandex, naver |
Active and reachable, but no useful Chinese index — left out of the plugin's default engine list. |
baidu |
Disabled. Answers with a CAPTCHA and suspends the client IP for an hour. |
sogou |
Disabled. The upstream engine raises AttributeError in this release. |
mojeek, ecosia, marginalia |
Dropped by keep_only; not reachable from this network. |
bing supports neither paging nor time_range, so on this instance pageno > 1
and time_range both narrow the result to nothing useful. The tool says so in
the argument descriptions rather than returning a silently short list.
Troubleshooting
| Symptom | Check |
|---|---|
| Tool absent from the catalog | grep -i "loading plugin" ~/.local/share/opencode/log/opencode.log | tail |
Cannot find package '@opencode/plugin' |
Package missing or wrong version. Match opencode --version exactly. |
| Installed it but still missing | opencode service restart — the service caches module resolution. |
fetch failed / no results at all |
curl -s "http://127.0.0.1:39217/config" | head -c 200 — is it up? |
| JSON format rejected | json must be in search.formats in settings.yml. |
| Results from an unexpected engine | The fallback in the table above. Check which engines actually appear. |
| Everything works, then stops after an edit | The running service loaded the old file. Restart it. |