diff --git a/plugins/searxng-search/README.md b/plugins/searxng-search/README.md
new file mode 100644
index 0000000..90c3083
--- /dev/null
+++ b/plugins/searxng-search/README.md
@@ -0,0 +1,179 @@
+# 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,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. |
diff --git a/plugins/searxng-search/SEARXNG-SETUP.md b/plugins/searxng-search/SEARXNG-SETUP.md
new file mode 100644
index 0000000..d4fbfae
--- /dev/null
+++ b/plugins/searxng-search/SEARXNG-SETUP.md
@@ -0,0 +1,285 @@
+# Deploying the SearXNG instance
+
+How the instance behind the `searxng_search` tool was built, and how to rebuild
+or repair it. Everything here was measured on this machine, not copied from a
+guide.
+
+- SearXNG Docker install:
+- SearXNG settings reference:
+- Plugin that consumes it: [`README.md`](README.md)
+
+## What is running
+
+| | |
+| --- | --- |
+| Host directory | `/home/xiaoshuai/Docker/searxng/` |
+| Containers | `searxng-core`, `searxng-valkey` |
+| Image | `docker.io/searxng/searxng:latest` |
+| SearXNG version | `2026.10.7-6671d89be` (image built 2026-10-07) |
+| Listen | `0.0.0.0:39217` — reachable from the LAN, no authentication |
+| Files | `docker-compose.yml`, `.env`, `core-config/settings.yml` |
+
+`docker-compose.yml` and `.env.example` are the **current official templates,
+unmodified** — both were diffed against
+`searxng/master/container/` and are byte-identical. Every decision this
+deployment makes lives in `.env` and `core-config/settings.yml`, so re-syncing
+the templates never clobbers local choices.
+
+## Files
+
+```
+/home/xiaoshuai/Docker/searxng/
+├── docker-compose.yml # official template, unmodified
+├── .env # version, bind address, port
+├── .env.example # from the template, commented out
+└── core-config/
+ └── settings.yml # mounted at /etc/searxng/
+```
+
+`core-config/` is bind-mounted over `/etc/searxng/`, so editing `settings.yml`
+on the host changes the running instance. Restart to apply:
+
+```sh
+cd /home/xiaoshuai/Docker/searxng
+docker compose restart core
+```
+
+> `settings.yml` contains `server.secret_key`. It lives under `~/Docker`, which
+> is **not** inside the git repository that tracks the OpenCode skills and
+> plugins. Do not move it into that repository.
+
+## Rebuilding from scratch
+
+```sh
+mkdir -p /home/xiaoshuai/Docker/searxng/core-config
+cd /home/xiaoshuai/Docker/searxng
+
+# 1. The official templates, from the searxng main repo.
+curl -fsSL -O https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml
+curl -fsSL -O https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example
+
+# 2. Bind address and port. 0.0.0.0 exposes it to the LAN with no auth.
+cp -i .env.example .env
+cat > .env <<'EOF'
+SEARXNG_VERSION=latest
+SEARXNG_HOST=0.0.0.0
+SEARXNG_PORT=39217
+EOF
+
+# 3. Config. Copy the body of the "settings.yml" section below.
+# Generate a real secret first:
+openssl rand -hex 32
+# paste it into server.secret_key
+
+docker compose up -d
+docker compose logs -f core
+```
+
+Pin `SEARXNG_VERSION` to a dated tag such as `2026.10.7-6671d89be` if you need
+reproducible restarts; `latest` moves.
+
+> **Do not use the `searxng/searxng-docker` repository.** It is superseded and
+> now contains only a deprecation notice — its `docker-compose.yml` is gone and
+> the raw URL 404s. The templates now live in the main repository under
+> `container/`, which is what the "Compose instancing" section of the
+> installation docs links to. Both files here were diffed against those
+> templates and are byte-identical.
+
+### Keeping the templates current
+
+```sh
+cd /home/xiaoshuai/Docker/searxng
+docker compose down
+curl -fsSLO https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml
+curl -fsSLO https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example
+docker compose up -d
+```
+
+`core-config/` is bind-mounted, so `settings.yml` survives this. Review the
+refreshed `.env.example` before overwriting `.env` — new variables may be
+needed, and your `SEARXNG_HOST` / `SEARXNG_PORT` will be lost if you replace the
+file without editing it.
+
+## The port is unusual on purpose
+
+`SEARXNG_PORT=39217` appears in three places and must agree:
+
+1. `.env` — read by the host side of the port mapping
+ `${SEARXNG_HOST:+...}${SEARXNG_PORT:-8080}:${SEARXNG_PORT:-8080}`
+2. `.env` again — `env_file` passes it into the container, and SearXNG binds
+ to it, so the container port is also 39217
+3. Nothing in `settings.yml` — port is not a SearXNG setting
+
+`0.0.0.0` is a deliberate choice: the instance is for the LAN and has no
+authentication. Set `SEARXNG_HOST=127.0.0.1` to restrict it to this machine,
+which is the right call if it is not actually being used from other devices.
+
+## settings.yml
+
+```yaml
+# Read the documentation before extending the defaults:
+# https://docs.searxng.org/admin/settings/
+#
+# This machine cannot reach DuckDuckGo / Brave / Wikipedia / Google /
+# Startpage / Qwant / Yahoo (network timeouts), so the default engine set
+# returns nothing. Only engines verified reachable from this host are kept.
+
+use_default_settings:
+ engines:
+ keep_only:
+ - baidu
+ - bing
+ - sogou
+ - "360search"
+ - mojeek
+ - yandex
+ - naver
+ - ecosia
+ - marginalia
+
+general:
+ instance_name: "SearXNG (LAN)"
+
+server:
+ secret_key: "