# 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: - OpenCode plugin config: - SearXNG search API: - How the paired instance was deployed: [`SEARXNG-SETUP.md`](SEARXNG-SETUP.md) ## Requirements 1. A reachable SearXNG instance with `json` in `search.formats` (disabled upstream by default). See `SEARXNG-SETUP.md`. 2. `@opencode/plugin` installed at **your exact OpenCode version**: ```sh opencode --version # e.g. v2.0.24 cd ~/.config/opencode npm i @opencode/plugin@2.0.24 # must match ``` 3. A service restart. The running service caches module resolution, so a freshly installed package is invisible until then: ```sh opencode service restart ``` Check the log if the tool does not appear: ```sh 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.options` is 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": "", "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,yandex` becomes `bing,yandex` + 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. | | `yandex` | Active. Best measured relevance here on both Chinese and English technical queries; first result for a V1→V2 migration query is the migration doc itself. Default. | | `bing` | Active, but needs `base_url: https://cn.bing.com`; `www.bing.com` blocks scraping from this network. Default. Supports neither paging nor `time_range`. | | `naver` | Active and strongest on English, but 0/9 relevant on a Chinese query, so it is left out of the defaults rather than allowed to dilute them. Pass `engines=naver` explicitly for English lookups. | | `360search` | Active, and the **worst** engine on both query types: 0/5 relevant on a Chinese query, where it splits on "V1"/"V2" and returns Android and CANN version-difference articles. Removed from the defaults. | | `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. |