Skip to content

opencode free tier: fingerprint pass renames the caller's shell to shell_client and leaks a placeholder note, because the captured definition is Linux-only #4971

Open

Description

@mostafa-binesh

Env: 9Router v0.5.104 (decolua/9router@87a74e1), OpenCode client on Windows (win32, pwsh), opencode free tier (/inference/v1). Also reproducible by POSTing to /v1/chat/completions directly — no client involved.

Problem: open-sse/utils/opencodeFingerprint.js treats a caller tool as "conflicting" whenever its description text differs from the frozen fingerprint definition, then works around it by renaming the caller's tool and appending a placeholder:

  • shell → shell_client, alias map recorded, restored on the response side
  • a placeholder shell appended
  • Do not call the \shell` tool; it is a placeholder that does nothing. Use the other tools you have been given instead.` injected into the system prompt

Two consequences:

1. The most common case is misclassified. OpenCode composes the shell description from process.platform:

// OpenCode, packages/core/src/tool/plugin/shell.ts:28-46
const OS = process.platform === "darwin" ? "macOS"
  : process.platform === "win32" ? "Windows"
  : process.platform === "linux" ? "Linux" : process.platform
const description = (shell) => [
  "Execute a shell command and return its output.",
  ...(shell ? [`Commands run on ${OS} using ${shell}.`] : []),
  "Quote file paths containing spaces or special characters.", ...
].join(" ")

The captured fingerprint says Commands run on Linux using bash. (captured on Linux). The same tool on Windows says Commands run on Windows using pwsh. — a 2-character difference inside a 533-character string, but isFingerprintDefinition() compares with === / JSON.stringify, so every request from a non-Linux OpenCode client takes the alias path. read is unaffected: its description is a plain literal on every platform, so it still matches byte-for-byte and is passed straight through.

2. The alias name is derived from the caller's tool list, so it collides. concealFingerprintToolNames() builds taken from every caller tool name, then bumps \${key}_client`→_client2→_client3. A client that already declares shell_client/shell_client2(a plugin, a harness shim) pushes the alias toshell_client3`. The model then sees four shell-ish names, and a call to one of them can reach the client as a tool it does not have.

Measured against this gateway — a stock OpenCode-shaped body vs. one that also declares the two plugin names:

caller tools names the model actually receives
read (exact), shell (Windows/pwsh) read, shell_client, shell
read, shell, shell_client, shell_client2 read, shell_client3, shell_client, shell_client2, shell

Both get the placeholder sentence; it is singular (the \shell` tool) because only shell` was appended.

Repro:

  1. POST { model: "<any opencode free model>", messages: [{role:"user",content:"List every tool name you were given, in order."}], tools: [...] } to /v1/chat/completions, with the tool arrays from the table above.
  2. The reply lists shell_client / shell_client3 and quotes the placeholder sentence verbatim.

Why it matters: the gate needs OpenCode's exact read/shell definitions on the wire — it does not need the caller's tool to be renamed. When the caller's tool already takes the same arguments (the real OpenCode shell has command/workdir/timeout/background in the same key order as the fingerprint), the definition can simply be adopted in place. That removes the alias, the alias map, the response-side restore, the collision-only alias numbering, and the placeholder sentence — for exactly the clients that never needed any of it.

Fix: #4972 — normalize the fingerprint definition in place when the caller's parameter names can carry it, and keep the alias + restore path only for genuinely different schemas (already covered by the existing tests).

Related: #4842 (v2 /inference/v1 fingerprint), #4132 (zen mirror).

Activity

  1. JustSEMI commented on Oct 16, 2026

    @JustSEMI

    same issue lol, using opencode CLI

  2. victor-petropoulos commented on Oct 17, 2026

    @victor-petropoulos

    Confirming this on macOS (darwin, OpenChamber-managed OpenCode server v2.0.29), no Windows involved.

    OpenCode composes the description as Commands run on macOS using ${shell}., so the mismatch fires for every request from this client too — same isFingerprintDefinition() === path, same concealFingerprintToolNames() rename. What surfaced it as a tooling outage rather than a prompt artifact: subagents read their own catalogs and found a default.shell placeholder (no-op, as your note describes) alongside a functional default.shell_client, and reported No tool named "shell_client" is currently available when the catalog was rebuilt without the gateway-injected entry.

    Three independent subagent catalog reads, verbatim, all consistent with your alias path:

    1. default.shell — described in-catalog as a placeholder that does nothing.
    2. default.shell_client — functional; 10/10 controlled echo probe calls succeeded when present.
    3. Absent entirely after an app restart — full 89-tool catalog scan returned zero shell tools, no alias, no placeholder.

    Two extra data points worth adding:

    • The placeholder is reachable and looks like the real tool. Agents that grab default.shell get silence, which reads exactly like a dead shell. That is likely why some of these reports looked like infrastructure flakiness rather than a rewrite.
    • Downstream cost. Because the gateway renames to shell_client and the response-side restore is per-request, any harness that persists tool names across requests (or across the gateway's own stateless boundary) sees names that do not match what it registered. Filed the host-side view as Subagent shell tool (shell_client) intermittently absent from catalog; placeholder default.shell twin confuses openchamber/openchamber#5050; triage there correctly redirected to OpenCode, but the registrar is this pass.

    Nothing here contradicts the diagnosis or #4972 — it just adds a second non-Linux platform and the operational symptom (agents reporting a missing tool rather than a renamed one). Happy to provide session transcripts or re-run instrumented probes against the fix.

  3. BIDZZ-RAWR commented on Oct 17, 2026

    @BIDZZ-RAWR
    Contributor

    Adding a Linux + zsh data point with the same operational symptoms (OpenCode v2.0.31 beta, Arch Linux x64, $SHELL=/usr/bin/zsh, self-hosted relay using @ai-sdk/openai-compatible with 9Router in the serving chain).

    • The fingerprint mismatch fires on Linux + zsh too: OpenCode composes Commands run on Linux using zsh. while the frozen definition says bash, so the === / isFingerprintDefinition() path triggers on every request.
    • Failure is per-request flaky, consistent with a per-request response-side restore: identical shell_client({command, workdir}) calls sometimes succeed, most fail with No tool named "shell_client" is currently available. Please use a tool from the available tool list.
    • Volume from the local session DB: 57 “No tool named” errors on 2026-10-16 and 232 on 2026-10-17 across sessions; one explore subagent retry-looped ~190 times on the same command class before being abandoned.
    • One long-lived session degraded further over time: after repeated failures it also returned No tool named "execute" for Code Mode, leaving only file tools (read/grep/glob) usable. Fresh sessions created afterwards were healthy again.
    • The injected “Do not call the shell tool; it is a placeholder that does nothing” system note reached every agent (main and subagents) and actively discouraged the only working path (shell), while models kept calling the renamed shell_client. Agents self-correct to shell only after being told explicitly.

    Local workarounds that work while #4972 ships:

    1. A small local plugin registering shell_client/bash as real tools, invoked via Code Mode (execute → tools.bash({command, workdir})) — reliable in healthy sessions.
    2. Instructing agents to call shell directly (as suggested in the OpenCode-side issue) — models comply after one explicit note.

    Can provide redacted transcripts/logs and re-test against the fix when it lands.

  4. BlackCatOfficialytb commented on Oct 19, 2026

    @BlackCatOfficialytb
    Image
  5. mostafa-binesh commented on Oct 19, 2026

    @mostafa-binesh
    ContributorAuthor

    10. Image

    You have to create a new session after installing the new version

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions