Skip to content

Plugin tools in catalog cannot be invoked directly: "No tool named …" #50312

Closed

Description

@ABDsheikho

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-ssh at ~/.config/opencode/plugins/remote.ts (V2 { id, async setup(ctx) }), confirmed active via /api/plugin (state.status: "active")

Reproduction

  1. 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" }] }
        },
      })
    })
  },
}
  1. Start a fresh agent session (plugin hot-reloads; loading plugin appears in the log, no load errors).
  2. Note the catalog lists the tool to the model as tools.ssh_connect (path + signature).
  3. 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/plugin shows 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 blob SHA 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

  1. LeoParkerOu commented on Sep 22, 2026

    @LeoParkerOu

    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.

  2. jeremialab commented on Sep 23, 2026

    @jeremialab

    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 probe plugin (.opencode/plugins/probe/index.ts, @opencode/[email protected], app 2.0.15):

    • The tool is definitely registered: inside setup, after ctx.tool.transform(...), ctx.tool.list() returns it
      ([..., "probe", "probe_variant_a", ...]).
    • Direct invocation from the model fails with the identical error: No tool named "probe" is currently available.
      (also fails when tried with a tools. prefix).
    • Ruled out — permissions: explicit allow rules in global and project config
      ({ "action": "probe", "resource": "*", "effect": "allow" }) make no difference; a ctx.permission.hook("evaluate", ...)
      registered by the plugin is never invoked for this tool, and forcing event.effect = "allow" changes nothing.
    • Ruled out — tool options: no options / { namespace } / { namespace, codemode: true } /
      { namespace, codemode: true, pinned: true } are all affected identically.
    • Ruled out — version drift: app 2.0.14 + package 2.0.15, and app 2.0.15 + package 2.0.15 both reproduce.
    • Ruled out — package resolution: the earlier Cannot find package '@opencode/plugin' (old V1 package name still
      present in the global config node_modules) was fixed by installing the V2 package; the plugin then loads with no
      error in the server log.

    Impact note: in this environment the code-execution workaround is unavailable because the execute permission is
    denied by policy, so plugin tools are completely unusable — there is no path to invoke them.

    Environment: opencode 2.0.15, Windows x64, opencode --standalone with isolated XDG_* roots.

  3. jeremialab commented on Sep 24, 2026

    @jeremialab

    Thanks — this explains it. Setting codemode: false on the tool fixes the issue on our side:

    • opencode 2.0.16, Windows x64
    • project-local V2 plugin, @opencode/[email protected]
    • execute permission denied by policy
    • before: the tool was present in ctx.tool.list() but missing from the model's direct tool set, and direct calls
      failed with No tool named "…" is currently available
    • after adding options: { codemode: false }: the tool appears in the model's tool list and direct calls execute
      normally (including with execute denied)

    So the default Code Mode behavior documented in this PR is exactly what we were hitting.

  4. JavaGT commented on Sep 27, 2026

    @JavaGT

    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

    export default {
      id: "agent-hygiene",
      async setup(ctx) {
        await ctx.tool.transform((editor) => {
          editor.add({ name: "bash", description: "Alias for the `shell` tool.", /* ... */ })
        })
      },
    }

    The host already exposes this capability natively as shell. After the plugin registers bash:

    • the model-facing catalog advertises tools.bash,
    • a direct bash({ command }) call fails with No 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's execute runs 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.before is dispatched. A guard that wants to observe or rewrite the call never runs, and there is no log line — a hook keyed on event.tool looks 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 put bash on 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, and tools["ns"]["tool"] are one capability with three surface names, and event.tool is the id of whichever surface happened to dispatch.

    Suggested fix directions

    1. Register ctx.tool.transform additions on the native tool list, not only the catalog-visible one — so bash(...) resolves directly.
    2. Or accept bare bash as an alias for shell at name resolution, so existing plugin aliases and the documented bash spelling both work.
    3. Either way, consider surfacing "a plugin registered this name, reachable only via the code-execution path" rather than a generic "use a tool from the available tool list", which sends the model hunting the catalog.

    Environment

    opencode v2 (binary), macOS 15 (darwin/arm64), global plugin at ~/.config/opencode/plugins/agent-hygiene/index.js, hot-reloaded and confirmed active.

  5. CyberTodBG commented on Sep 30, 2026

    @CyberTodBG

    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, grep next 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

    • opencode 2.0.20 (channel latest, binary; Linux x64 / WSL2), shell /bin/bash
    • Model: opencode-go / deepseek-v4.1-flash (hosted; Code Mode is the default)
    • Active plugins: cc-safety-net@latest plus local plugins; Code Mode enabled
    • opencode.log contains 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:

    • read 34, edit 9, write 1
    • 28 / 48 sessions affected, spanning 2026-09-22 → 2026-09-30 (up to and including 2.0.20)

    Direct calls that completed in the same window, for contrast: shell 2370, execute 1333, question 89, bash 26, webfetch 17, websearch 10, skill 5, subagent 1. 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)

    1. Direct shell call → succeeds

    2. [text] "Fixing one stray missing period from the edit, then publishing."
      Direct edit call (filePath / oldString / newString) → error:

      No tool named "edit" is currently available. Please use a tool from the available tool list.
      
    3. Reasoning:

      The edit tool isn't available directly here (Code Mode?). Actually the tools available are via execute. But this environment lists direct tools too. It says no tool named edit. Let me use sed via shell.

      Then a direct shell call runs sed -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 lists read/edit/write beside shell, 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

    1. Do not list execute-only tools in the top-level tool list, or mark them unambiguously as execute-only (e.g. tools.edit(...)), so the surface matches what dispatch accepts.
    2. When a direct call names an execute-only catalog tool, either route it transparently or return an error that names the required path instead of a bare "No tool named".
    3. Expose a config so the file tools can be made directly callable without Code Mode.
    4. Log these rejections at warn level; they were invisible in opencode.log, which made the problem hard to locate.

    Happy to attach the raw session exports (/api/experimental/session/{id}/export) if useful.

  6. aifeifei798 commented on Oct 25, 2026

    @aifeifei798

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

    request declares the tool server output result in opencode
    yes tool_calls delta, finish_reason: "tool_calls" dispatched, executes normally
    no 0 tool_calls deltas, finish_reason: "stop", the XML arrives in content no tool part, no tool.execute.before, turn ends cleanly

    The 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 the No 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 with reasoning + text and no tool part.

    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_calls deltas + "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 with options: { codemode: false } (as @jeremialab found) so it is declared in the request and dispatched natively.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions