Repository navigation
Description
Summary
Tools registered by a V2 plugin appear in the model-facing tool catalog with a tools. namespace prefix, but every direct invocation from the agent fails at the server with No tool named "…" is currently available, even when called without the prefix. The same tool executes fine when invoked through the code-execution tool.
Environment
- opencode version: 2.0.5 (channel: latest)
- OS: Linux 7.2.6-arch2-1 x86_64 (Arch)
- Terminal: xterm-256color, truecolor
- Shell: /usr/bin/bash
- Install/channel: latest (binary)
- Active plugins: source plugin
opencode-sshat~/.config/opencode/plugins/remote.ts(V2{ id, async setup(ctx) }), confirmed active via/api/plugin(state.status: "active")
Reproduction
- Write a V2 plugin that registers a tool (minimal form used):
export default {
id: "example",
async setup(ctx: any) {
await ctx.tool.transform((editor: any) => {
editor.add({
name: "ssh_connect",
description: "Open a persistent SSH connection to a remote server.",
input: { type: "object", properties: {}, additionalProperties: false },
async execute(args: any, context: any) {
return { content: [{ type: "text", text: "ran" }] }
},
})
})
},
}- Start a fresh agent session (plugin hot-reloads;
loading pluginappears in the log, no load errors). - Note the catalog lists the tool to the model as
tools.ssh_connect(path + signature). - Have the model call it directly — attempt A:
tools.ssh_connect, attempt B:ssh_connect.
Expected Behavior
The tool call resolves against the registered tool and executes (the plugin runs, returns its result).
Actual Behavior
Both direct invocations fail server-side with, respectively:
- No tool named "tools.ssh_connect" is currently available. Please use a tool from the available tool list.
- No tool named "ssh_connect" is currently available. Please use a tool from the available tool list.
Invoking the same tool through the code-execution tool ( tools.ssh_connect({host}) inside a {code} block) succeeds and the plugin's execute runs normally.
Additional Context
- Consistent in 6/6 fresh sessions; fully reproducible after opencode service restart and after plugin file edits (watcher reload).
- The tool is genuinely registered:
/api/pluginshows the plugin active, the tool appears in the catalog with the correct description/signature, and the code-execution path reaches the plugin. - Workarounds tried: calling with the tools. prefix, without the prefix, and via the code-execution path — only the last works.
- The plugin under test is a work-in-progress port of the open-source snez/opencode-ssh plugin (https://github.com/snez/opencode-ssh,
src/index.ts, upstream blobSHA f5219604…). - Host/config details sanitized; no credentials involved.
Plugins
An work-in-progress V2 port of https://github.com/snez/opencode-ssh
OpenCode version
opencode v2.0.5
Activity
LeoParkerOu commentedon Sep 22, 2026
I’d like to work on this issue.
I can reproduce the mismatch between the V2 plugin tool catalog and direct tool dispatch: the tool is advertised to the model, but both the namespaced and unprefixed calls fail, while the code-execution path succeeds.
I’ll trace the catalog-to-dispatch mapping, add a regression test for direct invocation, and keep the code-execution path and existing plugin behavior unchanged.
jeremialab commentedon Sep 23, 2026
Same behavior here on 2.0.15 (Windows x64), with a project-local V2 plugin — confirming this is not fixed and not version-specific.
Additional data points from a minimal
probeplugin (.opencode/plugins/probe/index.ts,@opencode/[email protected], app 2.0.15):setup, afterctx.tool.transform(...),ctx.tool.list()returns it(
[..., "probe", "probe_variant_a", ...]).No tool named "probe" is currently available.(also fails when tried with a
tools.prefix).(
{ "action": "probe", "resource": "*", "effect": "allow" }) make no difference; actx.permission.hook("evaluate", ...)registered by the plugin is never invoked for this tool, and forcing
event.effect = "allow"changes nothing.options: no options /{ namespace }/{ namespace, codemode: true }/{ namespace, codemode: true, pinned: true }are all affected identically.Cannot find package '@opencode/plugin'(old V1 package name stillpresent in the global config
node_modules) was fixed by installing the V2 package; the plugin then loads with noerror in the server log.
Impact note: in this environment the code-execution workaround is unavailable because the
executepermission isdenied by policy, so plugin tools are completely unusable — there is no path to invoke them.
Environment: opencode 2.0.15, Windows x64,
opencode --standalonewith isolatedXDG_*roots.jeremialab commentedon Sep 24, 2026
Thanks — this explains it. Setting
codemode: falseon the tool fixes the issue on our side:@opencode/[email protected]executepermission denied by policyctx.tool.list()but missing from the model's direct tool set, and direct callsfailed with
No tool named "…" is currently availableoptions: { codemode: false }: the tool appears in the model's tool list and direct calls executenormally (including with
executedenied)So the default Code Mode behavior documented in this PR is exactly what we were hitting.
JavaGT commentedon Sep 27, 2026
Additional evidence: same defect on a built-in capability alias, macOS/arm64
This is not specific to third-party plugin tools. It reproduces for a built-in capability registered through
ctx.tool.transform→editor.add({ name }), and the consequence is worse than a failed call: hooks never see the failure.Minimal reproduction
The host already exposes this capability natively as
shell. After the plugin registersbash:tools.bash,bash({ command })call fails withNo tool named "bash" is currently available. Please use a tool from the available tool list.,tools.bash({ command })inside a{code}block succeeds — the plugin'sexecuteruns normally.So the alias is reachable only through the code-execution path, exactly as described here.
Why this is worse than a 404
Name resolution fails before
tool.execute.beforeis dispatched. A guard that wants to observe or rewrite the call never runs, and there is no log line — a hook keyed onevent.toollooks installed while silently never firing.Measured over a 48h window on this host: 321 calls failed with
No tool named "bash". Splitting at the plugin commit: 273 before, 44 after.editor.add({ name: "bash" })did not putbashon the native tool list.The existing harness lesson generalises from this: match hooks on input shape (
typeof event.input?.command === "string"), never on a tool-name string.shell,bash, andtools["ns"]["tool"]are one capability with three surface names, andevent.toolis the id of whichever surface happened to dispatch.Suggested fix directions
ctx.tool.transformadditions on the native tool list, not only the catalog-visible one — sobash(...)resolves directly.bashas an alias forshellat name resolution, so existing plugin aliases and the documentedbashspelling both work.Environment
opencode v2 (binary), macOS 15 (darwin/arm64), global plugin at
~/.config/opencode/plugins/agent-hygiene/index.js, hot-reloaded and confirmed active.CyberTodBG commentedon Sep 30, 2026
Additional evidence: built-in Code-Mode tools fail the same way, and there is no escape hatch for them
This is reproducible for built-in tools, not only plugin tools. In Code Mode the model-facing catalog lists
read,edit,write,glob,grepnext to tools that are directly callable (shell,question,skill,subagent,webfetch,websearch), and direct calls to the file tools are rejected server-side with the identical message.Environment
/bin/bashcc-safety-net@latestplus local plugins; Code Mode enabledopencode.logcontains no line for these failures — the server rejects before logging, so the error is only visible in the session store.Measured impact
Across the 48 most recent sessions (excluding two that were investigating this error), 44 direct tool calls were rejected:
read34,edit9,write1Direct calls that completed in the same window, for contrast:
shell2370,execute1333,question89,bash26,webfetch17,websearch10,skill5,subagent1. The model is not inventing a name — it is choosing file tools from a catalog it cannot distinguish from the directly-callable ones.Clean transcript (session
ses_f0f1c1fdbffenlXsFKCIfdVqCN, 2026-09-30)Direct
shellcall → succeeds[text] "Fixing one stray missing period from the edit, then publishing."Direct
editcall (filePath/oldString/newString) → error:Reasoning:
Then a direct
shellcall runssed -i, which succeeds.That is the exact cost this issue describes: one rejected call, a reasoning detour, and a workaround — repeatedly.
Why this is distinct from the plugin-tool case
A plugin tool can be fixed with
options: { codemode: false }(per @jeremialab above). The built-in file tools have no equivalent switch, so the user cannot opt out. Because the catalog listsread/edit/writebesideshell, the rejection also reads as the model "hallucinating" a tool, even though the name came straight from the advertised list. The instructions do say these tools are not directly callable, but they still appear in the available-tools list, and that is what the model reacts to.Suggested fixes
tools.edit(...)), so the surface matches what dispatch accepts.opencode.log, which made the problem hard to locate.Happy to attach the raw session exports (
/api/experimental/session/{id}/export) if useful.fix(opencode): mark plugin tools as direct-callable (codemode: false)
aifeifei798 commentedon Oct 25, 2026
One more failure shape for the same root cause — worth adding because it produces no error at all, so there is nothing for the user or a plugin hook to see.
With Code Mode on, a plugin tool (or a Code-Mode-only built-in) is not in the request's
toolsarray. The model still emits the call in its raw template form, e.g.<tool_call><function=ctx_stats></function></tool_call>. What the client then receives depends on the server:tool_callsdelta,finish_reason: "tool_calls"tool_callsdeltas,finish_reason: "stop", the XML arrives incontenttool.execute.before, turn ends cleanlyThe second row is the same defect seen from the other side: servers that keep a
<tool_call>naming an undeclared tool as ordinary content (OpenAI-compatible servers that validate names against the request do — verified on a llama.cpp-derived one) never let the call reach name resolution, so theNo tool named "…"error from this issue's title never appears. What the user sees is a dead call block in the UI, and what the session store shows is an assistant message withreasoning+textand notoolpart.I hit this on v2.1.4 and originally mis-filed it as a dispatch hang: #58778 (now closed as a duplicate of this). The replay evidence is in that issue's follow-up comment: identical XML, tool declared vs not (3
tool_callsdeltas +"tool_calls"vs 0 deltas +"stop"), plus the session-store dump.Both mitigations mentioned above hold in this shape too: call through
execute(tools.<name>(…)), or register the tool withoptions: { codemode: false }(as @jeremialab found) so it is declared in the request and dispatched natively.