Skip to content

shell tool description names the backend shell, so models call a non-existent bash tool #57656

Closed

Description

@weierliteyangming-hue

Symptom

Models routinely call a tool named bash. This build registers only shell, so the call fails immediately:

No tool named "bash" is currently available. Please use a tool from the available tool list.

The model then retries with shell and the command runs. Cost: one wasted tool call, and an error surfaced to the user mid-task.

Why the model does it

Ranked, from the actual signals a model reads:

  1. The shell tool's own description uses "bash" as the verb. On Windows the description reads:

    Commands run on Windows using bash.

    This text is read at tool-selection time on every turn, and it names the interpreter rather than the tool. It is the strongest single cause.

  2. V2 documents bash as an alias of shell (docs: "shell tool named shell (bash kept as alias)"), so bash looks like a legitimate name in this ecosystem.

  3. Model prior: nearly every other coding agent names its command tool bash.

What this is not

Not a relay/proxy renaming tools. This is not the shell_client pattern from #56193 / #56794 - nothing rewrites the tool list here; the model reads the description above and types bash itself. Related, though: in #56193 a maintainer confirmed the description embeds the backend shell name (there, "using powershell"), the same text-vs-registration mismatch.

Reproduction

  1. Windows, opencode.json with "shell": "C:\Program Files\Git\bin\bash.exe".
  2. Ask the agent to run a shell command in a fresh session.
  3. Observe the tool-selection step: the model emits bash a percentage of the time (intermittent, model-dependent).
  4. The call fails with No tool named "bash" is currently available; the retry with shell succeeds.

Suggested fix (either is enough)

  • Expose bash as a real entry in the tool catalogue (an alias of shell), so the name the description advertises actually resolves; or
  • Reword the description so the tool name is unambiguous, e.g. Run a shell command (this tool is named "shell"; it executes via bash on Windows).

Environment

  • opencode v2.0.32 (bundled with OpenChamber 2.0.1)
  • Windows 11, Git Bash at C:\Program Files\Git\bin\bash.exe
  • opencode.json shell field points at that bash.exe
  • Plugins cannot work around it: the V2 plugin API only exposes ctx.tool.hook("execute.before", event); there is no tool-registration or tool-rename hook.

URL:

Activity

  1. opencode-agent commented on Oct 21, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up!

    To restate the request: models sometimes call a bash tool because the shell tool's description names the interpreter ("Commands run on Windows using bash."). You'd like either a real bash alias for shell, or wording that makes the tool's name clear. As it stands this is working as designed: the tool is registered as shell, the description correctly names the configured shell, and the model recovers after the error. So I'm treating this as an improvement request, and maintainers will consider it.

    You can get rid of the wasted call today with a plugin. The execute.before hook can change event.tool, and OpenCode looks up the tool from that field after the hook runs, so a bash call can be redirected to shell. It then goes through shell's normal permission checks:

    // .opencode/plugins/bash-alias.ts  (or ~/.config/opencode/plugins/bash-alias.ts for all projects)
    import { Plugin } from "@opencode/plugin"
    
    export default Plugin.define({
      id: "bash-alias",
      async setup(ctx) {
        await ctx.tool.hook("execute.before", (event) => {
          if (event.tool === "bash") event.tool = "shell"
        })
      },
    })

    Files in .opencode/plugins/ are loaded automatically. You can also point to the file from plugins in opencode.json. I haven't run this inside OpenChamber, so let us know if it doesn't load there.

    Related issues:

  2. weierliteyangming-hue commented on Oct 21, 2026

    @weierliteyangming-hue
    Author

    Confirmed working inside a third-party desktop harness, so here are the details the issue asked for.

    Environment: opencode v2.0.32 bundled with OpenChamber 2.0.1, Windows 11, opencode.json has "shell": "C:\Program Files\Git\bin\bash.exe". The shell tool description in this build reads exactly: Commands run on Windows using bash.

    The event.tool redirect works — and the plugin loads in this harness. A local plugin in the global plugin dir (~/.config/opencode/plugins/bash-alias.ts) is picked up by the config watcher, confirmed in the server log:

    timestamp=2026-10-21T07:08:10.297Z level=INFO run=171d4cf5
    msg="loading plugin" id="C:\Users\username\.config\opencode\plugins\bash-alias.ts"
    entrypoint=file:///C:/Users/username/.config/opencode/plugins/bash-alias.ts
    

    One correction for anyone copying the snippet: the Plugin.define({ ... }) form from the comment above did not match what this build actually loads from plugins/*.ts. The working shape here is the plain object export, same as other local plugins:

    export default {
      id: "bash-alias",
      async setup(ctx: any) {
        await ctx.tool.hook("execute.before", (event: any) => {
          if (String(event?.tool ?? "").toLowerCase() === "bash") {
            event.tool = "shell";
          }
        });
      },
    };

    (ctx.tool.hook("execute.before", …) with a mutable event.tool is confirmed real in this build — another local plugin already reads event.tool and mutates event.input.command the same way.)

    Since it sits in the global plugin directory it applies to every project, not just one.

    One thing worth considering upstream: the model-side cost is not just one wasted call. When the harness surfaces the failure as a user-visible banner mid-task, the user sees an error and has to decide whether the task is still running. Two cheap fixes would remove that entirely — register bash as a real catalogue alias of shell, or keep the backend name out of the description, e.g. Run a shell command (this tool is named "shell"; on Windows it executes via bash).

  3. weierliteyangming-hue commented on Oct 23, 2026

    @weierliteyangming-hue
    Author

    Still present on v2.0.32, and it is generated per-request — here is where

    Follow-up with the details, now that the event.tool redirect is confirmed working.

    1. Reproduced today on v2.0.32. Windows 11, "shell": "C:\\Program Files\\Git\\bin\\bash.exe", opencode --version → 2.0.32 (desktop harness). The shell tool's description still reads, verbatim: Commands run on Windows using bash.

    2. The source, so nobody has to hunt for it. packages/core/src/tool/plugin/shell.ts on dev:

    const description = (shell?: string) =>
      [
        "Execute a shell command and return its output.",
        ...(shell ? [`Commands run on ${OS} using ${shell}.`] : []),
        ...
      ].join(" ")

    and it is assigned per registration, same file:

    tool.description = description(ShellSelect.name(yield* compatibleShell))

    So ${shell} is the configured backend shell's name, interpolated straight into the text a model reads at tool-selection time. Two consequences:

    • It is not a stale constant. Grepping the binary for that sentence finds nothing — it only exists after concatenation, so fixing strings/docs cannot touch it. The template is the bug.
    • It is not specific to bash. Whatever shell is configured gets advertised as if it were a tool name: zsh, pwsh, fish become equally plausible emissions. bash is merely the likeliest, because it also matches the dominant model prior.

    3. This is the text/schema mismatch #56193 hit from the outside. That thread closed as not-planned against 9Router (it renamed shell → shell_client because the description differed from its saved copy), and the reasoning there restates the mechanism: "shell is the only shell tool it registers, and the description changes with the platform". The rename was 9Router's, but the trigger was this sentence advertising a name that is not in the catalogue.

    4. The cost is not always one call. When the model recovers it is one wasted call plus a user-visible error banner mid-task. #57584 tracks a case where the emission persists over many turns — different cause per that thread (model/history-driven, not descriptive), same failure mode, unbounded cost once the pattern is in the history. The execute.before plugin removes the symptom for users who know the bug exists and can write TypeScript; a default install has nothing.

    5. Minimal fix that keeps the information. The catalogue already carries the tool's name, so the sentence need not introduce a second candidate noun — either of:

    ...(shell ? [`On ${OS} it executes via ${shell}.`] : []),
    ...(shell ? [`This tool is named "shell"; on ${OS} it executes via ${shell}.`] : []),

    The alternative — registering bash as a real catalogue alias of shell — fixes the recovery path rather than the emission, which is why I still prefer the wording change. Happy to send a one-line PR for either wording; it changes no behaviour, only the string. Also happy to test a build from a branch in this harness if that helps.

  4. Doc4581 commented on Oct 27, 2026

    @Doc4581

    Independent reproduction on Linux 2.1.4, with no Windows bash.exe and no shell override. Same symptom, but a different source of the prior, so it is not only the description text.

    Environment: opencode 2.1.4, Ubuntu 24.04 x86_64, zsh, tmux. Plugins: superpowers, @tarquinen/[email protected]. No shell setting in opencode.jsonc.

    Evidence from ~/.local/share/opencode/opencode.db on this machine:

    • 23,409 completed bash calls from the V1 era vs 2,974 shell under V2. The models here ran under V1 for months, so bash is a strong prior even when the tool description never says "bash".

    • Since upgrading to V2 (2026-10-26): 789 successful shell calls and 3 rejected bash calls.

    • All three were in one 4.5-hour session (agent architect, model deepseek-v4-pro), and each failing assistant message also carried a valid parallel call (read), e.g.

      {"name": "bash", "input": {"command": "cd …/ui && cat vitest.config.*"}}
      {"name": "read", "input": {"path": "…"}}

      The same session had successful shell calls before and after, so this is intermittent model drift, not a permission rule or a restricted toolset.

    The alias (or the execute.before redirect — thanks for that) is needed for V1-history models too, not just the Windows description case. A dispatch-level alias map would also cover the other renamed V1 names if a model emits them (task → subagent).

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