@khalilgharbaoui/opencode-claude-code-plugin 0.36.0 → 0.36.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1727 +1,115 @@
1
- # @khalilgharbaoui/opencode-claude-code-plugin
2
-
3
- [![npm](https://img.shields.io/npm/v/@khalilgharbaoui/opencode-claude-code-plugin.svg)](https://www.npmjs.com/package/@khalilgharbaoui/opencode-claude-code-plugin)
4
-
5
- Use Claude models inside [opencode](https://opencode.ai) by driving the official **Claude Code CLI** (`claude`) as a subprocess. opencode therefore inherits whatever authentication that CLI already holds: a Claude subscription login, an API key, Bedrock, or Vertex. This plugin never reads, stores, or replays an OAuth token of its own.
6
-
7
- - **Your CLI's auth, untouched.** Because `claude` does the authenticating, there is no subscription token here to lift and replay against the Anthropic API. That replay is what proxy-style opencode plugins do, it is a practice Anthropic has disallowed for third-party tools in 2026, and it is structurally not something this plugin can do.
8
- - **opencode stays in charge of your machine.** Bash, Edit, Write, WebFetch and subagent dispatch are executed by opencode, behind its permission prompts and audit log, rather than by Claude Code. See [Selective tool proxy](#selective-tool-proxy).
9
- - **Headless by default, on your plan's ordinary usage limits.** `claude --print` usage on a subscription plan draws from the same usage limits as interactive Claude Code; the separate Agent SDK credit Anthropic announced for June 2026 was paused before it took effect. API-key authentication bills pay-as-you-go instead. See [Billing](#billing).
10
-
11
- > Maintained fork of [`unixfox/opencode-claude-code-plugin`](https://github.com/unixfox/opencode-claude-code-plugin). Published as `@khalilgharbaoui/opencode-claude-code-plugin` on npm.
12
-
13
- ---
14
-
15
- ## How this compares
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="site/public/banner-dark.svg">
3
+ <source media="(prefers-color-scheme: light)" srcset="site/public/banner-light.svg">
4
+ <img alt="opencode-claude-code-plugin: run Claude in opencode through the official claude CLI" src="site/public/banner-dark.svg" width="1200">
5
+ </picture>
16
6
 
17
- Three ways to reach Claude from opencode. They differ in who authenticates, who gets billed, whether Anthropic sanctions it, and how much of your machine opencode still governs.
7
+ # @khalilgharbaoui/opencode-claude-code-plugin
18
8
 
19
- | | opencode's native `anthropic` provider | This plugin | Proxy / token-reuse plugins |
20
- |---|---|---|---|
21
- | **Authentication** | An Anthropic Platform API key, held in opencode's own auth store. | Whatever the official `claude` CLI already holds: a subscription login, an API key, Bedrock, or Vertex. The plugin never reads, stores, or replays a token of its own, and there is no subscription token here to lift. | The Claude OAuth session, used outside the official client. Meridian runs a local proxy that maps Anthropic-style HTTP onto the Claude Agent SDK and your Claude session; `opencode-claude-auth` reads the OAuth tokens out of the macOS Keychain or `~/.claude/.credentials.json` and refreshes them against Anthropic's OAuth endpoint itself. |
22
- | **What is billed, and to whom** | Pay as you go on the Platform account that owns the key. | Whatever the CLI's own authentication bills. Headless `--print` is the Agent SDK path; an API key found anywhere the CLI looks switches the same turn onto Console pay-as-you-go instead. `apiKeySource` on the CLI's `system` init event is the field that says which, and the plugin warns once per process when a key is in effect. On a subscription, headless and interactive turns both draw from the plan's ordinary usage limits. See [which login bills what](#which-login-bills-what). | The subscription the reused session belongs to. Meridian's own FAQ: "Usage limits follow your Max subscription, not Anthropic API billing tiers." |
23
- | **Terms-of-service status** | The ordinary API route. Nothing unusual about it. | Sanctioned: the official client does the authenticating, and driving `claude` is what `claude` is for. | Disallowed. Anthropic disallowed reusing subscription authentication for third-party Claude use in February 2026, and each project says so in its own words: Meridian's wrapper "makes no claims regarding compliance with Anthropic's Terms of Service"; `opencode-claude-auth` calls itself "a community workaround" and notes that the terms say subscription tokens "should only be used with official Anthropic clients"; `opencode-claude-plan` quotes Consumer Terms 3.7 and asks you to accept that your account "could be suspended or terminated". |
24
- | **Model list and fast mode** | Whatever opencode's own provider registers. | 18 ids auto-registered, Haiku 4.5 through Opus 5.5 plus Fable and Mythos, each carrying a `(N×)` list-price suffix, and any other id `claude --model` accepts passes straight through. Three `-fast` Opus ids are this plugin's own markers and opt a headless session into fast mode through `--settings` (CLI 2.1.220+). See [Models](#models). | `opencode-claude-auth`'s README lists 14 model ids. Meridian's lists none, because model metadata comes from opencode's own `anthropic` provider. Neither README mentions fast mode. |
25
- | **Which tools run where, under whose permissions** | All of them are opencode's, behind opencode's permission prompts and audit log. | Your choice, per tool. `Bash`, `Edit`, `Write`, `WebFetch` and `Task` are proxied by default: Claude calls an in-process MCP tool and **opencode** executes it, under its own permissions and audit log. Anything neither proxied nor named in `extraDisallowedTools` runs inside Claude Code under `--dangerously-skip-permissions`. See [Selective tool proxy](#selective-tool-proxy) and [Read-only mode](#read-only-mode). | All of them are opencode's, because the model call is an ordinary provider call. This is the one row where the third column matches the native provider and this plugin has to work for the same result. |
26
- | **Reasoning and effort** | opencode's own reasoning controls. | Five picker variants per model, `low` through `max`, handed to the CLI as `CLAUDE_CODE_EFFORT_LEVEL` at spawn. Effort is fixed for the life of a `claude` process, so it is part of the session key, and an agent's own `reasoningEffort` beats the effort a call arrived with. Thinking is Anthropic's summarized digest, not raw chain-of-thought. See [Extended thinking](#extended-thinking). | Meridian's SDK-features file exposes a `thinking` key. Neither README documents per-model effort variants. |
27
- | **Context window** | Whatever the model exposes. | The registered limits: 200k context / 64k output on the 4.5 generation, 1M / 128k on 4.6 and later, all at standard pricing with no above-200K tier. Claude Code may also compact or clear its own context mid-conversation, which the plugin can announce but not prevent. | Not stated in either README. |
28
- | **Subagents** | opencode's own task tool and child sessions. | `Task` is proxied by default, so dispatch is an opencode child session under the caller's `permission.task` rule, and `task_batch` runs two or more concurrently because the CLI otherwise serialises MCP calls. Drop `Task` from `proxyTools` and Claude orchestrates internally with no opencode child-session visibility. See [OpenCode-native subagents](#opencode-native-subagents). | opencode's own, unchanged. |
29
- | **What you lose versus the native provider** | Baseline. | A `claude` child process per conversation (an idle `--print` holds around 250 MB) under an LRU cap, so many open chats cost memory. Claude Code can compact or clear its own context behind opencode's back. Session titles are a local keyword stub, not a model-written title. Two watchdogs exist only because a child that is alive but wedged emits no event to listen for. `/compact` runs as its own short-lived spawn, on Haiku by default. On opencode 2 there is no todo panel, because 2.x has no `todowrite` tool, and `/btw` is answered after the running turn rather than inside it. opencode hooks that overlap the plugin's hand-rolled features (`tool.definition`, the two compaction hooks, `chat.headers`, `permission.ask`) are deliberately not adopted, and opencode's own reasoning features are bypassed by design, because the whole point is to route through the CLI. Windows spawns through `cmd.exe` unquoted and is [not hardened](#scratch-files-on-disk). | Everything in the row to the left is avoided, because opencode's runtime is doing the work. What replaces it is the account risk in the terms row, plus one more moving part between opencode and Anthropic: a local HTTP proxy, or a reader of your credential store. |
9
+ [![npm version](https://img.shields.io/npm/v/%40khalilgharbaoui%2Fopencode-claude-code-plugin?style=flat-square&label=npm&labelColor=15181E&color=FFC46B)](https://www.npmjs.com/package/@khalilgharbaoui/opencode-claude-code-plugin)
10
+ [![npm downloads per month](https://img.shields.io/npm/dm/%40khalilgharbaoui%2Fopencode-claude-code-plugin?style=flat-square&label=downloads%2Fmonth&labelColor=15181E&color=FFC46B)](https://www.npmjs.com/package/@khalilgharbaoui/opencode-claude-code-plugin)
11
+ [![npm downloads total](https://img.shields.io/npm/dt/%40khalilgharbaoui%2Fopencode-claude-code-plugin?style=flat-square&label=downloads&labelColor=15181E&color=FFC46B)](https://www.npmjs.com/package/@khalilgharbaoui/opencode-claude-code-plugin)
12
+ [![GitHub stars](https://img.shields.io/github/stars/khalilgharbaoui/opencode-claude-code-plugin?style=flat-square&label=stars&labelColor=15181E&color=FFC46B)](https://github.com/khalilgharbaoui/opencode-claude-code-plugin/stargazers)
13
+ [![GitHub forks](https://img.shields.io/github/forks/khalilgharbaoui/opencode-claude-code-plugin?style=flat-square&label=forks&labelColor=15181E&color=FFC46B)](https://github.com/khalilgharbaoui/opencode-claude-code-plugin/forks)
14
+ [![Contributors](https://img.shields.io/github/contributors/khalilgharbaoui/opencode-claude-code-plugin?style=flat-square&label=contributors&labelColor=15181E&color=FFC46B)](https://github.com/khalilgharbaoui/opencode-claude-code-plugin/graphs/contributors)
15
+ [![Tests](https://img.shields.io/endpoint?url=https%3A%2F%2Fkhalilgharbaoui.github.io%2Fopencode-claude-code-plugin%2Fbadges%2Ftests.json&style=flat-square)](https://khalilgharbaoui.github.io/opencode-claude-code-plugin/internals/testing/)
16
+ [![Publish](https://img.shields.io/github/actions/workflow/status/khalilgharbaoui/opencode-claude-code-plugin/publish.yml?style=flat-square&label=publish&labelColor=15181E)](https://github.com/khalilgharbaoui/opencode-claude-code-plugin/actions/workflows/publish.yml)
17
+ [![License](https://img.shields.io/github/license/khalilgharbaoui/opencode-claude-code-plugin?style=flat-square&label=license&labelColor=15181E&color=FFC46B)](./LICENSE)
18
+ [![Buy me a coffee](https://img.shields.io/badge/support-buy%20me%20a%20coffee-FFC46B?style=flat-square&labelColor=15181E&logo=buymeacoffee&logoColor=FFC46B)](https://www.buymeacoffee.com/khalilgharbaoui)
30
19
 
31
- Where the third column names a project, the claim is that project's own README:
20
+ **Claude Code is the provider.** This opencode plugin runs Anthropic's Claude models through the official **Claude Code CLI** (`claude`) as a subprocess instead of calling the HTTP API. opencode inherits whatever that CLI is logged in as (a Claude subscription, an API key, Bedrock or Vertex), gets Claude's own tools, MCP servers and skills, and keeps running the tools that touch your machine itself, behind its own permission prompts. One package serves opencode 1.x and 2.x.
32
21
 
33
- - [ianjwhite99/opencode-with-claude](https://github.com/ianjwhite99/opencode-with-claude) starts [Meridian](https://github.com/rynfar/meridian) inside opencode's own lifecycle and points opencode's `anthropic` provider at it. Its disclaimer calls it an "unofficial wrapper", says the authors "make no claims regarding compliance with Anthropic's Terms of Service", and notes that no API keys are intercepted: the proxy uses the Agent SDK over your own OAuth session.
34
- - [griffinmartin/opencode-claude-auth](https://github.com/griffinmartin/opencode-claude-auth) registers its own auth provider, reads Claude Code's OAuth credentials from the Keychain or `~/.claude/.credentials.json`, caches and refreshes them, and syncs them into opencode's `auth.json`. Its disclaimer is quoted in the table.
35
- - [jcubic/opencode-claude-plan](https://github.com/jcubic/opencode-claude-plan) ships no plugin at all: it is a documented plan an agent can build one from, written after opencode removed its bundled Anthropic OAuth plugin on a legal request. Its legal note is the bluntest of the three.
36
- - [unixfox/opencode-claude-code-plugin](https://github.com/unixfox/opencode-claude-code-plugin) belongs in the middle column rather than the third, and deserves the credit: it is this plugin's archived ancestor and it already drove the `claude` CLI as a subprocess, so it inherited the CLI's authentication the same sanctioned way. What it does not have is the opencode-side mediation. Its README states that the CLI executes every tool, that permissions go through Claude Code's own allow/deny lists with "no opencode permission UI integration", that MCP servers are Claude's rather than opencode's, and that its session key is `(cwd, model)`, so two opencode instances in one directory on one model share a process and interfere. It is archived and links here.
22
+ - **Your CLI's login, untouched.** The plugin never reads, stores or replays a token. Lifting the OAuth session out of the official client is what proxy-style plugins do, and Anthropic disallowed that for third-party Claude use in February 2026. This plugin structurally cannot do it.
23
+ - **opencode runs the tools.** `Bash`, `Edit`, `Write`, `WebFetch` and `Task` are proxied by default: Claude calls an in-process MCP tool and opencode executes it, under its own permissions and audit log.
24
+ - **Billed as your CLI bills.** Headless `claude --print` on a subscription draws from the plan's ordinary usage limits; an API key bills pay as you go. The CLI's `apiKeySource` says which, every session, and the plugin warns when a stray `ANTHROPIC_API_KEY` would move the bill.
37
25
 
38
- Policy sources: Anthropic's [Agent SDK on a Claude plan](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan) page, which is authoritative and does change (read the dated note under [Billing](#billing) before quoting it), and the February 2026 report that [Anthropic banned subscription authentication for third-party Claude use](https://alternativeto.net/news/2026/2/anthropic-officially-bans-using-subscription-authentication-for-third-party-claude-use).
26
+ **Docs:** <https://khalilgharbaoui.github.io/opencode-claude-code-plugin/> (the same pages are the markdown under [`docs/`](./docs/), which GitHub renders on its own).
39
27
 
40
- ---
28
+ > Maintained fork of [`unixfox/opencode-claude-code-plugin`](https://github.com/unixfox/opencode-claude-code-plugin), which is archived and links here.
41
29
 
42
30
  ## Quickstart
43
31
 
44
- ### 1. Install and log in the Claude Code CLI
45
-
46
- The plugin drives an existing [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code); it does not bundle one. Check that `claude` is on your `$PATH` and authenticated:
47
-
48
- ```bash
49
- claude --version # e.g. 2.1.263 (Claude Code)
50
- claude auth status # which account you are signed in as
51
- claude auth login # run this if you are not signed in yet
52
- ```
53
-
54
- `login`, `status` and `logout` are the `claude auth` subcommands as of 2.1.263. Run `claude auth --help` if your install differs.
55
-
56
- ### 2. Add the plugin to your opencode config
57
-
58
- opencode reads a global config at `~/.config/opencode/opencode.json` (or `$XDG_CONFIG_HOME/opencode/` when that is set). A project-level `opencode.json` in your repo overrides the global one, and `OPENCODE_CONFIG=/path/to/config.json` points opencode at one specific file instead. Put the plugin in the global config so every project gets it:
59
-
60
- ```json
61
- {
62
- "plugin": ["@khalilgharbaoui/opencode-claude-code-plugin"]
63
- }
64
- ```
65
-
66
- That package spec is the whole install. Do **not** `npm install` the package yourself: opencode resolves and caches plugin packages on its own. You do not need a `provider` block either, unless you want to change one of the [options](#options-reference).
67
-
68
- ### 3. Restart opencode and verify
69
-
70
- Quit opencode fully and relaunch it: plugins are loaded once, at process start, so a reload is not enough.
71
-
72
- In the model picker you should now see a provider called **Claude Code (Default)** holding entries such as `Claude Haiku 4.5 (1×)`, `Claude Sonnet 5.5 (2×)` and `Claude Opus 5 (5×)`. The `(N×)` suffix is each model's list price relative to Haiku; see [Models](#models). Pick one and send a message.
73
-
74
- If the provider does not appear, if the models are there but a message fails, or if a version you just upgraded to is missing, go to [Troubleshooting](#troubleshooting). It is keyed on the first thing you see and names one check per symptom.
75
-
76
- ### Local development
77
-
78
- ```bash
79
- git clone https://github.com/khalilgharbaoui/opencode-claude-code-plugin
80
- cd opencode-claude-code-plugin
81
- bun install
82
- bun run build
83
- ```
84
-
85
- In your `opencode.json`, point at the local build with a `file://` URL:
86
-
87
- ```json
88
- {
89
- "plugin": ["file:///absolute/path/to/opencode-claude-code-plugin"]
90
- }
91
- ```
92
-
93
- CI installs and builds on **Node 24** (`.github/workflows/publish.yml`), which is the only version this package is built against. `package.json` declares no `engines` range, so older Node versions are untested rather than deliberately unsupported. opencode itself may run under Bun; the [interactive transport](#interactive-transport-experimental) requires that.
94
-
95
- ### opencode 2
96
-
97
- The same package runs on opencode 1.x and 2.x, and nothing changes for 1.x. The same config works too: opencode 2's native key is `plugins`, but it still reads the 1.x `plugin` key shown above, so an existing install needs no edit. A config used only by opencode 2 can spell it natively:
98
-
99
- ```json
100
- {
101
- "plugins": ["@khalilgharbaoui/opencode-claude-code-plugin"]
102
- }
103
- ```
104
-
105
- Your existing `provider.claude-code.options` block keeps working, because opencode 2 still reads 1.x config files. opencode 2's own spelling is `providers.claude-code.settings` (note the plural `providers`). All four places the plugin reads its settings from, lowest precedence first: `provider.claude-code.options`, `provider.claude-code.settings`, `providers.claude-code.settings`, and the plugin entry's own `options`, which wins over all of them. Plugin-level settings such as `accounts` usually go in that last one: `{"package": "@khalilgharbaoui/opencode-claude-code-plugin", "options": {"accounts": ["work"]}}`.
106
-
107
- For a local checkout, point opencode 2 at the **`dist` directory**, not the repository root. It loads `<dir>/server` or `<dir>/index` from a directory and never reads `package.json#main`:
108
-
109
- ```json
110
- {
111
- "plugins": ["/absolute/path/to/opencode-claude-code-plugin/dist"]
112
- }
113
- ```
114
-
115
- Verified live on opencode **2.0.11** with Claude Code 2.1.280: chat turns, proxied tools running through opencode 2's own `shell`, `edit`, `write`, `webfetch` and `subagent` tools and their permission rules, Claude's own tools rendered in the transcript, subagent dispatch with the agent list, reasoning variants, compaction, account providers, `/claude-code-doctor`, `/btw`, and the bundled configuration skill. Differences from 1.x:
116
-
117
- - **`/btw` is answered after the running turn**, not inside it. opencode 2's plugin API has no session-status route, which is what 1.x uses to write the answer into a turn that is still running. The aside is queued, so it can never swallow the turn's own continuation.
118
- - **No todo panel.** opencode 2 has no `todowrite` tool, so Claude's task list is not mirrored into one.
119
- - **Account failover and the plan-mode form** use opencode 2's `question` tool, which takes the same input as 1.x. Both are covered by offline tests only on 2.x, since neither can be triggered on demand.
120
-
121
- ---
122
-
123
- ## Models
124
-
125
- The plugin auto-registers the following, and they appear in the model picker with no extra config: Haiku 4.5, Sonnet 4.5/4.6/5/5.5, Opus 4.5/4.6/4.7/4.8/5/5.5 (plus three fast-mode Opus entries), Fable 5/5.1 and Mythos 5/5.1, each except Haiku carrying `low` / `medium` / `high` / `xhigh` / `max` reasoning variants.
126
-
127
- | ID | Display name | Context | Output | Reasoning variants | Price × |
128
- |---|---|---|---|---|---|
129
- | `claude-haiku-4-5` | Claude Haiku 4.5 | 200k | 64,000 | – | 1× |
130
- | `claude-sonnet-4-5` | Claude Sonnet 4.5 | 200k | 64,000 | low/medium/high/xhigh/max | 3× |
131
- | `claude-sonnet-4-6` | Claude Sonnet 4.6 | 1M | 128,000 | low/medium/high/xhigh/max | 3× |
132
- | `claude-sonnet-5` | Claude Sonnet 5 | 1M | 128,000 | low/medium/high/xhigh/max | 2× |
133
- | `claude-sonnet-5-5` | Claude Sonnet 5.5 | 1M | 128,000 | low/medium/high/xhigh/max | 2× |
134
- | `claude-opus-4-5` | Claude Opus 4.5 | 200k | 64,000 | low/medium/high/xhigh/max | 5× |
135
- | `claude-opus-4-6` | Claude Opus 4.6 | 1M | 128,000 | low/medium/high/xhigh/max | 5× |
136
- | `claude-opus-4-7` | Claude Opus 4.7 | 1M | 128,000 | low/medium/high/xhigh/max | 5× |
137
- | `claude-opus-4-8` | Claude Opus 4.8 | 1M | 128,000 | low/medium/high/xhigh/max | 5× |
138
- | `claude-opus-4-8-fast` | Claude Opus 4.8 Fast | 1M | 128,000 | low/medium/high/xhigh/max | 10× |
139
- | `claude-opus-5` | Claude Opus 5 | 1M | 128,000 | low/medium/high/xhigh/max | 5× |
140
- | `claude-opus-5-fast` | Claude Opus 5 Fast | 1M | 128,000 | low/medium/high/xhigh/max | 10× |
141
- | `claude-opus-5-5` | Claude Opus 5.5 | 1M | 128,000 | low/medium/high/xhigh/max | 4× |
142
- | `claude-opus-5-5-fast` | Claude Opus 5.5 Fast | 1M | 128,000 | low/medium/high/xhigh/max | 8× |
143
- | `claude-fable-5` | Claude Fable 5 | 1M | 128,000 | low/medium/high/xhigh/max | 10× |
144
- | `claude-fable-5-1` | Claude Fable 5.1 | 1M | 128,000 | low/medium/high/xhigh/max | 10× |
145
- | `claude-mythos-5` | Claude Mythos 5 | 1M | 128,000 | low/medium/high/xhigh/max | 10× |
146
- | `claude-mythos-5-1` | Claude Mythos 5.1 | 1M | 128,000 | low/medium/high/xhigh/max | 10× |
147
-
148
- `claude-mythos-5` and `claude-mythos-5-1` are Mythos-class counterparts to the corresponding Fable models, but without safety classifiers, and are **limited availability via [Project Glasswing](https://anthropic.com/glasswing)**. They're registered unconditionally; if your Claude account lacks access, `claude --model` just errors. Use the corresponding generally available `claude-fable-5` or `claude-fable-5-1` otherwise.
149
-
150
- Capabilities for every model: text + image input, text output, tool use, attachments. No temperature control, no PDF/audio/video, no interleaved streaming.
151
-
152
- **Price ×** is each model's per-token list price relative to Haiku, the cheapest model. It's derived exactly from Anthropic's published pricing (input and output ratios both come out the same: Haiku $1/$5 = 1×, Sonnet 5 and 5.5 $2/$10 = 2×, Sonnet 4.5/4.6 $3/$15 = 3×, Opus 5.5 $4/$20 = 4×, Opus $5/$25 = 5×, Opus 5.5 fast mode $8/$40 = 8×, Fable/Mythos 5 and 5.1 / Opus 5 and 4.8 fast mode $10/$50 = 10×). So **Fable/Mythos 5 and 5.1, and fast-mode Opus 5 and 4.8, all cost 2× standard Opus 5**, and fast mode is 2× the standard price on every Opus that offers it. The same multiplier is shown as a `(N×)` suffix on the display name in opencode's model picker, since opencode has no dedicated multiplier field. On a flat Max/Pro subscription it doubles as a rough guide to how fast each model drains your usage limit.
153
-
154
- Fable 5.1 and Mythos 5.1 keep the same $10/M input and $50/M output rates as 5.0, but cache reads cost $0.25/M instead of $1/M. Their cache-write rate remains $12.50/M.
155
-
156
- Sonnet 5 and Sonnet 5.5 are $2/M input and $10/M output, with cache writes at $2.50/M and cache reads at $0.20/M. Sonnet 5's price was announced as introductory until 2026-08-31, but Anthropic cancelled the increase to $3/$15, so $2/$10 is its standard price. Sonnet 5.5 runs on any recent Claude Code, but **2.1.284 is the first release that knows it**. An older CLI still serves it, on fallback limits (a 200k context window instead of 1M, and an estimated cost). The plugin warns once when the CLI reports that, and `claude update` fixes it.
157
-
158
- Opus 5.5 is priced below the Opus line at $4/M input and $20/M output, with cache writes at $5/M and cache reads at $0.20/M (0.05× input rather than the usual 0.1×). It needs **Claude Code 2.1.280 or newer**: the API rejects it from an older CLI with a 400 naming that floor, which shows up as a failed turn.
159
-
160
- The model ID is passed straight through to `claude --model`, so anything Claude Code accepts works. The three `-fast` IDs are the one exception, described below.
161
-
162
- ### Fast mode
163
-
164
- `claude-opus-5-5-fast`, `claude-opus-5-fast` and `claude-opus-4-8-fast` run the same models at up to 2.5× the output tokens per second, at 2× the price ($8/M input, $40/M output for Opus 5.5, the 8× column; $10/M input, $50/M output for Opus 5 and 4.8, the 10× column). Pick them in the model selector like any other model.
165
-
166
- The `-fast` suffix is this plugin's own marker, not a model name Anthropic serves. The plugin strips it and spawns `claude --model claude-opus-5 --settings '{"fastMode":true}'`, because that settings layer is the only way to opt a headless (`--print`) session into fast mode: there is no `--fast` flag, and the old `claude-opus-4-6-fast` style model names are retired. Requires Claude Code 2.1.220+; below that the plugin skips the opt-in and you get standard speed.
167
-
168
- Fast mode is not available everywhere, and it **fails soft**: an ineligible account drops back to standard speed with no error. Known blockers:
169
-
170
- - **Usage credits are off.** The most common one. Run `/usage-credits` in an interactive `claude` session to enable them.
171
- - **Not first-party.** Fast mode is Anthropic-API-only; Bedrock, Vertex, and Foundry are excluded.
172
- - **Free tier**, or an organization that has turned fast mode off.
173
- - **Cooldown.** Fast mode has its own rate limit; after a hit, Claude Code falls back to standard until it clears.
174
- - `CLAUDE_CODE_DISABLE_FAST_MODE=1` in the environment turns it off outright.
175
-
176
- Because a downgrade is otherwise invisible, and because the picker shows these IDs at 10× regardless, the plugin logs a **warning** (once per reason) when a fast turn actually ran at standard speed, naming the reason. If you see it, switch to the non-fast ID so the picker's price matches your bill.
177
-
178
- ### Picking a variant
179
-
180
- Variants set the underlying reasoning effort. They're regular opencode model variants — pick them in the model selector. If you'd previously declared variants in your project's `opencode.json`, they're merged on top of the defaults so nothing gets lost.
181
-
182
- ---
183
-
184
- ## Billing
185
-
186
- By default this plugin drives Claude Code headlessly (the Agent SDK path, `claude --print`). On a Claude subscription plan that usage draws from your plan's ordinary usage limits, the same pool as interactive Claude Code. Authenticating the CLI with an API key instead bills as ordinary pay-as-you-go API usage.
187
-
188
- Anthropic's own page is the authoritative source and it changes: <https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan>
189
-
190
- > **The history, so older text does not mislead you.** Anthropic announced a separate monthly Agent SDK credit for headless and third-party usage, to start on June 15, 2026, and paused it the same day. The page, fetched on 2026-09-27, opens with that June 15 update: nothing has changed, Agent SDK usage, `claude -p` and third-party apps still draw from subscription usage limits, and the credit is not available. Earlier versions of this README, and some third-party write-ups, describe the credit as if it were in effect. Re-read the page rather than this section when it matters. The mechanism the plugin exposes is the same either way: `apiKeySource` is what tells you whether a turn is on the subscription or on pay-as-you-go.
191
-
192
- One thing in this plugin interacts with the above: [`ignoreAnthropicApiKey`](#options-reference) stops a stray `ANTHROPIC_API_KEY` in your environment from silently redirecting the CLI onto pay-as-you-go API billing. The experimental [interactive transport](#interactive-transport-experimental) drives the real `claude` TUI instead of `--print`; it does not change what a turn draws from.
193
-
194
- ---
195
-
196
- ## Configuration
197
-
198
- The minimum config is just the `plugin` entry above. Everything below is optional override that goes in a `provider.claude-code` block.
199
-
200
- ### Multiple Claude Code accounts
201
-
202
- Declare account names once and the plugin expands them into separate opencode providers:
203
-
204
- ```json
205
- {
206
- "plugin": ["@khalilgharbaoui/opencode-claude-code-plugin"],
207
- "provider": {
208
- "claude-code": {
209
- "options": {
210
- "accounts": ["personal", "work"]
211
- }
212
- }
213
- }
214
- }
215
- ```
216
-
217
- `default` is always implicit, so the config above creates:
218
-
219
- | Provider ID | Display name | Claude config dir |
220
- |---|---|---|
221
- | `claude-code-default` | `Claude Code (Default)` | normal `~/.claude` |
222
- | `claude-code-personal` | `Claude Code (Personal)` | `~/.claude-personal` |
223
- | `claude-code-work` | `Claude Code (Work)` | `~/.claude-work` |
224
-
225
- Non-default accounts use `CLAUDE_CONFIG_DIR` through a generated wrapper script, so auth/session state stays isolated per account. Shared capability files and folders are symlinked from `~/.claude` into each account dir when present:
226
-
227
- ```text
228
- CLAUDE.md
229
- settings.json
230
- skills/
231
- agents/
232
- commands/
233
- plugins/
234
- ```
235
-
236
- Identity/session state is not shared.
237
-
238
- Login each account once:
239
-
240
- ```bash
241
- CLAUDE_CONFIG_DIR="$HOME/.claude-personal" claude auth login
242
- CLAUDE_CONFIG_DIR="$HOME/.claude-work" claude auth login
243
- ```
244
-
245
- The account model IDs are internally suffixed, for example `claude-sonnet-4-6@work`, so long-lived Claude subprocess sessions do not collide across accounts. The generated wrapper strips the suffix before calling `claude --model`.
246
-
247
- #### Account failover
248
-
249
- With more than one account configured, an account running out of usage mid-task no longer just ends the turn. The plugin asks, using opencode's own `question` form:
250
-
251
- ```text
252
- Account limit
253
- The Claude account "work" is out of usage in the five_hour window, which resets at
254
- 2026-09-20T18:00:00.000Z. Continue this task on another configured account?
255
- Leaving this unanswered waits, at no cost.
256
-
257
- personal Run on "personal" until 2026-09-20T18:00:00.000Z. …
258
- default Run on "default" until 2026-09-20T18:00:00.000Z. …
259
- stop End this turn now and leave the account as it is.
260
- ```
261
-
262
- Pick an account and the task continues on it **inside the same opencode turn**, with no new message from you. This is on by default because the pick is the consent: nothing moves until you choose, and leaving the form open costs nothing.
263
-
264
- What a pick does, in full:
265
-
266
- - **It is sticky for the limited account, not for the session.** A usage limit belongs to the account, so one pick governs every session running on `work`, and subagents follow their parent for free. It lasts until the limit's reset time, or until opencode restarts when the CLI did not report one. Child sessions never show the form themselves.
267
- - **The conversation is replayed, not resumed.** Claude transcripts live under each account's own `CLAUDE_CONFIG_DIR`, so `--resume` cannot cross accounts. The plugin starts a fresh Claude session on the target and replays the thread from opencode's history, then tells it to carry on. That costs input tokens on the new account, and anything the CLI held but opencode did not is gone.
268
- - **Per-profile MCP servers do not come along.** A server configured only in the limited account's Claude profile is simply absent on the target.
269
- - **`stop`, dismissing the form, or any answer that is not one of the offered accounts** ends the turn exactly the way the rate-limit error ends it today.
270
- - **An account that cannot serve at all gets the same form.** When Claude Code reports that an account's login expired (`authentication_failed`), or that it is on hold, unverified or has a billing problem, the plugin writes a note naming the account and what to do, and offers the switch if another account is configured. For an expired login the note gives the exact command, for example `CLAUDE_CONFIG_DIR=~/.claude-work claude auth login`. The switch lasts until opencode restarts, so restart after logging in again to move back. With a single account you get the note alone.
271
-
272
- Only two things open the form: a `rate_limit_event` the CLI marked `rejected`, and the two known account-limit error texts (`Third-party apps now draw from your extra usage…`, `You've hit your individual spend limit`). A generic 4xx, a timeout or a bad flag never does, deliberately: a transient failure must not quietly move where your usage is billed.
273
-
274
- Not available on the [interactive transport](#interactive-transport-experimental) (no proxy server, TUI stdin) or on compaction turns. Set `"accountFailover": "off"` to keep the plain error.
275
-
276
- ### Subagents: your account, their model
277
-
278
- opencode's agent config cannot express "inherit the account, choose the model". A subagent that omits `model` inherits the invoking agent's whole model string; one that pins `model` inherits neither half, so pinning Opus also pins whichever account was written into it. This plugin closes that gap, because it is the piece that knows the account is the *provider* while the model is only a `--model` flag.
279
-
280
- Write an agent markdown file. Nothing goes in `opencode.json`.
281
-
282
- ```markdown
283
- ---
284
- description: Designs and builds UI work
285
- mode: subagent
286
- ---
287
- You are a designer...
288
- ```
289
-
290
- `@designer` now runs on **the account of the session that invoked it**, on whatever model you point it at. Which model comes from one of two places.
291
-
292
- Per agent, in the agent's own file:
293
-
294
- ```yaml
295
- forceModel: claude-haiku-4-5
296
- ```
297
-
298
- Or once, for every subagent that pins nothing, in the provider options:
299
-
300
- ```json
301
- { "provider": { "claude-code": { "options": { "defaultSubagentModel": "claude-opus-5" } } } }
302
- ```
303
-
304
- The rules, in order:
305
-
306
- | The agent | Runs on |
307
- | --- | --- |
308
- | `forceModel: <id>` | the caller's account, that model |
309
- | `mode: subagent`, no model, `defaultSubagentModel` set | the caller's account, that model |
310
- | `mode: subagent`, no model, no default set | untouched, inherits the caller's model |
311
- | `model: <provider>/<id>` | exactly that, account and all (untouched) |
312
- | anything opencode ships (`explore`, `general`, `compaction`) | untouched |
313
-
314
- **`defaultSubagentModel` is unset by default and nothing is overridden without it.** That is deliberate: this feature rewrites what the model picker said would run, so an existing setup that upgrades the plugin has to behave exactly as it did before. Built-ins are excluded for the same reason, since forcing Opus onto a cheap exploration agent would be an expensive surprise nobody asked for. An unknown model id is refused and the original kept, rather than spawning the CLI with a `--model` it will reject.
315
-
316
- Two things worth knowing. The overridden model is part of the Claude session key, so a subagent forced to Opus never shares a `claude` process with a Fable parent in the same directory. And opencode still prices the turn against the model *it* routed, so a cost readout attributes the work to the caller's model, not the one that actually ran.
317
-
318
- ### The effort an agent runs at
319
-
320
- The same file can state its own thinking budget:
321
-
322
- ```yaml
323
- reasoningEffort: high
324
- ```
325
-
326
- That beats whatever effort the call arrived with. It has to, because opencode resolves one effort for a session and a subagent inherits it, which is wrong in the expensive direction: a caller who picked `max` for their own turn otherwise hands `max` to every worker it dispatches, and a mechanical lane burns a weekly cap at the costliest setting available. Model and effort together are what a turn costs, so both belong with the agent rather than with whoever happened to dispatch it.
327
-
328
- An agent that declares nothing keeps the inherited effort, so this changes nothing until a file asks for it. An unrecognised level is refused and the inherited one kept, since the CLI rejects a level it does not know. Compaction is exempt: its summary always gets the full budget.
329
-
330
- ### The prompt cache an agent writes
331
-
332
- The third thing a turn costs is the prompt cache it writes, and the same file can state that too:
333
-
334
- ```yaml
335
- cacheTtl: 5m
336
- ```
337
-
338
- Or once, for every subagent that declares nothing, as `defaultSubagentCacheTtl` in the provider options. Values are `5m` and `1h`; anything else warns and leaves the CLI alone. It applies to headless spawns only: `/compact` and the experimental interactive transport keep the CLI's own default.
339
-
340
- Claude Code's automatic default is a 1-hour cache on a subscription, and a 1-hour cache write is billed above a 5-minute one. That trade pays off for a long-lived main session, which re-reads the cache it wrote. It does not pay off for a fan-out of short workers: each one writes an hour-long cache, finishes, and never reads it again, and all of it comes out of the same weekly limit. Declaring `cacheTtl: 5m` on the workers while the main session keeps the default is the point of the knob.
341
-
342
- **It is unset by default**, for the same reason `defaultSubagentModel` is: an upgrade must not quietly change how anybody's turns are cached.
343
-
344
- One piece of Claude Code trivia is worth stating plainly, because it is the opposite of what the names suggest. The CLI has a per-agent `experimental.cacheTtl` and a `CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL`, and **neither of them does anything to this plugin.** Both apply only to subagents the CLI runs itself, through its own `Task` tool, which this plugin disallows by default so that opencode runs the subagent instead. An opencode subagent arrives here as its own `doStream` and its own `claude --print` process, and the CLI counts that as a main conversation. So the knob that reaches every process this plugin spawns is the main-conversation one, `CLAUDE_CODE_PROMPT_CACHE_TTL`, which is what `cacheTtl` sets. Measured on CLI 2.1.280 by reading `usage.cache_creation` back off a real turn: the main variable moved the writes to `ephemeral_5m_input_tokens`; the subagent variable left them at 1 hour.
345
-
346
- Like model and effort, the TTL is part of the Claude session key, so changing it respawns rather than sharing a process. Compaction is exempt.
347
-
348
- Forking an opencode session normally throws that cache away: the fork arrives as a new session with the thread copied in and no Claude session behind it, so the whole conversation is re-rendered as text into the first message and paid for again as cache writes. Setting `forkSessions: true` in the provider options branches the parent's Claude conversation with `claude --resume <parent> --fork-session` instead. Measured on CLI 2.1.280 with haiku 4.5 over a ~13k-token thread, the forked turn wrote 814 cache tokens and read 39,710, against 22,355 written and 17,385 read for the replay: $0.0058 against $0.0467 for that one turn, with the parent's own transcript byte-identical afterwards. It is off by default because a resumed Claude conversation reuses the system prompt recorded on its first request, so a forked session answers under the parent's appended system prompt rather than the current turn's. Anything it cannot match exactly keeps today's replay: another account, an unknown or busy parent, a fork cut mid-conversation or taken mid tool call, a different cwd, model, agent, effort or cache TTL, compaction, and the interactive transport.
349
-
350
- To force an **account** rather than a model, pin the full string. This only applies if you declared [`accounts`](#multiple-claude-code-accounts) in the first place; with the default single-account setup there is nothing to pin. Both halves are needed, because the provider selects the account's config dir and the `@account` marker is what the model was registered under for that provider:
351
-
352
- ```yaml
353
- model: claude-code-work/claude-opus-5@work
354
- ```
355
-
356
- ### Fallback model chain
357
-
358
- `forceModel` and the model picker each name exactly one model, so a model this
359
- account cannot run today is a dead turn. The two ordinary ways to get there are
360
- a **retired id** (Anthropic retires model names on a published schedule, and an
361
- agent file written six months ago outlives them) and a **per-model usage cap**.
362
- An ordered chain degrades instead of failing:
363
-
364
- Per agent, in the agent's own file, either YAML spelling:
365
-
366
- ```yaml
367
- forceModel: claude-opus-5
368
- fallbackModels: [claude-sonnet-5, claude-haiku-4-5]
369
- ```
370
-
371
- Or once, as the default for every agent that declares none:
372
-
373
- ```json
374
- { "provider": { "claude-code": { "options": { "fallbackModels": ["claude-sonnet-5"] } } } }
375
- ```
376
-
377
- A per-agent list **replaces** the provider one rather than extending it, because
378
- a merge would append the provider's expensive tail to an agent that deliberately
379
- named two cheap models.
380
-
381
- **Exactly two things arm it, and "an error" is not one of them:**
382
-
383
- 1. **The CLI refuses the model.** Measured on Claude Code 2.1.280: a retired,
384
- sunset or made-up id produces an assistant frame tagged
385
- `"error": "model_not_found"` and a result with `is_error: true`,
386
- `api_error_status: 404` and the text *"There's an issue with the selected
387
- model (…). It may not exist or you may not have access to it."* Note that the
388
- result's `subtype` is `success`, which is why a refused model used to finish
389
- as an ordinary reply with the CLI's error standing in for Claude's answer.
390
- 2. **A usage limit with nowhere else to go.** Only when
391
- [account failover](#account-failover) has no other account to offer, meaning
392
- a single configured account or every other one already limited. **When
393
- another account exists the switch form wins and the chain does not fire**:
394
- moving your billing is your decision, moving to a cheaper model is not, and a
395
- per-model weekly cap is exactly the case a chain helps with.
396
-
397
- An expired login, a billing hold, a network failure, a tool error and every
398
- other CLI error kind are deliberately excluded: they fail identically on the
399
- next model, so retrying would spend a spawn per entry to print the same message.
400
-
401
- On a trigger the failed attempt is dropped whole (its process killed, its
402
- session id discarded), a fresh process spawns on the next model with the **same
403
- account, thinking budget and working directory**, the conversation replays into
404
- it, and a note goes into the reply:
405
-
406
- ```
407
- ▌ **model fallback:** "claude-opus-5" was refused by the Claude CLI
408
- (model_not_found: it is retired, misspelled, or this account cannot use it), so
409
- this turn is being served by "claude-sonnet-5" instead. The account, the
410
- thinking budget and the working directory are unchanged.
411
- ```
412
-
413
- The refused attempt's output never reaches you and never reaches a rebuilt
414
- transcript, and neither does the note, which the plugin wrote rather than
415
- Claude.
416
-
417
- The rails: **entries are model names from this plugin's own list** (an unknown
418
- one is refused with a warning and skipped, exactly as an unknown `forceModel`
419
- is); **the chain never crosses accounts**, since the `@account` marker is taken
420
- from the id the turn arrived with and an entry spelling its own is ignored;
421
- **each model is tried at most once per turn**, and **an exhausted chain surfaces
422
- the original error unchanged**. Unset (the default) means no chain, so upgrading
423
- never moves a turn onto a model nobody picked.
424
-
425
- Not applied to compaction turns (a second model would rewrite the summary
426
- opencode stores), to title stubs, or to the
427
- [interactive transport](#interactive-transport-experimental). A title stub never
428
- reaches the CLI, so there is nothing there to fall back from.
429
-
430
- ### Options reference
431
-
432
- ```json
433
- {
434
- "plugin": ["@khalilgharbaoui/opencode-claude-code-plugin"],
435
- "provider": {
436
- "claude-code": {
437
- "options": {
438
- "cliPath": "claude",
439
- "proxyTools": ["Bash", "Edit", "Write", "WebFetch", "Task"],
440
- "skipPermissions": true,
441
- "permissionMode": "default",
442
- "bridgeOpencodeMcp": true,
443
- "strictMcpConfig": false,
444
- "idleProcessTimeoutMs": 900000
445
- }
446
- }
447
- }
448
- }
449
- ```
450
-
451
- | Option | Type | Default | Description |
452
- |---|---|---|---|
453
- | `cliPath` | string | `"claude"` | Path to the `claude` executable (a binary, not a shell command with flags). opencode's config hook seeds this with `"claude"`, so under opencode this default always applies; `CLAUDE_CLI_PATH` is only consulted when `createClaudeCode()` is called directly and the option is absent. Account providers wrap it with a generated script; never point it at one of those yourself. |
454
- | `accounts` | string[] | – | **Optional.** Most setups need no accounts at all: with this unset you get a single `Claude Code (Default)` provider on your normal `~/.claude` login. Supply names only to run several Claude logins side by side; `default` stays implicit, so `["work", "personal"]` gives you `Claude Code (Default)`, `Claude Code (Work)` and `Claude Code (Personal)`. See [Multiple Claude Code accounts](#multiple-claude-code-accounts). |
455
- | `accountFailover` | `"ask"` \| `"off"` | `"ask"` | When this account runs out of usage mid-task, show a form listing the other configured accounts and continue on the one you pick, inside the same turn. Only ever fires when more than one account is configured, so a single-account setup is unaffected. `"off"` keeps the plain rate-limit error. See [Account failover](#account-failover). |
456
- | `cwd` | string | see description | Working directory for the spawned CLI. Resolved **lazily per request**, first match winning: this explicit value, then the opencode session's own `directory` (so `opencode serve` and the web UI spawn in the right project even though one server handles many), then `process.cwd()` when it is a real directory, then the project directory captured at plugin init (this rescues macOS GUI launches, where `process.cwd()` is `/`), and finally `process.cwd()` regardless. [Startup diagnostics](#startup-diagnostics) reports which tier won. Session tier contributed by [@galvani](https://github.com/galvani). |
457
- | `skipPermissions` | boolean | `true` | Pass `--dangerously-skip-permissions` to `claude`. It is still passed when `proxyTools` is set: proxied calls go through opencode's permission system regardless, but unproxied CLI built-ins do not. The one case where the flag is dropped is `permissionMode: "plan"`, because the CLI lets the skip flag override plan mode outright. See [Plan mode](#plan-mode). |
458
- | `permissionMode` | `acceptEdits` \| `auto` \| `bypassPermissions` \| `default` \| `dontAsk` \| `plan` | – | Forwarded to headless `claude --permission-mode`. `"plan"` also suppresses `--dangerously-skip-permissions` (see the row above). Not version-gated, so check that your installed CLI accepts the value. The [interactive transport](#interactive-transport-experimental) does not forward it. |
459
- | `permissionPreset` | `"read-only"` | – | A named permission posture, so you set one option instead of combining five and getting one wrong. Opt-in: unset is exactly today's behaviour. `"read-only"` replaces `skipPermissions`, `permissionMode`, `controlRequestBehavior` and `controlRequestToolBehaviors`, and filters the write and command tools out of `proxyTools`. See [Read-only mode](#read-only-mode). |
460
- | `defaultSubagentCacheTtl` | string | – | Prompt cache TTL (`5m` or `1h`) for plugin-discovered `mode: subagent` agents whose own definition states no `cacheTtl`. Reaches the CLI as `CLAUDE_CODE_PROMPT_CACHE_TTL`. Unset means the CLI keeps choosing, which is 1 hour on a subscription. An unrecognised value warns and changes nothing. See [The prompt cache an agent writes](#the-prompt-cache-an-agent-writes). |
461
- | `defaultSubagentModel` | string | – | Model that plugin-discovered `mode: subagent` agents run on when their own definition pins nothing. The caller's account is kept; only the model name changes. An agent's own `forceModel` wins over it, and an unknown id is refused rather than spawned. Unset means no implicit override at all. See [Subagents: your account, their model](#subagents-your-account-their-model). |
462
- | `fallbackModels` | string[] | – | Ordered models to try when the one a turn would run on is refused. The default for agents that declare no `fallbackModels` of their own; a per-agent list **replaces** this one rather than extending it. Always the same account, never a different one. Only two things arm it: the CLI refusing the model (`model_not_found`) and a usage limit on an account with no other account to offer. Each model is tried at most once per turn and an exhausted chain surfaces the original error. Unset means no chain at all. See [Fallback model chain](#fallback-model-chain). |
463
- | `proxyTools` | string[] | `["Bash", "Edit", "Write", "WebFetch", "Task"]` | Claude built-in tools to route through opencode's executor + permission UI. Opt-in extras: `"Question"`, `"Compress"`. See [Selective tool proxy](#selective-tool-proxy). |
464
- | `extraDisallowedTools` | string[] | – | Extra Claude built-ins to switch off with `--disallowedTools`, on top of what `proxyTools` implies. Claude's names, e.g. `["NotebookEdit"]`. See [Closing a tool with no proxy](#closing-a-tool-with-no-proxy). |
465
- | `proxyToolTimeoutMs` | `Record<string, number>` | – | Optional wall-clock backstop per proxy tool, in ms, keyed by proxy tool name (`bash`, `task`, …). A call normally ends on an event the plugin listens for (result, abort, next message, process exit, chat deletion), not on a timer; see [How a proxied call ends](#how-a-proxied-call-ends). Defaults: 10 min flat, `task` / `task_batch` → none, `question` → 30 min. `0` disables a tool's deadline; negative or non-numeric values are ignored. For `bash`, the call's own `input.timeout` is honoured on top (`max(resolved, input.timeout)`). See [Per-tool proxy timeouts](#per-tool-proxy-timeouts). |
466
- | `planModeQuestion` | boolean | `false` | Route `ExitPlanMode` approval through opencode's native `question` tool instead of a text "(yes/no)" prompt. Opt-in, and currently unreachable on the default headless transport, which is not offered an `ExitPlanMode` tool at all. See [Plan mode](#plan-mode). |
467
- | `controlRequestBehavior` | `allow` \| `deny` | `allow` | Default response when `skipPermissions: false` and Claude sends a `can_use_tool` control request. |
468
- | `controlRequestToolBehaviors` | `Record<string, "allow" \| "deny">` | – | Per-tool override for `can_use_tool`. Example: `{ "Bash": "deny", "Read": "allow" }`. |
469
- | `controlRequestDenyMessage` | string | built-in message | Message returned to Claude on a deny. |
470
- | `bridgeOpencodeMcp` | boolean | `true` | Auto-translate your opencode `mcp` block into Claude's `--mcp-config`. See [MCP bridge](#mcp-bridge). |
471
- | `mcpConfig` | string \| string[] | – | Extra `--mcp-config` paths/JSON passed alongside the bridged config. |
472
- | `strictMcpConfig` | boolean | `false` | Pass `--strict-mcp-config` so Claude loads **only** the configured servers and ignores `~/.claude/settings.json`. |
473
- | `hotReloadMcp` | boolean | `true` | With MCP bridging on, compare the merged MCP config and runtime status at the start of each turn and respawn the `claude` process when they drifted, so a server you just enabled, disabled or finished connecting becomes visible without restarting opencode or opening a new chat. It only ever acts at a safe boundary: never during `/compact`, never on the interactive transport, never while a proxied call is still in the air, a turn is still running or a plan-mode approval is outstanding, and the Claude session id is preserved for `--resume` so the conversation continues. A log line at INFO names which servers joined and left. A server that flaps buys at most one respawn per minute per conversation (`CLAUDE_CODE_MCP_HOT_RELOAD_COOLDOWN_MS`). Set `false` to keep a cached subprocess until the chat is reset. It does not reload other provider options and does not watch the contents of files named in `mcpConfig`. |
474
- | `mcpConnectWaitMs` | number | `3000` | How long the first turn of a conversation waits for MCP servers opencode reports as still connecting before planning the `claude` spawn without them. Only opencode 2 can report that state (`pending`); opencode 1's own status call blocks until every server has decided, so this budget is what makes the two majors behave alike. Set `0` to always plan with whatever the host says at that instant. Aborting the turn also ends the wait at once. A server slower than the budget is not lost either way: it is still bridged, and `hotReloadMcp` moves the conversation onto a process that has it on the next turn. |
475
- | `proxyOpencodeMcpTools` | boolean | `false` | Route opencode's MCP-backed tools through the in-process `opencode_proxy` server instead of bridging them straight into Claude's `--mcp-config`, so each call executes once, inside opencode, with its permission prompt and its tool row. **The default changed from `true` to `false` in this release, and no behaviour changed with it:** at `true` it used to route nothing at all, because discovery read opencode's tool registry, which contains built-ins and plugin-declared tools and has never contained an MCP tool. Discovery now reads the model tool set opencode passes the provider, which is where MCP tools actually are, so the option works, and turning it on is the operator's decision rather than a silent migration of traffic that the direct bridge is handling today. Two caveats before enabling it: pair it with `strictMcpConfig: true`, because a server also registered in Claude Code's own config is reached directly and bypasses the proxy entirely; and a routed call runs in opencode with the calling agent's permissions, the same trade [`proxyOpencodeTools`](#options-reference) makes. Servers whose tools are not found stay on the direct bridge, and a warning says so, so do not treat this as an exactly-once guarantee for write-capable tools. |
476
- | `proxyOpencodeTools` | string[] | `[]` | Forward explicitly named opencode tools through the proxy (for example, a plugin's `compress` or V2 Code Mode `execute`). V1 uses registry ids; V2 uses the current model tool snapshot, including its real JSON Schema and agent visibility, not the registry's empty schemas. A forwarded tool runs inside opencode with the calling agent's permissions. A name already held by a proxy def is dropped with a warning. The read-only preset refuses `execute`. See [Forwarding opencode's own tools](#forwarding-opencode-s-own-tools) and [V2 Code Mode](#v2-code-mode). |
477
- | `stripContextReminders` | boolean | `false` | Remove opencode-dcp's `<dcp-system-reminder>` blocks from message text when no `compress` tool is proxied, so an order the model cannot follow stops being re-sent with every message that carries it. Inert as soon as `compress` is reachable. See [Trimming unsatisfiable context reminders](#trimming-unsatisfiable-context-reminders). |
478
- | `webSearch` | `"claude"` \| `"disabled"` \| `<tool>` | `"claude"` | Routing for Claude's built-in `WebSearch`. See [WebSearch routing](#websearch-routing). |
479
- | `multiStepContinuation` | boolean | `true` | Append a system-prompt hint nudging Claude to chain tool calls within one turn instead of pausing between subtasks. Each opencode turn boundary requires the user to manually press "continue", so for multi-step tasks this reduces friction. Set `false` to disable. |
480
- | `autoContinueIncompleteTurns` | boolean \| `"smart"` | `"smart"` | Smartly continue incomplete Claude CLI results inside the same opencode turn. Reduces manual "continue" presses when Claude ends after reasoning/tool activity without a useful final answer. Also gates the `▌ **no reply:**` note on a turn that ends with no text and no tool call. Set `false` to disable both. |
481
- | `compactionModel` | string | `"claude-haiku-4-5"` | Model used when opencode invokes `/compact`. Override per-process via the `CLAUDE_CODE_COMPACTION_MODEL` env var (env wins over config). See [Compaction](#compaction). |
482
- | `ignoreAnthropicApiKey` | boolean | `false` | Strip `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` from every spawned `claude` process so it authenticates with your logged-in subscription instead of pay-as-you-go API billing. The plugin warns once at startup whenever an API key is detected, regardless of this setting. See [Billing](#billing). |
483
- | `idleProcessTimeoutMs` | number | – | Kill a retained headless Claude worker after this many idle milliseconds following a completed turn. The timer starts when a turn finishes, a new turn cancels it, a worker that is mid-turn when it fires is left alone and re-timed, and the session id is preserved for `--resume`. Values above Node's maximum timer delay (`2147483647`) are ignored. Omit or set `0` to retain workers until LRU eviction (16 processes). Interactive transport is excluded. Contributed by [@bernardofortes](https://github.com/bernardofortes). |
484
- | `bridgeOpencodeSkills` | boolean | `false` | Expose your opencode skills to Claude's native `Skill` tool, from every root opencode itself reads. Off by default because every bridged skill is also in the system prompt opencode forwards, so a large set is paid for twice per turn; the bundled configuration skill is staged either way. See [Skill bridge](#skill-bridge). Written by [@broskees](https://github.com/broskees). |
485
- | `bridgeSkipNativeSkills` | boolean | `true` | Leave a skill unbridged when the Claude session already loads it from `$CLAUDE_CONFIG_DIR/skills`, the project's `.claude/skills`, or an installed plugin, so one skill does not reach the model twice. Matched by resolved path, by identical `SKILL.md`, or by name. `false` bridges everything and reinstates the duplicates. See [Skills Claude already has](#skills-claude-already-has). |
486
- | `logging` | object | all defaults | The plugin's own logger, four independent fields: `file` (boolean, default `false`), `dir` (string, default `~/.local/share/opencode-claude-code/`), `mode` (`"silent"` \| `"debug"`, default `"silent"`) and `level` (`"debug"` \| `"info"` \| `"notice"` \| `"warn"` \| `"error"`, default `"info"`). Goes under `provider.claude-code.options` like every other row here. See [Logging](#logging). |
487
- | `turnStats` | boolean | `false` | Append a one-line cost / duration / cache footer to each finished turn. See [Per-turn stats](#per-turn-stats). |
488
- | `forkSessions` | boolean | `false` | Branch a forked opencode session off the parent's Claude conversation with `--fork-session` instead of replaying its history as text. See [The prompt cache an agent writes](#the-prompt-cache-an-agent-writes). |
489
- | `interactive` | boolean | `false` | **Experimental.** Drive the interactive `claude` TUI (subscription billing) instead of headless `--print`. Requires opencode running under Bun with PTY support; silently falls back to headless otherwise. The tool proxy, `permissionMode` and `/btw` are all unavailable on it, so read [What it does not support](#what-it-does-not-support) before enabling. Env: `CLAUDE_CODE_INTERACTIVE_TRANSPORT=1`. |
490
- | `interactiveBypass` | boolean | `false` | Deprecated/no-op with `interactive`: Claude Code's TUI shows a manual safety confirmation for `bypassPermissions`, so the plugin intentionally does not pass it. |
491
- | `interactiveAllowTools` | string[] | `["Bash", "Edit", "Write", "Read", "WebFetch"]` | With `interactive`: built-in tools pre-allowed without prompting (replaces the default list). MCP server wildcards (`mcp__<server>__*`) are always added from the bridged config. |
492
- | `interactiveSystemPrompt` | boolean | `true` | With `interactive`: append this plugin's CLI/AGENTS/continuation prompt via `--append-system-prompt-file`. The transport intentionally does not forward opencode's own system prompt, because it can trigger Claude Code's third-party-app usage gate on subscription accounts. Set `false` only for diagnostics. |
493
-
494
- ### Environment variables
495
-
496
- Every variable the plugin itself reads, in one place. Config is read once at opencode startup, so these are the way to change behaviour for a single run without editing `opencode.json`. Claude Code's own variables (`CLAUDE_CODE_DISABLE_THINKING` and friends) are passed through untouched and are listed under [Extended thinking](#extended-thinking).
497
-
498
- | Variable | Read by | Effect |
499
- |---|---|---|
500
- | `CLAUDE_CLI_PATH` | provider factory | Fallback `claude` path when `cliPath` is absent. Under opencode the config hook always supplies `cliPath`, so this only applies to direct `createClaudeCode()` use. |
501
- | `CLAUDE_CODE_COMPACTION_MODEL` | compaction spawn | Model for `/compact`. Wins over the `compactionModel` option. See [Compaction](#compaction). |
502
- | `CLAUDE_CODE_INTERACTIVE_TRANSPORT` | transport selection | `1` turns on the experimental [interactive transport](#interactive-transport-experimental) for one process, same as `interactive: true`. |
503
- | `CLAUDE_CODE_INTERACTIVE_BYPASS` | transport selection | Requests `bypassPermissions` in interactive mode. Deliberately ignored, with a warning, for the reason in the `interactiveBypass` row above. |
504
- | `CLAUDE_CODE_START_WATCHDOG_MS` | start watchdog | Milliseconds a `claude` process may stay completely silent on stdout after a turn is written, or after a proxy tool result should have resumed it, before the plugin acts. First expiry respawns the process and resumes the session; a second ends the turn with an error rather than hanging. Default `90000`; a positive integer is required and anything else falls back to that. Mainly a knob for reproducing the hang. |
505
- | `CLAUDE_CODE_RESULT_FALLBACK_MS` | wire-inactivity watchdog | Milliseconds a `claude` process that has already produced output may stay silent on stdout before the turn is closed without a `result`. The close is announced in the reply as a `▌ **stream timeout:**` note. Default `60000`; a positive integer is required and anything else falls back to that. Like the start watchdog, mainly a knob for reproducing a hang. |
506
- | `OPENCODE_CLAUDE_CODE_LOG_FILE` | logger | `1` writes the log file, `0` forces it off even when `logging.file` is `true`. See [Logging](#logging). |
507
- | `OPENCODE_CLAUDE_CODE_LOG_DIR` | logger | Directory for the log file, overriding `logging.dir`. |
508
- | `OPENCODE_CLAUDE_CODE_LOG_LEVEL` | logger | Minimum level to emit, overriding `logging.level`. An unrecognised value falls through to config. |
509
- | `DEBUG` | logger | `DEBUG=opencode-claude-code` promotes the logger to `mode: "debug"`, echoing every emitted level to opencode's TUI. |
510
- | `OPENCODE_CLAUDE_CODE_PLUGIN_NO_CLEANUP` | startup cleanup | `1` skips the removal of a stale **unscoped** `opencode-claude-code-plugin` install from opencode's plugin cache. That old package is a different artifact that shadows this scoped one when both are present; set this if you are deliberately keeping it. |
511
- | `OPENCODE_CLAUDE_CODE_PLUGIN_FORCE_CLEANUP` | startup cleanup | `1` runs that cleanup even when the marker at `$XDG_STATE_HOME/opencode-claude-code-plugin/cleanup-stale.json` (default `~/.local/state/...`) records that this plugin version already swept. Without it the cleanup walks opencode's plugin cache once per installed version rather than on every launch. |
512
- | `OPENCODE_CLAUDE_CODE_NO_TMP_SWEEP` | scratch directory | `1` skips the sweep of `<tmpdir>/opencode-claude-code-<pid>` directories left behind by plugin processes that were killed. See [Scratch files on disk](#scratch-files-on-disk). |
513
- | `OPENCODE_WORKTREE` | MCP bridge | Overrides worktree-root detection, which otherwise walks up from the working directory looking for a `.git` entry. |
514
- | `CLAUDE_CODE_MCP_HOT_RELOAD_COOLDOWN_MS` | MCP bridge | Minimum gap between two `hotReloadMcp` respawns of one conversation, default `60000`. A server that flaps between connected and failed would otherwise cost a kill and a `--resume` spawn on every turn. `0` disables the guard; a real second change lands on the first turn after the gap. |
515
- | `OPENCODE_CONFIG` / `OPENCODE_CONFIG_DIR` | config discovery | Where the plugin looks for your opencode config when bridging MCP and skills. See [Discovery order](#discovery-order-highest-to-lowest-priority). |
516
- | `OPENCODE_VERSION` | startup diagnostics | Reported as the opencode version when set, sparing the plugin a `--version` spawn. Diagnostics only. |
517
- | `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` | spawn environment | Not set by the plugin: these are yours, and Claude Code authenticates with them in preference to your subscription login when present. `ignoreAnthropicApiKey: true` strips them from the spawn. See [Billing](#billing). |
518
- | `DISABLE_AUTOUPDATER` | spawn environment | Set to `1` on every `claude` the plugin spawns, **only if you have not set it yourself**. The plugin detects your CLI version once and caches it, and gates `--thinking-display summarized`, `--plugin-dir` and fast mode on the answer, so a CLI that updates itself mid-session would leave those gates describing a binary that is no longer running. Export `DISABLE_AUTOUPDATER=0` to keep the autoupdater; your value is never overwritten, and updating the CLI between opencode restarts works normally either way. |
519
- | `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | spawn environment | Set to `1` on every spawned `claude` under the same never-overwrite rule. It suppresses the CLI's non-essential network calls and is a second, independent way Claude Code declines to auto-update. Export it yourself (including as an empty string, which the CLI reads as off) to take control. |
520
-
521
- The plugin also honours the usual path conventions rather than defining its own: `XDG_CONFIG_HOME` and `XDG_CACHE_HOME` (falling back to `~/.config` and `~/.cache`), `HOME` / `USERPROFILE`, and Claude Code's `CLAUDE_CONFIG_DIR` when the interactive transport needs to find the session transcript. Account providers set `CLAUDE_CONFIG_DIR` themselves for the process they spawn.
522
-
523
- ### Overriding model metadata
524
-
525
- To rename a model, change a limit, or add a custom one:
526
-
527
- ```json
528
- {
529
- "plugin": ["@khalilgharbaoui/opencode-claude-code-plugin"],
530
- "provider": {
531
- "claude-code": {
532
- "models": {
533
- "claude-sonnet-4-6": {
534
- "name": "Sonnet (custom)",
535
- "limit": { "context": 1000000, "output": 32768 }
536
- }
537
- }
538
- }
539
- }
540
- }
541
- ```
542
-
543
- Anything you supply is merged on top of the defaults; you don't need to redeclare every model.
544
-
545
- ---
546
-
547
- ## Interactive transport (experimental)
548
-
549
- By default the plugin spawns `claude --print` (headless). The interactive transport instead drives the real interactive `claude` TUI under a native PTY inside opencode's Bun runtime, types your prompt into it, and streams the session transcript (`~/.claude/projects/<encoded-cwd>/<session-id>.jsonl`) back through the same pipeline the headless transport uses. Claude Code names that directory from the cwd's **resolved real path** with every non-alphanumeric character replaced by `-`, so a working directory reached through a symlink (on macOS `/tmp` is a symlink to `/private/tmp`) is named after the target: `/tmp/scratch` becomes `-private-tmp-scratch`. It was built as insurance for the day headless usage is billed differently from interactive usage; today both draw from the same plan usage limits (see [Billing](#billing)), so it is not a way to change what a turn costs.
550
-
551
- ```json
552
- "options": { "interactive": true }
553
- ```
554
-
555
- Or per-process: `CLAUDE_CODE_INTERACTIVE_TRANSPORT=1`.
556
-
557
- ### Requirements
558
-
559
- - opencode must be running under **Bun** with `Bun.Terminal` (PTY) support. If it isn't, the flag is ignored and the headless transport is used — nothing breaks.
560
- - A logged-in `claude` (subscription auth). The whole point is plan billing, so API-key auth gains nothing here.
561
-
562
- ### What carries over from the headless transport
563
-
564
- - The plugin's appended prompt (Claude CLI context, AGENTS.md guidance, continuation rules). The interactive transport intentionally does not forward opencode's own system prompt, because live testing showed that payload can trigger Claude Code's third-party-app usage gate on subscription accounts.
565
- - The MCP bridge: bridged servers are passed via `--mcp-config` + `--strict-mcp-config`, and every bridged server is pre-allowed as `mcp__<server>__*`.
566
- - The [skill bridge](#skill-bridge): the same `--plugin-dir` staging the headless spawn uses, so the TUI's native `Skill` tool can load your opencode skills too.
567
- - Model selection, session reuse, and the whole streaming/usage pipeline.
568
-
569
- Set `interactiveSystemPrompt: false` only for diagnostics. While disabled, the interactive session will not receive the plugin's CLI context, AGENTS.md guidance, or continuation hints.
570
-
571
- ### What it does not support
572
-
573
- This is the part to read before turning it on. Three whole features of this plugin are simply absent on the interactive transport:
574
-
575
- - **No tool proxy.** The interactive spawn starts no proxy MCP server at all, so `mcp__opencode_proxy__bash`, `edit`, `write`, `webfetch`, `task`, `task_batch`, `question` and `compress` do not exist for that session. Claude uses its own built-in tools directly, which means opencode does not execute them, does not prompt for them, and does not log them. Everything in [Selective tool proxy](#selective-tool-proxy) applies to the headless transport only.
576
- - **No `permissionMode`.** The interactive spawn never passes your `permissionMode` to the CLI, so `"plan"` and the rest have no effect there. Permission handling is the pre-allow list described below and nothing else.
577
- - **No [`/btw`](#side-questions-with-btw).** Side questions ride Claude Code's `side_question` control protocol over the headless process's stdio. Asking one in an interactive session returns an error telling you so.
578
-
579
- ### What else is different
580
-
581
- - **Permissions:** the interactive TUI has no `can_use_tool` control channel, so tools can't be approved per-call through opencode. Built-in tools are pre-allowed via a settings allow list (default `Bash, Edit, Write, Read, WebFetch`; override with `interactiveAllowTools`). `bypassPermissions` is intentionally not used here because Claude Code shows a manual safety confirmation in the TUI and defaults to exit.
582
- - **Input is text-only:** images and other non-text blocks are dropped (with a logged warning); tool results are rendered as labeled text.
583
- - **Output granularity:** text arrives per transcript record, not token-by-token, so it can feel chunkier than headless streaming.
584
- - **Token counts come from the transcript, one count per API call.** The session JSONL writes one record per content block (thinking, text, tool_use) and every record of a call repeats that call's final usage, so the transport counts each call once, keyed by its message id. The numbers then mean exactly what they do on the headless transport: [`turnStats`](#per-turn-stats) gets the turn's totals and opencode gets the last call's context plus the turn's output. Before this was fixed a four-tool turn reported 1,306 output tokens against a real 653, and its input and cache counts were one call's instead of the turn's. An all-zero `<synthetic>` record (how the CLI writes "Login expired" or a session limit into the transcript) is not counted as a call.
585
- - **How a turn finishes:** a turn that reaches a terminal stop reason (`end_turn`, `stop_sequence`, `max_tokens`) finishes exactly as a headless turn does, so it is an ordinary completed reply and [`turnStats`](#per-turn-stats) applies to it. `max_tokens` is deliberately a completed turn rather than a failure: the call happened and billed, and the truncation is what auto-continue reads. Before this was fixed every interactive turn finished as an error instead, which also suppressed the stats footer.
586
- - **Turn timeout:** a turn that produces no terminal stop within 30 minutes is reported honestly as an error result (visible truncation), not silently ended.
587
- - **No idle eviction:** `idleProcessTimeoutMs` does not apply to interactive sessions.
588
- - `/compact` always uses the headless transport regardless of this setting.
589
-
590
- ---
591
-
592
- ## Selective tool proxy
593
-
594
- This is the core feature.
595
-
596
- By default, the plugin proxies `Bash`, `Edit`, `Write`, `WebFetch`, and `Task`. It disables Claude's corresponding built-in tool and exposes an equivalent through an in-process MCP server. Claude calls the MCP version, which blocks until opencode runs the tool through its own executor and permission system.
597
-
598
- ### Default proxied tools
599
-
600
- | `proxyTools` value | Claude built-ins disabled | Proxy MCP tool exposed |
601
- |---|---|---|
602
- | `"Bash"` | `Bash` | `mcp__opencode_proxy__bash` |
603
- | `"Edit"` | `Edit`, `MultiEdit` | `mcp__opencode_proxy__edit` |
604
- | `"Write"` | `Write` | `mcp__opencode_proxy__write` |
605
- | `"WebFetch"` | `WebFetch` | `mcp__opencode_proxy__webfetch` |
606
- | `"Task"` | `Agent` | `mcp__opencode_proxy__task`, `mcp__opencode_proxy__task_batch`, and on a host that runs background subagents `mcp__opencode_proxy__task_status`, `mcp__opencode_proxy__task_cancel` |
607
- | `"Question"` | `AskUserQuestion` | `mcp__opencode_proxy__question` |
608
- | `"Compress"` | none | `mcp__opencode_proxy__compress` |
609
-
610
- ### OpenCode-native subagents
611
-
612
- `Task` is proxied by default. The proxy disables Claude CLI's `Agent` tool and emits an unexecuted `task` call; it does not register a replacement task tool. OpenCode's built-in TaskTool remains responsible for permission checks, creating or resuming the child session, selecting the configured subagent, and foreground/background lifecycle.
613
-
614
- - **Permissions:** the calling agent's `permission.task` rule applies to the target `subagent_type`. Grant `task: "allow"` on agents that should delegate without a prompt; an `ask` or `deny` rule remains authoritative. The plugin never bypasses this decision.
615
- - **Resume:** pass the child session ID back as `task_id` to continue that subagent session. Omit it to create a fresh child.
616
- - **Nested tasks:** current opencode defaults `subagent_depth` to `1`, so a first-level child cannot launch another child. Increase top-level `subagent_depth` to permit deeper nesting, and explicitly grant `permission.task` on every subagent that should delegate; opencode otherwise adds a task deny to spawned subagent sessions.
617
- - **Background:** see [Background subagents](#background-subagents) below. Foreground is the default.
618
- - **Several at once:** `mcp__opencode_proxy__task_batch` takes a `tasks` array of ordinary task inputs and runs them concurrently. It exists because Claude Code sends MCP requests one at a time: when the model emits two `task` calls in one response, the second only leaves the CLI after the first has returned (measured live, 2026-09-06), so "launch two subagents" was always serial. The plugin turns one `task_batch` call into N opencode `task` calls inside a single tool boundary, which opencode executes in parallel, then hands the model every result together, labelled in task order. Same permissions, same no-deadline default, same `subagent_type` list. Enabled whenever `Task` is proxied. Designed and first implemented by [@broskees](https://github.com/broskees) on his fork.
619
-
620
- **Steering models to it.** Headless Claude Code CLIs expose no `Agent`/`Task`
621
- dispatch tool of their own (verified on 2.1.211), while they *do* expose
622
- `TaskCreate` — a todo tool. So "use a subagent" requests get mis-resolved:
623
- a todo appears, nothing runs, and the model may still narrate a successful
624
- dispatch. Two spawn-time countermeasures prevent that. The plugin injects
625
- opencode's live agent-type list into the `task` proxy description (so the model
626
- picks a real `subagent_type` instead of guessing a Claude Code name like
627
- `general-purpose`, and doesn't grep configs to check a subagent exists), and
628
- appends a system-prompt note naming
629
- `mcp__opencode_proxy__task` as the only dispatch path — with the ToolSearch
630
- recovery step for harnesses that defer MCP tool schemas. Both apply per Claude
631
- process at spawn, and provider options are read once at opencode startup, so
632
- `proxyTools` changes need a full opencode restart.
633
-
634
- ### Background subagents
635
-
636
- A foreground `task` call blocks the conversation until the subagent finishes, and so does `task_batch`. Background dispatch is the other shape: start a subagent, keep working, collect the result later. opencode owns it, this plugin surfaces it, and it is **off unless the opencode process has `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`** (or the blanket `OPENCODE_EXPERIMENTAL=true`) in its environment on opencode 1.x. On opencode 2.x it is unconditional.
637
-
638
- ```sh
639
- OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true opencode
640
- ```
641
-
642
- With it set, `mcp__opencode_proxy__task` takes `background: true` and the call comes straight back:
643
-
644
- ```xml
645
- <task id="ses_f10789724ffes9OfQApCB04IRe" state="running">
646
- <summary>Background task started</summary>
647
- <task_result>
648
- The task is working in the background. You will be notified automatically when it finishes.
649
- </task_result>
650
- </task>
651
- ```
652
-
653
- Claude keeps working. When the subagent finishes, opencode prompts the same conversation with the result as a new message, so it arrives as its own turn rather than as that call's result. Measured end to end on opencode 1.18.33 with claude-haiku-4-5: the dispatch returned in 14 s while the child's 30-second command was still running, Claude ran another tool and ended its turn 18 s in, and the `<task ... state="completed">` message landed 43 s later. That notification is automatic, so the right thing after a background dispatch is to end the turn, not to wait or poll.
654
-
655
- **opencode 2 uses different envelopes for the same thing**, so the plugin tells the model about its own host's. There a background dispatch answers in prose rather than XML:
656
-
657
- ```text
658
- The subagent is working in the background (sessionID: ses_f0cb9005fffekrDPNa1Px8Jp0J). You will be notified automatically when it finishes.
659
- ```
660
-
661
- and the completion arrives as `<subagent sessionID="…" state="completed" description="…">`. That `sessionID` is the `task_id` for the two tools below. Measured on opencode 2.0.16.
662
-
663
- **The gate is enforced at the schema, not at the call.** On a host without the flag opencode rejects `background: true` outright with `Background subagents require OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`, which costs a whole dispatch. So the plugin reads the host's own `task` schema (the same registry fetch that supplies the agent-type list) and, when it has no `background` property, strips the field before Claude ever sees it. Nothing about a default install changes: the model is shown exactly `description`, `prompt`, `subagent_type`, `task_id`, `command`. Either way `plugin.log` says which:
664
-
665
- ```
666
- background subagent gate {"supported":true,"registryResolved":true,"hostApi":"v1", ...}
667
- ```
668
-
669
- **Collect and cancel.** opencode delivers a background result by pushing it into the conversation and offers nothing else: no route reads a result back, and nothing stops a background child. A notification that never lands (an interrupted turn, an errored turn, a compaction across it) would lose the work, and a subagent running away could only be stopped from another pane. So on a host that runs background subagents, and only there, two more proxy tools ride along with `Task` in the same way `task_batch` does:
670
-
671
- | Tool | What it does |
672
- | --- | --- |
673
- | `mcp__opencode_proxy__task_status` | Reads the state of a background subagent by its `task_id` and returns its result if it has finished. A recovery path, not a progress poll: a healthy background task delivers its own result. A result is handed over once, so asking again reports the state without repeating the output. |
674
- | `mcp__opencode_proxy__task_cancel` | Stops a background subagent. A cancelled subagent sends no completion notification. |
675
-
676
- The `task_id` is the child's own opencode session id: the `id` in the `<task …>` envelope on opencode 1.x, the `sessionID` the dispatch reported on opencode 2. Both tools are answered inside the plugin rather than executed by opencode, because opencode has no tools of these names, and both refuse any session whose parent is not the conversation doing the asking. Neither can be named in `proxyTools`: they appear only when the host advertises background support, so upgrading changes nothing about what the model can do or spend on a default install.
677
-
678
- Both work on opencode 1.x and on opencode 2. On opencode 2 they run over the session routes a plugin is actually given there (`session.context` and `session.interrupt`); opencode 2 gives a plugin no all-sessions run-state map, so "still running" is read off the child's own transcript instead. Verified live on 2.0.16: start, `task_status` answering `running`, `task_cancel` answering `Stopped.`, and a finished child collected once.
679
-
680
- **What `/claude-code-doctor` says about it.** The report has a **Background subagents** section: whether `background` was offered to Claude and the two tools registered, which opencode major, and what decided it (the live `task` schema, a registry that did not answer, or opencode 2 offering it unconditionally), plus the background tasks this process has collected or cancelled. The gate is read while a turn plans its proxy tools, so in a fresh process the section reads `Not read yet this process`: send one message and run it again.
681
-
682
- ### Proxy endpoint security
683
-
684
- The proxy is a small HTTP MCP server on an ephemeral loopback port, and calling it runs Bash, Edit and Write through opencode's executor. Since 0.13.2 it requires a 256-bit bearer token, generated per server and handed to Claude in the `headers` block of the `0600` MCP config file the plugin writes. Requests are also rejected unless the `Host` header matches the bound `127.0.0.1:<port>` authority, no `Origin` header is present, and the content type is `application/json`.
685
-
686
- **Upgrade if you are on 0.13.1 or earlier.** Before this, any local process could post to that port and execute commands as you, and a web page you visited could do the same blind, without reading the response. Reported by @willmcginnis in [#28](https://github.com/khalilgharbaoui/opencode-claude-code-plugin/pull/28); tracked as [GHSA-3mxm-w7gf-3c5x](https://github.com/khalilgharbaoui/opencode-claude-code-plugin/security/advisories/GHSA-3mxm-w7gf-3c5x) (High, CVSS 7.5). No exploitation is known: it was found by code audit, not an incident.
687
-
688
- **Restart every opencode you have running.** A plugin is read once, when the process starts, so an opencode you left open keeps the old code and keeps serving an unauthenticated proxy port for as long as it lives, however new the installed version is. Long-lived sessions are the ones to check:
689
-
690
- ```sh
691
- lsof -nP -iTCP -sTCP:LISTEN | grep opencode
692
- curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:PORT/mcp \
693
- -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
694
- ```
695
-
696
- A patched process answers `401`. A `200` is a pre-0.13.2 process still running, and restarting it is the fix.
697
-
698
- Nothing to configure. If proxied tools ever stop working after a Claude Code upgrade, check the plugin log for `proxy-mcp rejected a request`, which names which guard failed.
699
-
700
- ### Scratch files on disk
701
-
702
- Everything the plugin writes for the Claude CLI to read goes into one per-process directory, `<tmpdir>/opencode-claude-code-<pid>`, created `0700`:
703
-
704
- | File | Mode | Holds |
705
- | --- | --- | --- |
706
- | `mcp-<hash>.json` | `0600` | the bridged MCP config, including any `{env:VAR}` values substituted from your environment |
707
- | `proxy-<hash>.json` | `0600` | the proxy endpoint's bearer token |
708
- | `opencode-cc-sys-<uuid>.md` | `0600` | the full appended system prompt, forwarded opencode instructions and AGENTS.md included |
709
- | `skills-<hash>/` | inherits | staged skill plugin dirs for the skill bridge; the `0700` parent is what keeps them private |
710
-
711
- On a shared host the OS tmpdir is world-writable and the pid name is guessable, so the plugin refuses a path that already exists but is a symlink, is not a directory, or is not owned by you: it falls back to a fresh `mkdtemp` name and logs a warning naming both paths.
712
-
713
- The directory is removed on normal exit. `SIGKILL` skips that, so on first use each run the plugin also sweeps `<tmpdir>/opencode-claude-code-<pid>` directories whose pid is no longer running and which you own. Anything else, another user's directory, a live process's, a symlink, a name that is not exactly that pattern, is left alone. Set `OPENCODE_CLAUDE_CODE_NO_TMP_SWEEP=1` to turn the sweep off.
714
-
715
- **Windows is not hardened here.** Both spawn sites pass `shell: true` on `win32`, so the CLI argument list goes through `cmd.exe` unquoted. Treat Windows as unsupported until that is fixed; see the note in `docs/agents-history.md`.
716
-
717
- ### Closing a tool with no proxy
718
-
719
- `proxyTools` only reaches built-ins the plugin can replace. A built-in with no opencode equivalent, `NotebookEdit` today and whatever Claude Code ships next, stays enabled and unmediated no matter what you put in that list. `extraDisallowedTools` names them directly:
720
-
721
- ```json
722
- "options": {
723
- "extraDisallowedTools": ["NotebookEdit"]
724
- }
725
- ```
726
-
727
- These go straight to `claude --disallowedTools`, so use Claude's tool names rather than opencode's. There is no replacement: the capability goes away rather than being routed through opencode, which is the point, but the model then has to work without it.
728
-
729
- Unknown entries in `proxyTools` are logged as a warning at spawn rather than passing silently, so a typo shows up as "ignoring unknown proxyTools entries" in the plugin log instead of quietly leaving the matching built-in unmediated.
730
-
731
- ### Context compression
732
-
733
- `"Compress"` is off by default. Add it when you run a harness that expects the model to manage its own context (opencode-dcp injects exactly those instructions), and the plugin exposes `mcp__opencode_proxy__compress`:
734
-
735
- ```json
736
- "options": {
737
- "proxyTools": ["Bash", "Edit", "Write", "WebFetch", "Task", "Compress"]
738
- }
739
- ```
740
-
741
- It is the one proxy tool opencode never sees. The call is answered inside the plugin: the model passes a `summary`, the plugin stores it, and the turn continues normally. At the start of the **next** turn the Claude Code session is discarded and a fresh `claude` starts with that summary prepended to its system prompt, and nothing else. The earlier conversation is not replayed, so a thin summary means real lost context. The reset waits if the incoming turn is carrying tool results for the running process.
742
-
743
- Without it, the appended system prompt tells the model that `compress` is unavailable and to ignore instructions that ask for it, which is the right answer when nothing implements it.
744
-
745
- The round trip is verified live (Claude Code 2.1.263, opencode 1.18.31, haiku): the model called `mcp__opencode_proxy__compress` with a build identifier in its summary, the plugin logged `compress stored summary; session resets next turn`, the next turn logged `compress reset: dropped claude process and session id` and spawned a second `claude`, and that fresh process answered with the identifier it could only have read from the summary in its system prompt.
746
-
747
- Only those seven values are actually proxied; anything else you put in `proxyTools` is ignored. Proxying `Edit` also disables `MultiEdit` — opencode has no batched-edit equivalent, so Claude is forced to fan out into single `Edit` calls that each flow through the permission UI. The `"Question"` proxy is version-gated on opencode's built-in `question` tool: on builds that lack the registry entry the def is silently dropped (a forwarded call would otherwise render as `⚙ invalid`), so add it only on opencode versions that ship the `question` tool.
748
-
749
- Without `"Task"` in `proxyTools`, Claude's built-in `Agent` tool stays enabled and Claude orchestrates subagents internally with no opencode child-session visibility. To opt out of all proxying, including Task, use an explicit empty list:
750
-
751
- ```json
752
- "options": { "proxyTools": [] }
753
- ```
754
-
755
- ### Forwarding opencode's own tools
756
-
757
- `proxyTools` names the tools this plugin ships defs for, and MCP-backed opencode tools can be routed with [`proxyOpencodeMcpTools`](#options-reference). Neither covers a tool that **another opencode plugin declares directly**: it belongs to no MCP server, so the automatic match (`<server>` or `<server>_<tool>`) skips it and the model is never offered it. opencode-dcp's `compress` is the case that matters in practice, because DCP then injects "MAX CONTEXT LIMIT REACHED ... You MUST use the `compress` tool now" reminders that the model has no way to act on.
758
-
759
- `proxyOpencodeTools` is the explicit allowlist. Empty by default:
760
-
761
- ```json
762
- "options": {
763
- "proxyTools": ["Bash", "Edit", "Write", "WebFetch", "Task"],
764
- "proxyOpencodeTools": ["compress"]
765
- }
766
- ```
767
-
768
- Names are opencode's tool ids as `client.tool.list()` reports them, matched case-insensitively. An unknown name is skipped with a warning rather than failing the spawn. Forwarded tools use the same broker as every other proxy tool, so [how a proxied call ends](#how-a-proxied-call-ends) applies to them unchanged: abort, orphan sweep, session deletion and child exit all release them.
769
-
770
- This is deliberately not automatic. A forwarded tool executes inside opencode with the calling agent's permissions, so which ones cross over is your decision, not the plugin's.
771
-
772
- **The `compress` name collision.** Two different tools want it: DCP's, which rewrites opencode's transcript, and [this plugin's](#context-compression), which resets the Claude Code session. They compress different windows, and after a DCP compress the live `claude` process still holds its full context until something restarts it. If you enable both, the plugin's own tool keeps the name and the forwarded one is dropped with a warning in the log:
773
-
774
- ```
775
- WARN: proxyOpencodeTools entry dropped: a proxy tool already holds that name, and it keeps it {"collided":["compress"]}
776
- ```
777
-
778
- Pick one. The appended system prompt describes whichever is actually reachable, so the model is told the right semantics either way.
779
-
780
- Verified live on Claude Code 2.1.263 and opencode 1.18.31 with DCP loaded: the plugin logged `forwarding opencode tools through the proxy {"tools":["compress"]}`, started the proxy with `tools: ["bash","compress"]`, received `proxy-mcp tool call received {"toolName":"compress"}`, queued it through the normal broker, and DCP really ran, returning `Compressed 3 messages into [Compressed conversation section]`. One wrinkle worth knowing: DCP's compress rewrites opencode's message history mid-turn, which makes opencode abort the provider stream at that tool boundary. The pending call is released normally and the result still reaches the model on the next step as text, so the turn completes, but you will see one `abort between proxy tool boundaries` line in the log each time.
781
-
782
- ### Trimming unsatisfiable context reminders
783
-
784
- DCP anchors its nudges into message text as `<dcp-system-reminder>` blocks, so each one is re-sent with every message that carries it. If no `compress` tool is reachable they are an order the model cannot follow, and the plugin already tells it to ignore them. `stripContextReminders: true` stops paying for them too:
785
-
786
- ```json
787
- "options": { "stripContextReminders": true }
788
- ```
789
-
790
- Off by default. It removes those blocks from user and assistant text before the transcript reaches the CLI, including the fresh-session rebuild, where every anchored reminder would otherwise replay at once. It leaves opencode's own `<system-reminder>` blocks alone: those are opencode's instructions to the model, not an unsatisfiable order.
791
-
792
- It switches itself off whenever `compress` is named in `proxyTools` or `proxyOpencodeTools`, since the reminder is then something the model can act on. The check is on configuration, so a name that is configured but missing from opencode's registry still counts as reachable and nothing is stripped, which errs toward keeping the reminder.
793
-
794
- ### Subagent todos
795
-
796
- When Claude works through a multi-step task it emits `TaskCreate` / `TaskUpdate` calls. The plugin translates those into opencode's full-list `todowrite` so the todo panel populates. Inside a **subagent** that translation is blocked unless you say otherwise: opencode's task tool injects `todowrite: false` into the tools dict for any subagent without an explicit rule, so the plugin's synthetic emissions surface as `⚙ invalid todowrite` rows instead of todos. The built-in `general` subagent denies it by default.
797
-
798
- Grant it per subagent definition in `opencode.json`:
799
-
800
- ```json
801
- {
802
- "agent": {
803
- "multistep": {
804
- "description": "Multi-step worker whose progress should be visible as todos",
805
- "mode": "subagent",
806
- "model": "claude-code-default/claude-opus-5",
807
- "permission": {
808
- "todowrite": "allow",
809
- "todoread": "allow",
810
- "task": "deny"
811
- }
812
- }
813
- }
814
- }
815
- ```
816
-
817
- Notes on that example:
818
-
819
- - `todowrite: "allow"` is the load-bearing line. Without it you get `⚙ invalid` rows, not a broken run.
820
- - `todoread` is worth allowing too so the subagent can re-read its own list across turns.
821
- - `task: "deny"` is explicit rather than implied. Leave it denied unless this subagent should itself delegate, in which case set `"allow"` and raise the top-level `subagent_depth` (opencode defaults it to `1`, so a child cannot spawn a grandchild).
822
- - Provider and agent config are read at startup, so restart opencode fully after editing.
823
-
824
- The todos render in the **subagent's own session view**, not the parent's panel. Navigate to it in the TUI with `session.child.next` (and back with `session.parent`); run `opencode --print-logs` or check the keybindings if those actions are unbound in your setup.
825
-
826
- To confirm the data actually landed rather than trusting the UI:
827
-
828
- ```bash
829
- sqlite3 ~/.local/share/opencode/opencode.db \
830
- "select id, parent_id from session order by rowid desc limit 5;"
831
- # then, with the child session id:
832
- sqlite3 ~/.local/share/opencode/opencode.db \
833
- "select tool, state from part where session_id='<child-id>' and tool='todowrite';"
834
- ```
835
-
836
- ### What you get with proxying on
837
-
838
- - opencode's **permission prompts** for every Bash/Edit/Write/WebFetch call. The default `--dangerously-skip-permissions` is still passed to `claude`, but it only governs Claude's own built-in tools; a proxied call is executed by opencode and answers to opencode's rules instead. Built-ins that are neither proxied nor listed in `extraDisallowedTools` do run under that flag.
839
- - opencode's **audit log** captures the calls.
840
- - Per-tool **policy rules** in opencode apply.
841
-
842
- ### What you give up
843
-
844
- - A small per-call latency hop through `127.0.0.1:<random>/mcp`.
845
- - Batched-edit ergonomics: with `Edit` proxied, Claude can no longer use `MultiEdit`, so a refactor that would have been one tool call becomes N single `Edit` calls.
846
- - **One extra Claude Code API call per `claude` process**, and it is a `ToolSearch`. A proxied tool reaches the model as an MCP tool, and Claude Code 2.1.280 defers MCP tools behind its own `ToolSearch` tool, so before the first proxied call of a session the model spends one request finding the tool. Claude's built-in `Bash` is never deferred, so an unproxied tool goes straight to the call.
847
-
848
- Measured on 2.1.280 with `claude-haiku-4-5`, three runs a side, one `echo` command: 3 CLI API calls with `Bash` proxied against 2 with the CLI running it, and roughly twice the cache reads. It is paid **once per process, not once per call**: the same task with two sequential commands measured 4 calls against 3, with a single `ToolSearch` either way. It is also not a function of how many tools you have, since a run with `strictMcpConfig: true` and 28 tools still spent it. Setting `ENABLE_TOOL_SEARCH=0` does remove it, and costs far more than it saves (all ~164 tool definitions then sit in every prompt, which measured 2.5 to 4 times the total cost and tripped a compaction), so that is not a fix and the plugin does not do it. `ToolSearch` is one of Claude's internal tools, so you never see the call, only the cost. Full numbers: `docs/agents-history.md` under `#g166`.
849
-
850
- ### How a proxied call ends
32
+ 1. **Install and log in the Claude Code CLI.** The plugin drives an existing `claude`; it does not bundle one.
851
33
 
852
- A proxied call ends when something happens to it, not when a clock runs out. The plugin holds the CLI's request open and listens to the process, the stream and the protocol for the events that actually decide the call's fate; each one releases the call on the spot, and tells the CLI where there is still a CLI to tell:
34
+ ```bash
35
+ claude --version # e.g. 2.1.284 (Claude Code)
36
+ claude auth status # which account you are signed in as
37
+ claude auth login # only if you are not signed in yet
38
+ ```
853
39
 
854
- | What happens | What the plugin does |
855
- |---|---|
856
- | opencode returns the tool's result | resolves the call; the CLI gets the result and carries on |
857
- | you abort the turn (Esc / Ctrl+C) | sends the CLI an `interrupt`, which answers with its own result, and rejects every call the turn had pending, whether the abort lands before content, mid-turn, or while opencode is running the tool between two stream boundaries. A stop that lands before the turn has asked the CLI for anything is the one exception: it interrupts nothing and releases nothing, because the calls still pending there belong to the previous step and the next message orphans them as usual |
858
- | you send the next message in that chat | rejects every call the previous turn left pending as orphaned, so the CLI gets an error result and the new turn starts clean |
859
- | the `claude` process closes its output or exits, mid-turn or between turns | rejects its pending calls; a mid-turn death also ends the turn as a visible error |
860
- | you delete the chat in opencode, or opencode exits | kills the worker and rejects its pending calls |
861
- | the CLI hangs up on its own request | keeps the call so a late result can still be delivered as a plain-text continuation (see below) |
862
-
863
- Because every ending is observed rather than inferred from elapsed time, a `task` can run until it is finished: **`task` and `task_batch` have no deadline by default**. Earlier flat ceilings fired mid-subagent, Claude believed its dispatch had failed, and the eventual result was dropped because the parent turn had already ended on the timeout error; a 60-minute one did the same to anything longer. What the default gives up is only that nothing fires on the clock alone, so a chat parked in a `task` holds its `claude` worker until one of the events above happens. That is the operator's decision to make, so no timer makes it for them.
864
-
865
- So that a call with no deadline is never silent, the plugin says it is still waiting. Five minutes in, and every five minutes after, a call without a deadline logs a warning naming the tool, the call id, how long it has waited, and what will end it. It never ends the call, it only reports one, which is the whole point: the thing a deadline used to provide was visibility, not correctness, and visibility is what is kept. Calls that do have a deadline get one notice rather than a heartbeat, at 60% of the way to it, saying how long is left and which option would extend it. Before this, a deadline reported a call only by killing it: the first thing you heard was the failure, which is no use while there is still time to react. It is one line, never repeated, because the deadline itself is the next thing that will speak, and deadlines under a minute are skipped entirely since the notice and the rejection would arrive together. The line reaches your terminal (warnings always go to stderr), so a subagent that has genuinely wedged shows up on its own instead of waiting to be noticed. `/claude-code-doctor` lists the same calls on demand.
866
-
867
- The same events are also what let a legitimately long call complete, which is the second half of the story: the CLI's own HTTP client used to give up on a silent reply at about five minutes whatever the tool deadline said. Every held call therefore keeps its connection visibly alive. A client that advertises SSE gets immediate headers and a keepalive comment every 15 seconds (since 0.15.0); a client that only accepts JSON gets its headers immediately as well, as a chunked body carrying keepalive whitespace on the same cadence, which is still one valid JSON-RPC response when the result lands, on success and on error. Keepalives are about the connection, not the tool: they never extend or replace a deadline. Claude's MCP client timeout for the proxy server, written into the generated `--mcp-config`, is set to the largest effective deadline, and to the largest value the CLI accepts (Node's timer maximum, about 24.8 days) while any tool has no deadline, because the CLI rejects a `timeout` of `0` outright.
868
-
869
- ### Per-tool proxy timeouts
870
-
871
- Deadlines still exist, as an explicit backstop rather than the mechanism that decides when a call is over. If a tool with one has not been resolved within that many milliseconds, the call is rejected and Claude receives a timeout error.
872
-
873
- A deadline does not count time opencode is still spending on the call. When it passes, the plugin asks opencode whether the session is still busy. If it is (a permission prompt waiting for your answer, or the tool itself still running), the call keeps waiting and is checked again every minute. The deadline only applies once opencode is idle, or when opencode cannot be asked. Before this, answering a permission prompt after ten minutes meant Claude had already been told the command timed out. Your late approval then cancelled Claude's next action, which it reported as you rejecting it. Resolved per tool, most-specific layer winning:
874
-
875
- 1. flat default — 10 min (matches Claude CLI's own Bash ceiling)
876
- 2. per-tool default: **`task` / `task_batch`: none**, **`question`: 30 min**, everything else: 10 min
877
- 3. your `proxyToolTimeoutMs` override (case-insensitive key; a positive value replaces the default, `0` removes the deadline, anything else is ignored)
878
- 4. for `bash` only, the call's own `input.timeout`: the proxy never undercuts a build the caller explicitly asked to run long (`max(resolved, input.timeout)`), and a positive `input.timeout` restores a deadline that `bash: 0` removed
879
-
880
- `question` keeps 30 minutes because it blocks on a human reading a form, and a form nobody answers is not an event. A positive `task` override restores a wall-clock backstop for operators who want one; if it fires, the error tells Claude not to "schedule a wake-up": that is a Claude Code affordance which cannot fire in this headless/proxy context, so deferring silently loses the work.
881
-
882
- Two watchdogs are a different thing again and are unchanged: the start watchdog (90 s of complete silence after a turn is written, respawn then error, see `CLAUDE_CODE_START_WATCHDOG_MS`) and the wire-inactivity watchdog (60 s of silence after content, see `CLAUDE_CODE_RESULT_FALLBACK_MS`; when it fires the reply gets a `▌ **stream timeout:**` note so the turn does not just stop). Those exist because a process that is alive but wedged emits no event to listen to, and a proxy call is never what they are waiting on: a CLI parked inside a proxied tool is producing nothing on purpose, and both watchdogs know that.
883
-
884
- If Claude nevertheless abandons the HTTP call, the plugin preserves narration emitted while opencode was running the tool, renders it on return, and delivers the late completion as a plain-text continuation naming the original call. It tells Claude not to run the tool again. A silent post-tool continuation gets one resumed-process retry, preserving the original model, account, effort, and proxy configuration; a second failure ends with an error rather than an indefinite hang. Buffered narration is capped at 500 lines and 2 MiB, with a warning if output was dropped.
885
-
886
- ```json
887
- "options": {
888
- "proxyTools": ["Bash", "Edit", "Write", "WebFetch", "Task"],
889
- "proxyToolTimeoutMs": { "Task": 5400000, "bash": 1800000 }
890
- }
891
- ```
892
-
893
- ---
894
-
895
- ## Side questions with /btw
896
-
897
- After a normal Claude Code turn, at any time, including while Claude is still working:
898
-
899
- ```text
900
- /btw Why did you choose that approach?
901
- ```
902
-
903
- The plugin registers the command without replacing an existing user-defined `btw` command. The question goes to Claude Code's native `side_question` control protocol on the conversation's live process, using the same model, account, and context. Claude Code answers it on a separate call, concurrently with whatever the main turn is doing. Claude never sees the aside afterwards: the question never enters Claude Code's own transcript, and the plugin keeps every `/btw` exchange out of the prompt it sends the model.
904
-
905
- Where the answer appears, in the conversation either way:
906
-
907
- - **A receipt, straight away**, when you asked while a turn was running, in the reply you are watching, so a `/btw` typed mid-turn is visibly taken rather than looking swallowed until the answer arrives:
908
-
909
- ```text
910
- ▌ **btw:** <your question, in full>
911
- ▌ *sent to Claude on the side*
912
- ```
913
-
914
- It quotes the question back untruncated because the prompt box clears on submit and no `/btw` message is ever created, so this is the only place you can read back what you sent. If opencode is between two streams at that moment (it was running a tool), the receipt lands when the next one opens.
915
- - **Inside the running turn's own reply**, as soon as the answer arrives, when you asked while Claude was working. It is written into the reply you are already watching as its own block, headed `▌ **btw:** <your question>`, so it stays there and is easy to pick out. Every line of the aside, answer included, carries that `▌` bar, so it reads as one block down its whole height. Nothing is queued and the `/btw` message itself is dropped, because the answer is already in the transcript. The turn goes on to deliver its own reply as usual.
916
- - **As its own `/btw` message and answer** when the conversation is idle, or when the turn had no stream open to write into at that moment (opencode was running a tool between two of them). In the second case the pair lands when the turn ends; nothing is announced in the meantime, because the answer itself is what arrives.
917
- - Follow-ups work: earlier asides in the conversation are sent along as the aside's history.
918
-
919
- Notes:
920
-
921
- - Requires Claude Code CLI **2.1.258 or newer**, the oldest verified version.
922
- - Requires a live **headless** process for the conversation. Send a normal message with a Claude Code model first if the process has not started or was evicted; the answer in the transcript tells you when that is the case. Interactive transport is not supported.
923
- - Asking immediately after starting a turn is fine. The conversation's process only exists once that turn reaches the model, so `/btw` waits for it (up to 30 seconds) instead of falling back to being queued. If no Claude Code process turns up in that window, because the running turn belongs to another provider, the question is answered when the turn ends.
924
- - One aside per conversation at a time. A second `/btw` while one is in flight is asked once the turn ends.
925
- - An aside costs nothing in opencode's counters: a `/btw` pair reports 0 tokens and $0, and a block written into a running turn adds nothing to that turn's usage. The control response has no usage fields, so aside usage is not counted anywhere; this does not mean the request is free.
926
- - An aside written into a turn is marked, and the plugin strips it again if the conversation ever has to be replayed into a fresh Claude Code process. It was never Claude's own output.
927
- - A request times out after two minutes. Abort and timeout cancel that side request without killing the main session. If the running turn is still not over after 30 minutes, the plugin gives up on that `/btw`; ask again once the turn ends.
928
- - The answer is never delivered as a notification: it always lands in the conversation, where it stays. The only two toasts left are the cases where nothing reaches the conversation at all, a bare `/btw` (which shows the usage text) and a turn that ran past the 30 minute wait.
929
-
930
- Fully restart opencode after upgrading to load the command and runtime changes. Other providers do not gain Claude's native side-question behavior from this command.
931
-
932
- ## Plugin health with /claude-code-doctor
933
-
934
- ```text
935
- /claude-code-doctor
936
- ```
937
-
938
- Prints, in the chat, what the plugin currently thinks is happening. The plugin answers it itself: no model is called, nothing is billed, and the reply reports 0 tokens. It is the thing to paste into a bug report.
939
-
940
- It carries the startup-diagnostics fields (plugin version, opencode version, `claude` path and version, the working directory and which resolution tier picked it, providers, accounts, `proxyTools`, the on-disk MCP servers, the `permissionPreset` in force per provider, transport, whether an `ANTHROPIC_API_KEY` is present) plus the live runtime state the startup block cannot know:
941
-
942
- - every live `claude` child, by opencode session id and model, with its pid, whether a turn is in flight, how long it has been up, and the effort it was spawned at,
943
- - every pending proxy call, with the tool, the call id, how long it has waited, and its deadline,
944
- - each proxy server's URL with one unauthenticated `initialize` posted to it: `401, good` is the patched behaviour, and anything else is flagged unsafe with the fix (restart every opencode window, since a window opened before 0.13.2 keeps serving an open port). See [Proxy endpoint security](#proxy-endpoint-security).
945
-
946
- When Claude Code refused an entry in an `--mcp-config` it was handed, an **MCP config entries Claude Code skipped** section names each one with the CLI's own category and sentence. That section only appears when there is something in it. It matters because a skipped server is absent from the CLI's server list entirely rather than listed as broken, so the model silently does not have those tools; if the skipped name is `opencode_proxy` the report says so plainly, because then it is the plugin's own server and every proxied tool call in the session fails. The same thing is a warning in your terminal when it happens.
947
-
948
- A **Plugins Claude Code did not load** section works the same way for Claude plugins: one the CLI demoted at load time (for example, a dependency that is not installed) is absent from its plugin list, so its skills, commands and MCP servers are silently missing. The skill bridge is such a plugin (`opencode-skills`), and a failure there is reported as the plugin's own bug rather than your config. A plugin warning only counts when its content did not load; advisory feedback about a plugin that did load stays in the log at INFO.
949
-
950
- A **Hooks Claude Code ran that failed** section covers your own Claude Code hooks, which are the third thing that fails without leaving a trace. Claude Code runs a `SessionStart` hook on every `claude` it starts for you, and when one exits non-zero it discards the hook's contribution and answers the turn normally: the context that hook was supposed to add is simply missing, on every turn of that session, with nothing on screen. The section names the hook, the event, its exit code and its outcome, and it is a warning in your terminal the first time it happens. Only the hook's **stderr** is shown, capped: a hook's stdout is what Claude Code splices into the model's context, so it has no business in a bug report. These are your hooks in your Claude Code settings, not opencode's, and the plugin never passes `--include-hook-events`, so only the `SessionStart` family is ever reported.
951
-
952
- ```text
953
- /claude-code-doctor usage
954
- ```
955
-
956
- adds a **Plan usage** section: the CLI's own answer to `/cost`, which is the subscription-or-API-key line, how much of the 5-hour and 7-day windows is used, when each resets, and what has been contributing to them. It is measured free (`num_turns: 0`, `$0`, no API call: the CLI answers it locally), so it costs no tokens and nothing is billed. It is opt-in anyway because reading it starts a short-lived `claude` process, which runs your `SessionStart` hooks and takes a few seconds. Without the argument the section says so and the report stays instant. A CLI that cannot answer leaves one line saying why and the rest of the report is unaffected.
957
-
958
- The `permissionPreset` row reads `provider: preset` for every registered provider, `none` where none is set, so two accounts configured with different postures are not collapsed into one answer. When a preset is in force, a **Permission preset overrides** block under the table lists the options it replaced, in the same words the log uses. A name the plugin does not recognise is reported as `readonly (unknown, nothing applied)` rather than shown as if it took effect: a typo'd safety option runs at full permissions, and the report is where you find that out. See [Read-only mode](#read-only-mode).
959
-
960
- Nothing secret goes in it: not the proxy bearer token, not the value of `ANTHROPIC_API_KEY`, not the system prompt, not a pending call's arguments. A `claude-code-doctor` command you defined yourself is never overwritten. The name has no space in it because opencode reads everything after the first space as the command's arguments. The whole exchange is kept out of any transcript replayed to the CLI, like a `/btw` pair.
961
-
962
- ### Filing an issue: /claude-code-doctor bundle
963
-
964
- ```text
965
- /claude-code-doctor bundle
966
- ```
967
-
968
- **When filing an issue, paste `/claude-code-doctor bundle`.** It returns the report above plus the recent `NOTICE`, `WARN` and `ERROR` lines from this process's plugin log, redacted so the whole thing is safe to put in a public issue. It starts no process and costs no tokens, so unlike `usage` it stays instant.
969
-
970
- The point is `plugin.log` itself. It is off by default, and when it is on it has no redaction guarantee at all: it holds spawn argv with `--settings` JSON and absolute paths, the bridged MCP config target, your skill directories, opencode and Claude session ids, and error prose the CLI wrote. Nobody can safely attach it to a GitHub issue, so bug reports arrive as screenshots and guesses instead.
971
-
972
- The redaction is an **allowlist**, not a filter, because a filter fails silently the first time someone logs a new field. Per line, what survives is:
973
-
974
- - the timestamp and the level,
975
- - the message text **only** when it is one of the 112 `NOTICE`/`WARN`/`ERROR` message literals extracted from the plugin's own source. A message built at runtime, including every CLI error string the plugin re-logs, becomes `[redacted message, N chars]` and only its data fields remain,
976
- - data fields whose key is on an explicit allowlist **and** whose value is then the kind that entry declares: versions, counts, booleans, enums, durations, exit codes, model and tool and server names, paths, and the loopback proxy URL with its query dropped. The allowlist applies at every nesting depth.
977
-
978
- Everything else, including every key the allowlist does not name, becomes `[redacted, N chars]`, which keeps the shape so you can see a field was there without seeing it. Session ids become a short hash salted per bundle, so two lines about one conversation still correlate in the paste and nowhere else, and your home directory becomes `~` across the whole report, the table included.
979
-
980
- Never in a bundle: prompt or reply text, system prompts or the appended prompt file, tool inputs or outputs, file contents, environment values, bearer tokens, the proxy `authToken`, API keys, `Authorization` headers, MCP server env or headers, URL credentials or query strings, or the raw spawn argv. The argv is kept as option names with every value replaced, which is what a spawn bug report actually needs.
981
-
982
- Kept on purpose, so read it before pasting: folder paths below your home directory (project and config folder names such as `~/.claude-<account>`) and your `accounts` names. A maintainer needs both to read a cwd or an account problem, and only you can tell whether a folder or account name is something you would rather not publish.
983
-
984
- It is capped at 120 lines and 24,000 bytes, newest first, and says how many lines it left out. With file logging off it says so, tells you how to turn it on, and still returns the report:
985
-
986
- ```sh
987
- OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode
988
- ```
989
-
990
- The plain `/claude-code-doctor` output is unchanged by any of this.
991
-
992
- ## Per-turn stats
993
-
994
- Off by default. With `turnStats: true`:
995
-
996
- ```text
997
- ▌ **stats:** $0.0123 · 4.2 s · 2 CLI turns · in 1.2k · out 812 · cache read 45.1k · cache write 2.0k
998
- ```
999
-
1000
- One line at the end of a finished turn, from the numbers the CLI already reports on its `result`. Notes:
1001
-
1002
- - Never on a `/compact` turn (the footer would be appended to what opencode stores as the summary) and never on a turn that ended in error, where the error is the thing to read.
1003
- - It is its own text part led by `▌ **stats:**`, and the plugin strips it again if the conversation is ever replayed into a fresh Claude Code process. The model never reads its own accounting.
1004
- - Token counts are the turn's totals, which is what matches the cost. They are deliberately not what opencode records for the message: opencode reads that as context occupancy (its context gauge and its auto-compaction), so it gets the input and cache counts of the turn's last API call, plus the whole turn's output. opencode's own cost figure for a multi-call turn therefore counts only the last call's input and cache; the CLI's real cost is this line and `providerMetadata["claude-code"].costUsd`.
1005
- - The cost is what the CLI reported for the turn, not a billing guarantee.
1006
-
1007
- The same numbers are logged at INFO whatever this option is set to, and `total_cost_usd`, `duration_ms`, `duration_api_ms`, `num_turns`, `usage`, `modelUsage` and `permission_denials` always reach `providerMetadata` (denials by tool name and id only, never their inputs).
1008
-
1009
- ## Things the CLI says that are no longer silent
1010
-
1011
- Four Claude Code stream events used to reach nothing but a debug log:
1012
-
1013
- - **A rate-limit rejection.** When the CLI reports `status: "rejected"` (or a rejected extra-usage state), the turn now carries a `▌ **rate limit:**` line naming the window, the reason extra usage is unavailable, when it resets, and the four things that can be done about it. Warned once per identity per process. See [Billing](#billing) and [which login bills what](#which-login-bills-what).
1014
- - **A context compaction Claude Code did on its own.** A `▌ **context compacted:**` note says so, with the before and after token counts, so an answer that suddenly forgets the start of the conversation has a visible cause.
1015
- - **A conversation Claude Code cleared.** Sending `/clear` as a message, or a plan-mode exit that clears context, makes Claude Code start a fresh conversation while opencode still shows the old messages. A `▌ **claude code reset:**` note says so. The plugin deliberately does not replay the earlier messages, since that would undo the clear. Start a new opencode session if you want the two to match.
1016
- - **A `result` whose subtype is not `success`** (`error_max_turns`, `error_during_execution`, …). The subtype is named in the transcript and the turn finishes as an error instead of an ordinary reply.
1017
- - **A CLI-executed tool that failed.** Its result is forwarded with the AI SDK's error flag, so opencode renders the row as failed rather than as a success whose output happens to be an error message.
1018
- - **A turn that finished cleanly without saying anything.** No text, no tool call, no error: opencode files it as an ordinary reply, so what you get is a blank message with nothing to distinguish it from a crash. A `▌ **no reply:**` note now says which of the two shapes it was, thinking-with-no-answer or nothing at all, and that nothing failed and nothing is pending. It is its own text part and is stripped from any transcript rebuilt for the CLI. Never on a compaction turn, a failed turn, an aborted one, or one that ended on a question, and suppressed entirely by `"autoContinueIncompleteTurns": false`. There is deliberately no automatic retry: measured across the whole retained plugin log, every finished turn carried between 940 and 5,080 characters of reply and none was silent.
1019
-
1020
- At session start the plugin also warns once per process for each MCP server Claude Code could not connect (its tools are simply absent otherwise) and once when the CLI's own `apiKeySource` says an API key is in effect, which is the field that tells you pay-as-you-go billing is happening. See [`ignoreAnthropicApiKey`](#options-reference).
1021
-
1022
- ## Configuration skill
40
+ 2. **Add the package to opencode's global config.** That spec is the whole install. Do not `npm install` it yourself: opencode resolves and caches plugin packages on its own.
1023
41
 
1024
- The package includes a `claude-code-plugin` skill so your agent can configure it without asking you to navigate all its options. Ask, for example:
42
+ ```json
43
+ {
44
+ "plugin": ["@khalilgharbaoui/opencode-claude-code-plugin"]
45
+ }
46
+ ```
1025
47
 
1026
- ```text
1027
- Use the claude-code-plugin skill to configure a work account and idle worker cleanup.
1028
- ```
48
+ `~/.config/opencode/opencode.json` on opencode 1.x. opencode 2 reads the same key; its native spelling is `plugins`.
1029
49
 
1030
- It covers accounts, models and agent effort, proxy tools, permissions, MCP/skill bridging, timeouts, logging, upgrades and troubleshooting. It directs the agent to preserve JSONC comments, change only requested settings, validate the result, protect credentials and ask before paid probes or broader permissions.
50
+ 3. **Quit opencode fully and relaunch.** Plugins load once, at process start. The model picker now has a **Claude Code (Default)** provider with entries such as `Claude Sonnet 5.5 (2×)` and `Claude Opus 5 (5×)`; the suffix is each model's list price relative to Haiku.
1031
51
 
1032
- The plugin registers the bundled directory with opencode's `skills.paths`, making it available to other providers too on supporting opencode versions. For Claude turns it also loads through Claude's native Skill tool as `opencode-skills:claude-code-plugin`, even when `bridgeOpencodeSkills` is `false`. This requires CLI `--plugin-dir` support and applies to the headless and interactive spawns; compaction never loads the native bridge.
52
+ Nothing in the picker? [Troubleshooting](./docs/troubleshooting/symptoms.md) is keyed on the first thing you see. `/claude-code-doctor` in any session prints what the plugin thinks is happening, without calling a model.
1033
53
 
1034
- No separate skill installation or copying is needed. It ships with each package version, so upgrading updates the reference. Fully restart opencode to load it. `test-configure-skill.ts` checks coverage of provider/logging options, model ids, proxy tools and environment variables; maintainers must update behavior and default guidance in the same change as the implementation.
54
+ ## What you get
1035
55
 
1036
- ## Skill bridge
1037
-
1038
- opencode and Claude Code use the same on-disk skill format, a `<name>/SKILL.md` whose frontmatter carries `name` and `description`, but they read from overlapping, not identical, directories. opencode looks in `.opencode/skills/`, `~/.config/opencode/skills/`, `~/.agents/skills/` and more; the Claude CLI looks in `~/.claude/skills/`, the project's `.claude/skills/` and its own plugins. Where they differ, opencode advertises a skill in the system prompt it forwards, the model calls `Skill("browser-automation")`, and Claude answers `Unknown skill`. Where they overlap, the same skill reaches one session twice.
1039
-
1040
- By default the plugin discovers your opencode skills, stages a throwaway Claude Code plugin directory that links them, and passes it as `claude --plugin-dir`. They register natively, prefixed with the plugin name:
1041
-
1042
- ```text
1043
- opencode-skills:browser-automation
1044
- opencode-skills:rtk
1045
- ```
1046
-
1047
- Claude can invoke them with the Skill tool or as `/opencode-skills:<name>`. `--plugin-dir` is scoped to the spawned session, so nothing is written into your `~/.claude`.
1048
-
1049
- Discovery covers every root opencode itself reads, first match wins:
1050
-
1051
- 1. Walking up from the working directory: `.opencode/skills/`, `.claude/skills/`, `.agents/skills/` at each level.
1052
- 2. `~/.opencode/skills/`.
1053
- 3. `$OPENCODE_CONFIG_DIR/skills/` and `.../skill/`.
1054
- 4. `~/.config/opencode/skills/` and `.../skill/` (or `$XDG_CONFIG_HOME`).
1055
- 5. `~/.claude/skills/` and `~/.agents/skills/`.
1056
-
1057
- A project skill shadows a global one of the same name, and an opencode-managed copy shadows an external one. Step 5 is opencode's own "external" scan and honours its `OPENCODE_DISABLE_EXTERNAL_SKILLS` and `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` variables. A skill is known by the `name:` its `SKILL.md` frontmatter declares, falling back to the directory name, which is what opencode advertises. If the skill set is unchanged the staged directory is reused between spawns.
1058
-
1059
- ### Skills Claude already has
1060
-
1061
- Those roots overlap Claude Code's own, which reads `$CLAUDE_CONFIG_DIR/skills/` (`~/.claude/skills/` by default), the project's `.claude/skills/`, and the `skills/` folder of every installed plugin. Without care one skill reaches a single session twice, costing prompt tokens on every turn and leaving it ambiguous which copy answers.
1062
-
1063
- So `bridgeSkipNativeSkills` (**on by default**) leaves a skill unbridged when Claude already loads it. A skill counts as already loaded when:
1064
-
1065
- - it is literally the same directory, symlinks resolved;
1066
- - its `SKILL.md` is byte-identical to a native one, wherever that one lives (this is the case for a skill installed as a Claude plugin *and* symlinked into `~/.agents/skills`);
1067
- - a **different** skill of the same name is registered under user or project scope. Plugin skills are namespaced `<plugin>:<name>` and so never take a bridged name, only duplicate its content.
1068
-
1069
- Only that last case changes which copy answers `Skill("<name>")`, so it is logged at WARN naming both paths; the others are logged at INFO. Set `bridgeSkipNativeSkills: false` to bridge everything regardless and get the duplicates back.
1070
-
1071
- One limitation worth knowing: the plugin scan reads `installed_plugins.json` and does not check whether that plugin is actually enabled, so a skill from a disabled plugin can be treated as native. If a skill disappears, grep `plugin.log` for `skills claude code already loads`: one line names both paths and the reason.
1072
-
1073
- ### Enabling it
1074
-
1075
- The bridge itself is **off by default**: every bridged skill's name and description is also in the system prompt opencode already forwards, so a large skill set is paid for twice on every turn. Set `bridgeOpencodeSkills: true` when the model tries `Skill("<name>")` for a skill opencode advertises and gets `Unknown skill`; the bundled configuration skill is staged either way. When on, the bridge applies to the headless and interactive spawns alike, never to compaction, and it is skipped on a Claude CLI without `--plugin-dir` (the plugin probes `claude --help` and logs a notice).
1076
-
1077
- This bridge was written by [@broskees](https://github.com/broskees) (Joseph Roberts) on his fork and absorbed here with credit; see [Credits](#credits).
1078
-
1079
- ## WebSearch routing
1080
-
1081
- Claude Code ships a built-in `WebSearch` tool. The `webSearch` option controls who actually executes those calls:
1082
-
1083
- | `webSearch` value | Behavior | When to use |
1084
- |---|---|---|
1085
- | `"claude"` (default) | Claude CLI runs WebSearch internally via Anthropic. Zero setup, no extra cost, no API key. The query is shown in the transcript as a `> Web search:` line (opencode has no `WebSearch` tool registry entry, so a raw tool row would render as `⚙ invalid`). | Most users. |
1086
- | `"<opencode-tool-name>"` (e.g. `"websearch_web_search_exa"`) | Forward to that opencode-side tool with `executed:false`. Requires the corresponding MCP server to be configured in opencode (e.g. [exa-mcp-server](https://github.com/exa-labs/exa-mcp-server)). | You want a specific search backend (Exa, Tavily, Brave) and have the MCP wired up in opencode. |
1087
- | `"disabled"` | `WebSearch` is added to `--disallowedTools` so the model can't call it. | Compliance/security scenarios where outbound search isn't allowed. |
1088
-
1089
- ```json
1090
- "options": { "webSearch": "websearch_web_search_exa" }
1091
- ```
1092
-
1093
- **Trade-offs**
1094
-
1095
- - Claude-side execution: free with your Claude usage, no API key, but no opencode visibility into queries/results, no caching/rate-limit hooks.
1096
- - opencode-side execution: choose any backend, queries flow through opencode's audit/policy/cache, but costs money (search APIs are paid) and adds a network hop.
1097
- - Some Claude-specific tool features stay on the built-in side (notably `MultiEdit` — see the note above).
1098
-
1099
- ---
1100
-
1101
- ## MCP bridge
1102
-
1103
- If `bridgeOpencodeMcp` is true (the default), the plugin reads your opencode config's MCP servers, translates them into Claude's MCP schema, writes a private temp file, and passes it to `claude --mcp-config`. It accepts V1 `mcp.<server>` and V2 `mcp.servers.<server>`; V2's `servers` container and timeout defaults are not servers. `disabled: true` is supported alongside legacy `enabled: false`. Live runtime status takes precedence when available.
1104
-
1105
- ### Discovery and precedence
1106
-
1107
- The disk bridge reads global config, then `OPENCODE_CONFIG`, then project direct files and `.opencode` files. V1 keeps its existing repo-boundary discovery, `.opencode` ordering and per-server deep merges.
1108
-
1109
- On V2, project discovery walks to the filesystem root (including ancestors above the repo). Direct files are applied parent-first, then `.opencode` files parent-first: the closest file wins within each group, and all `.opencode` files override direct files. Each higher-precedence server entry **replaces the entire server object**, so repeat its type, URL/command and other required fields in an override. Both `.json` and `.jsonc` are read, with `.jsonc` winning within a directory. Runtime toggles continue to participate in the hot-reload hash.
1110
-
1111
- ### Servers that connect late
1112
-
1113
- A server opencode has not finished connecting to when a turn is planned used to be dropped from that turn's `claude` spawn, and a reused process keeps the `--mcp-config` it was spawned with, so it stayed invisible for the rest of the conversation. Two things stop that now.
1114
-
1115
- First, "still connecting" is no longer read as "not connected". opencode 2 reports such a server as `pending`, which is not a decision, so the bridge leaves its configured state alone instead of forcing it off, and the turn waits up to `mcpConnectWaitMs` (3 s by default, `0` to disable) for the host to decide. opencode 1 has no `pending` status: its own status call blocks until every server resolves, so nothing changes there and the wait costs one status call exactly as before.
1116
-
1117
- Second, if the server joins later anyway, `hotReloadMcp` moves the conversation onto a `claude` process with the new config and `--resume`, at the start of a later turn. That only happens at a safe boundary: not during `/compact`, not on the interactive transport, and not while a proxied call is still in the air, a turn is still running or a plan-mode approval is outstanding. The log line names the servers:
1118
-
1119
- ```
1120
- INFO: opencode MCP servers changed, respawning claude {"joined":["slowmcp"],"left":[],...}
1121
- ```
1122
-
1123
- A server that flaps between connected and failed is capped at one respawn per minute per conversation; override with `CLAUDE_CODE_MCP_HOT_RELOAD_COOLDOWN_MS`.
1124
-
1125
- This matters most where a turn can arrive before the host has started its servers: `opencode run`, scripted use and slow servers. The TUI normally connects everything before the first prompt.
1126
-
1127
- ### V2 Code Mode
1128
-
1129
- V2 normally exposes MCP tools through Code Mode's `execute` and its catalog, rather than as individual server-prefixed model tools. `proxyOpencodeMcpTools` matches individual tools only; on a Code Mode-only snapshot it warns and falls back to the direct Claude MCP bridge. This fallback does **not** execute tools under opencode's permission policy.
1130
-
1131
- To explicitly opt into Code Mode through opencode instead, use these provider settings (headless transport):
1132
-
1133
- ```json
1134
- {
1135
- "providers": {
1136
- "claude-code": {
1137
- "settings": {
1138
- "proxyOpencodeTools": ["execute"],
1139
- "bridgeOpencodeMcp": false,
1140
- "strictMcpConfig": true
1141
- }
1142
- }
1143
- }
1144
- }
1145
- ```
1146
-
1147
- Preserve other entries in `proxyOpencodeTools`. `execute` is a code runner that can call **all tools in the session's Code Mode catalog**, not just MCP; opting in must be deliberate. It runs in opencode with the calling agent's permissions, and the plugin refuses it under `permissionPreset: "read-only"`. The plugin preserves the model-visible schema and tells Claude to call `mcp__opencode_proxy__execute` (discoverable via `ToolSearch`), using the original `search(...)` and `tools[...]` catalog signatures inside its code argument.
1148
-
1149
- `bridgeOpencodeMcp: false` prevents a second direct MCP connection, while `strictMcpConfig: true` excludes Claude's own MCP sources. Do not add the same servers through explicit `mcpConfig` if you want Code Mode-only routing. OpenCode still owns the MCP connections; disabling the disk bridge does not disable its catalog.
1150
-
1151
- For individual MCP proxies instead, set `codemode: false` on the relevant V2 MCP servers, then use `proxyOpencodeMcpTools: true` with `strictMcpConfig: true`. Neither approach guarantees exactly-once side effects across retries. Fully restart all opencode server/GUI processes after changing provider settings or plugin code; a new chat alone is insufficient. Real Claude smoke tests consume usage and run configured hooks, so request approval first.
1152
-
1153
- ### Translation
1154
-
1155
- | opencode `type` | Claude `type` |
56
+ | | |
1156
57
  |---|---|
1157
- | `local` | `stdio` |
1158
- | `remote` | `http` |
1159
-
1160
- If you want to manage MCP servers only via `~/.claude/settings.json`, set `bridgeOpencodeMcp: false`.
1161
-
1162
- To replace (rather than augment) bridged MCP with your own:
1163
-
1164
- ```json
1165
- "options": {
1166
- "bridgeOpencodeMcp": false,
1167
- "mcpConfig": "/path/to/your/mcp.json",
1168
- "strictMcpConfig": true
1169
- }
1170
- ```
1171
-
1172
- ---
1173
-
1174
- ## Sessions
1175
-
1176
- Each chat keeps a long-lived `claude` subprocess so the model retains its native context across turns.
1177
-
1178
- - **Session key**: `(cwd, model, tool-scope, opencode-session-id)`. The opencode session id comes from the `x-session-affinity` header opencode sets on third-party provider calls. Two chats in the same project on the same model run in **separate** CLI processes — they don't race. In account mode, model IDs are suffixed per account, so account sessions do not collide.
1179
- - **Same chat, multiple turns** → process reused, full Claude context retained.
1180
- - **New chat** → fresh process under the new session key.
1181
- - **Resumed chat after restart** → in-memory state is gone; a new process spawns and the conversation history is summarized and prepended.
1182
- - **Abort (Esc / Ctrl+C)** → the plugin sends the Claude CLI a stream-json `interrupt` control request, so the CLI actually stops generating and running tools instead of finishing the abandoned turn on your bill. The process stays alive for the next message in that chat, and any proxied call the aborted turn left behind is released when that message arrives (see [How a proxied call ends](#how-a-proxied-call-ends)). If a turn is somehow still running when the next one starts, it is interrupted first (5 s cap). Contributed by [@broskees](https://github.com/broskees).
1183
- - **Abort during the first moment of a turn** → a turn spends a little time preparing before it asks the CLI for anything: resolving the spawn directory, probing the `claude` version, reading opencode's MCP status and tool registry. A stop pressed in that window used to be dropped, and the turn spawned, ran and billed anyway. It now ends the turn there: nothing is spawned, nothing is written, no running process is interrupted, and the reply is simply empty.
1184
- - **Idle timeout** → when `idleProcessTimeoutMs` is set, a completed headless turn arms an eviction timer (unset or `0` keeps workers until LRU eviction). Reuse cancels it, a worker found mid-turn when it fires is left alone and re-timed, and eviction preserves the session id, so the next message resumes the same conversation with `--resume`. An idle `claude --print` holds around 250 MB, which is the reason to set it if you keep many chats open.
1185
- - **Cap**: 16 active processes, LRU eviction. A process that is mid-turn is never the victim: eviction takes the oldest **idle** one, and when every process is busy it evicts nothing and warns instead, so a running answer is never truncated to make room.
1186
- - **Deleted chat** → deleting a session in opencode kills its `claude` workers at once and forgets their session ids and per-chat state; there is nothing left to resume. Other chats, and the shared fallback bucket used when no session id is known, are untouched.
1187
- - **opencode exits** → every retained worker is killed on the way out, so a hard shutdown does not leave `claude` processes reparented to init.
1188
- - **Crash** → if the CLI dies mid-turn (no terminal `result` line), the turn ends with a visible error naming the exit code or signal and the last stderr the CLI wrote, not a silent `stop` that reads as a short but finished answer. An abort you asked for is not reported this way.
1189
-
1190
- ---
1191
-
1192
- ## Read-only mode
1193
-
1194
- ```json
1195
- "options": {
1196
- "permissionPreset": "read-only"
1197
- }
1198
- ```
1199
-
1200
- That is the whole configuration. The turn can read your code and search the
1201
- web, and it cannot write a file, run a command, or execute code.
1202
-
1203
- Presets exist because read-only was previously a combination you had to get
1204
- exactly right. `permissionMode: "plan"` alone does not do it, and neither does
1205
- any single Claude Code flag, because this plugin puts a second execution path
1206
- next to the CLI's own tools: `proxyTools` defaults to `Bash`, `Edit`, `Write`,
1207
- `WebFetch` and `Task`, and each of those is an MCP tool the CLI calls and
1208
- **opencode** executes. No CLI flag reaches them. So the preset works at three
1209
- layers:
1210
-
1211
- | Layer | What read-only does | Why it is needed |
1212
- | --- | --- | --- |
1213
- | Claude CLI tools | `--restricted` (CLI 2.1.258+) | Removes Bash, the REPL and the other code-running built-ins, removes WebFetch, confines the file tools to the working directories, and refuses bypass |
1214
- | Claude CLI tools, older CLIs | `--disallowedTools Bash Write Edit NotebookEdit REPL JavaScript WebFetch` | `--restricted` is version-gated; these names are not |
1215
- | The opencode proxy | `bash`, `write`, `edit`, `webfetch`, `task` and `task_batch` are dropped from `proxyTools` | These run in opencode, so the CLI flags above never see them |
1216
- | Everything else | `--permission-prompts none` (CLI 2.1.263+) and `controlRequestBehavior: "deny"` | A bridged MCP tool or a read outside the working directory is neither of the above |
1217
-
1218
- `skipPermissions` is forced to `false`, and that is not a style choice:
1219
- `--restricted --dangerously-skip-permissions` is a startup error on CLI 2.1.280
1220
- (`Error: bypassPermissions not supported in restricted mode`), so a spawn
1221
- carrying both would not run at all.
1222
-
1223
- **The preset replaces rather than merges.** Setting `skipPermissions`,
1224
- `permissionMode`, `controlRequestBehavior` or `controlRequestToolBehaviors`
1225
- next to it has no effect; each dropped value is logged at NOTICE at startup so
1226
- you can see it happen. `proxyTools` is the exception: it is filtered, so a list
1227
- naming `Question` keeps it. An unrecognised preset name applies **nothing** and
1228
- logs a WARN, rather than guessing at what you meant.
58
+ | **Selective tool proxy** | Choose, per tool, whether Claude Code or opencode executes it. Proxied calls end on an event (the result, an abort, the next message, the child exiting), never on a clock. [Guide](./docs/guides/tool-proxy.md) |
59
+ | **Claude's own tools, MCP and skills** | `Read`, `Grep`, `Glob`, `WebSearch` run in Claude Code; your opencode MCP servers are bridged in; your opencode skills can be staged for Claude's `Skill` tool. [MCP](./docs/configuration/mcp.md) · [Skills](./docs/configuration/skills.md) |
60
+ | **Several accounts, with failover** | `"accounts": ["personal", "work"]` becomes one provider per account. Out of usage mid-task? opencode's question form offers the others, and nothing moves until you pick. [Accounts](./docs/configuration/accounts.md) |
61
+ | **Subagents: your account, their model** | `forceModel`, `reasoningEffort` and `cacheTtl` in an agent file, inheriting the caller's account. `task_batch` runs several subagents at once. [Subagents](./docs/configuration/subagents.md) |
62
+ | **18 models, reasoning variants, fast mode** | Haiku 4.5 through Opus 5.5, Fable and Mythos, each with a `(N×)` list-price suffix, `low` to `max` effort variants, and a fallback chain for a model this account cannot run today. [Models](./docs/models.md) |
63
+ | **`/btw` and `/claude-code-doctor`** | Side questions on the live process, and a health report whose `bundle` form is redacted by allowlist so it is safe to paste into a public issue. [`/btw`](./docs/guides/btw.md) · [Doctor](./docs/guides/doctor.md) |
64
+ | **Read-only preset, plan mode** | `"permissionPreset": "read-only"` holds at the CLI, the proxy and the permission layer at once. [Permissions](./docs/configuration/permissions.md) |
1229
65
 
1230
- **What stops working.** Reads are fine (`Read`, `Grep`, `Glob`, `WebSearch`),
1231
- but anything that would raise a permission prompt is denied, and that includes
1232
- bridged MCP tools and the `question` proxy. Claude's own `AskUserQuestion`
1233
- still renders its stop-and-wait markdown, so the model can still ask you
1234
- things. If you need one specific tool allowed, do not use the preset: set the
1235
- underlying options yourself.
1236
-
1237
- **On an older CLI** the preset still holds through `--disallowedTools` plus the
1238
- plugin's own denial of every permission request, and it warns naming what is
1239
- missing. Below 2.1.258 you lose the cwd confinement on reads; below 2.1.263 the
1240
- denial happens in the plugin rather than in the CLI, one layer instead of two.
1241
-
1242
- Measured end to end on CLI 2.1.280 and `claude-haiku-4-5`: a turn under the
1243
- preset asked to write a file and run a command did neither, the file was never
1244
- created, and the CLI's own `permission_denials` recorded the single blocked
1245
- `Write` with no Bash attempt at all, because there was no Bash tool to attempt
1246
- with.
1247
-
1248
- ---
1249
-
1250
- ## Plan mode
1251
-
1252
- Set `permissionMode: "plan"` to forward `--permission-mode plan` to Claude. The plugin handles `ExitPlanMode` specially — instead of forwarding it as a tool call, it converts it to a confirmation prompt that flows through opencode normally.
1253
-
1254
- > **Plan mode never permits edits, and you do not have to configure anything for that.** The CLI lets `--dangerously-skip-permissions` override `--permission-mode plan` outright, and `skipPermissions` defaults to `true`, so until this was fixed anyone asking for plan mode silently got full write access (measured on CLI 2.1.258: the run wrote a file on request without a prompt). The plugin now drops the skip flag whenever `permissionMode` is `"plan"`; every other mode governs prompting, which is what that flag is for, so those still pass it.
1255
- >
1256
- > Two things to know. Nothing releases plan mode mid-session: headless Claude Code is not offered an `ExitPlanMode` tool, so approving a plan in chat does not unlock writes, and leaving plan mode means changing the config and restarting opencode. The plugin warns about this once at startup. And the CLI still writes its own plan document under `~/.claude*/plans/`, which is its own feature and outside your workspace; your files and commands are untouched.
1257
-
1258
- By default that prompt is text: the plan is rendered as markdown, followed by `**Do you want to proceed with this plan?** (yes/no)`, and you answer in your next message.
1259
-
1260
- ### Approval as a real form (`planModeQuestion`, opt-in)
1261
-
1262
- Set `planModeQuestion: true` to route the approval through opencode's native `question` tool instead:
1263
-
1264
- ```json
1265
- "options": {
1266
- "permissionMode": "plan",
1267
- "planModeQuestion": true
1268
- }
1269
- ```
1270
-
1271
- The plan is still rendered, but the turn then ends on `tool-calls` and opencode runs its own `question` tool, so approval is a form rather than prose. Your answer is fed back to the CLI as the `tool_result` for the original `ExitPlanMode` call, which is what actually unlocks plan mode on the Claude side. A "yes" typed as ordinary text never does that. Anything other than picking `yes` (including custom text) comes back as rejection feedback the model is told to act on.
1272
-
1273
- > **This cannot currently fire on the default headless transport, so leaving it off costs you nothing.** The form it delivers through works (see [AskUserQuestion](#askuserquestion)), but headless `--print` does not offer the model an `ExitPlanMode` tool at all on CLI 2.1.258, and the bridge keys on that tool call. Measured three ways: asked directly for its tool list in plan mode, the CLI returned `Agent, Bash, Edit, ListAgents, Read, ReportFindings, ScheduleWakeup, Skill, ToolSearch, Workflow, Write` and nothing else; asked to do work it said "I'm unable to exit plan mode from within the tool set available to me"; and a full probe through this plugin with `planModeQuestion: true` produced no `ExitPlanMode` anywhere in `plugin.log` while the model asked for approval in prose. The name is still known to the CLI (`--disallowedTools ExitPlanMode` validates silently, where a bogus name warns), so this reads as headless dormancy rather than removal, the same shape as the [`AskUserQuestion` fallback](#askuserquestion). The text path below is what you actually get, and it works. Re-run those probes on a newer CLI before assuming the bridge is reachable. On opencode builds with no `question` registry entry the plugin silently keeps the text path (look for `plan-mode question gate` in the log).
1274
-
1275
- Approval bridge contributed by [@CollieIsCute](https://github.com/CollieIsCute).
1276
-
1277
- ---
1278
-
1279
- ## AskUserQuestion
1280
-
1281
- opencode ships a built-in `question` tool (`packages/opencode/src/tool/question.ts`) that renders a real TUI form with options and a custom-answer field — near-identical to Claude Code's `AskUserQuestion` (`multiSelect` → `multiple`). The plugin can route `AskUserQuestion` through it so the prompt becomes an actual form instead of plain text. Two modes:
1282
-
1283
- ### With `"Question"` in `proxyTools` (opt-in)
1284
-
1285
- > **Correction, September 6, 2026: this is no longer blocked, and earlier releases of this README were wrong about why.** The missing form was attributed to an upstream TUI regression. The real cause was local: a notification plugin awaited macOS `alerter` dismissal inside `tool.execute.before`, so the question tool never started. Native providers load that same global plugin, which is why their identical failure did not isolate the TUI. With the hook made non-blocking, the form renders, and the full path through this plugin is verified: on plugin 0.18.0 / Claude Code 2.1.258 / opencode 1.18.29, Claude called `mcp__opencode_proxy__question`, the request appeared in `GET /question`, the reply completed the tool, and Claude's answer contained a token it could only have read from the tool result. Confirmed in a real terminal too: with `"Question"` enabled and opencode relaunched, the proxied call rendered as a TUI form and the clicked answers came back into the turn.
1286
- >
1287
- > `"Question"` is still opt-in, because turning it on disables Claude's own `AskUserQuestion` (see the fallback below) and that trade should be deliberate. If your form does not render, see [a question form never renders](#a-question-form-never-renders-and-the-turn-hangs) before assuming an upstream bug.
1288
-
1289
- Add `"Question"` to `proxyTools`. Claude's built-in `AskUserQuestion` is disabled via `--disallowedTools`, and the plugin exposes `mcp__opencode_proxy__question` in its place. A primary agent needs no permission entry (verified on opencode 1.18.29 with no `permission` block at all); if a subagent's form is refused, grant it `permission.question: "allow"` on that agent, the same way [subagent todos](#subagent-todos) need `todowrite`. The model calls the proxy, opencode renders the form, and the operator's answers come back as arrays of selected labels. On builds that lack the `question` registry entry the def is silently dropped at spawn (version gate), and the deny/markdown fallback below applies instead.
1290
-
1291
- `proxyTools` replaces the default list rather than adding to it, so repeat the defaults you still want:
1292
-
1293
- ```json
1294
- "options": {
1295
- "proxyTools": ["Bash", "Edit", "Write", "WebFetch", "Task", "Question"]
1296
- }
1297
- ```
1298
-
1299
- To turn it back off, drop `"Question"` from the list. It is **not** in the default list, so no configuration means the deny/markdown fallback below stays in force.
1300
-
1301
- The same spawn-time caveat as `"Task"` applies: provider options are read once at opencode startup, so restart opencode fully after adding it. Question calls get a 30-minute proxy deadline (raise it with `proxyToolTimeoutMs` if you expect to be AFK longer; an expired call comes back as an error, not an answer).
1302
-
1303
- ### Without the proxy (default fallback)
1304
-
1305
- When `"Question"` is not in `proxyTools` (or the opencode version lacks the `question` tool), the plugin handles `AskUserQuestion` as follows:
1306
-
1307
- 1. **It renders the full question.** The tool's payload — every question, header, option label, and option description — is emitted as readable markdown into the assistant stream so the user actually sees the choices (same approach as `ExitPlanMode`).
1308
- 2. **It is never auto-allowed at the CLI gate.** Allowing it would let the headless Claude CLI resolve its own question (no TTY → fabricated/empty answer) and proceed on a guess. `controlRequestBehaviorForTool` hard-denies `AskUserQuestion` and returns a message telling the model to **stop and wait for the operator's answer** — end the turn, call no further tools, and never self-answer. (Before v0.7.0 this message also offered an "if the run is non-interactive, proceed with a reasonable guess" fallback. The model could not reliably tell interactive opencode from a headless run and routinely took it, so questions appeared to be skipped — [issue #8](https://github.com/khalilgharbaoui/opencode-claude-code-plugin/issues/8). For genuinely unattended runs, use the `controlRequestToolBehaviors` override below instead.)
1309
-
1310
- This hard-deny sits **below** `controlRequestToolBehaviors` in precedence but **above** the global `controlRequestBehavior`. So:
1311
-
1312
- - The global `controlRequestBehavior: "allow"` does **not** override it (interactive setups stay correct by default).
1313
- - An explicit per-tool entry **does**. For a fully unattended/automated deployment that prefers "guess and continue" over "stop and wait", restore the old auto-allow:
1314
-
1315
- ```json
1316
- "provider": {
1317
- "claude-code": {
1318
- "options": {
1319
- "controlRequestToolBehaviors": { "AskUserQuestion": "allow" }
1320
- }
1321
- }
1322
- }
1323
- ```
1324
-
1325
- With `"allow"`, the Claude CLI answers its own `AskUserQuestion` internally and the run never blocks — appropriate only when no operator is watching and forward progress matters more than a correct decision.
1326
-
1327
- ---
1328
-
1329
- ## Compaction
1330
-
1331
- When you run `/compact` in opencode, the plugin handles it on a short-lived dedicated Claude CLI spawn instead of routing it through your main conversation process. Three reasons:
1332
-
1333
- 1. **Cost.** The summarizer reads your entire transcript every time. Routing through a smaller model keeps `/compact` from burning your Opus budget.
1334
- 2. **Latency.** Claude Haiku 4.5 hits ~150 tok/s with a hard 8k output cap, so compaction completes predictably (~30s for a long transcript).
1335
- 3. **Cleanliness.** The compaction spawn skips MCP servers, the tool proxy, and the multi-step continuation hint. It's a one-shot text-out call; the rest is overhead.
1336
-
1337
- The transcript itself is serialized rich: tool inputs and tool results are both included (each clipped at 10k chars), with oldest entries dropped first when the aggregate exceeds 180k chars. The summarizer sees actual tool activity rather than placeholders.
1338
-
1339
- ### Picking a different compaction model
1340
-
1341
- | Source | How | Wins over |
1342
- |---|---|---|
1343
- | Env var (per-process) | `CLAUDE_CODE_COMPACTION_MODEL=claude-sonnet-4-6 opencode` | config, default |
1344
- | `opencode.json` (per-project) | `"compactionModel": "claude-sonnet-4-6"` under `provider.claude-code.options` | default |
1345
- | Default | `claude-haiku-4-5` | – |
1346
-
1347
- Anything Claude Code's `--model` accepts works as a value.
1348
-
1349
- ---
1350
-
1351
- ## Extended thinking
1352
-
1353
- The plugin forwards Claude's thinking blocks (`thinking_delta` stream events) to opencode as reasoning parts, so the "Thinking" row in the chat panel shows whenever the model uses extended thinking. This works across every Claude 4 family model the CLI supports.
1354
-
1355
- What you see is a **summary** of the model's thinking, not the raw chain-of-thought. Anthropic [stopped exposing raw thinking on the Claude 4 family](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#summarized-thinking) and ships a server-generated digest instead. For Claude Opus 4.7 specifically, [thinking content is omitted from responses by default](https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7#thinking-content-omitted-by-default); the plugin opts back in by passing `--thinking-display summarized` on every spawn. Claude Code CLI 2.1.142+ is required for that flag to take effect; older CLIs skip it silently.
1356
-
1357
- ### Reasoning effort
1358
-
1359
- Each model exposes five picker variants, `low` / `medium` / `high` / `xhigh` / `max`. An agent's own `reasoningEffort` frontmatter accepts six values: those five plus `minimal`, which maps to the CLI's `low`. The plugin hands the level to the CLI as `CLAUDE_CODE_EFFORT_LEVEL` at spawn, which Claude Code treats as the session-wide override: it beats the `effortLevel` in that account's `settings.json` and a shell export of the same variable. Effort is fixed for the life of a `claude` process, so it is part of the session key. Changing effort retires the previous effort's process and remembered transcript ID before replaying the conversation into a fresh process. Switching back cannot resume stale context; same-effort streaming turns still reuse their process. This reset is scoped to the same directory, model, provider/account, agent, and conversation. If the previous effort still has pending work (including tool results, plan approval, recovery, or `/btw`), the switch is rejected: finish that work at its original effort first. Title, compaction, and `/btw` calls do not trigger effort resets.
1360
-
1361
- Earlier versions injected a thinking keyword such as `(ultrathink)` into the user message instead. Claude Code stopped recognising every keyword except `ultrathink`, so that path is gone and nothing is appended to your messages any more. Compaction skips request and agent effort overrides, but still inherits a shell-level `CLAUDE_CODE_EFFORT_LEVEL` when set.
1362
-
1363
- ### Env-var overrides
1364
-
1365
- The plugin respects the standard Claude Code thinking env vars. If you set them in your shell, they pass through to the spawned process untouched, with the one exception in the first row.
1366
-
1367
- | Env var | Effect |
1368
- |---|---|
1369
- | `CLAUDE_CODE_EFFORT_LEVEL=<level>` | Session effort override. Passes through when no effort was requested; a variant or an agent's `reasoningEffort` replaces it for that spawn. |
1370
- | `CLAUDE_CODE_PROMPT_CACHE_TTL=5m\|1h` | Prompt cache TTL for the whole machine. Passes through when no agent asked; an agent's `cacheTtl` or `defaultSubagentCacheTtl` replaces it for that spawn. |
1371
- | `CLAUDE_CODE_DISABLE_THINKING=1` | Disable thinking entirely. |
1372
- | `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` | Disable adaptive thinking only. |
1373
- | `CLAUDE_CODE_SHOW_THINKING_SUMMARIES=0` | Suppress summaries (the plugin sets this to `1` by default when unset). |
1374
-
1375
- ---
1376
-
1377
- ## Quirks worth knowing
1378
-
1379
- - **Empty text blocks are dropped.** Claude sometimes opens a `content_block_start` for text but never sends a delta. The plugin no longer emits the empty block (which was triggering Anthropic 400s like `cache_control cannot be set for empty text blocks`).
1380
- - **Smart incomplete-turn continuation.** By default, the plugin keeps the current opencode stream open and feeds Claude CLI a small internal continuation message when Claude emits a `result` after reasoning/tool activity without a useful visible answer. It still stops normally on final-looking answers, questions, blockers, errors, aborts, or internal safety-budget exhaustion. It also resumes an answer the model was cut off mid-sentence: a `max_tokens` stop means truncation rather than completion, so the turn continues instead of ending on half a sentence, capped at 8 attempts and 10 minutes. Every other stop reason is taken at face value. Disable with `"autoContinueIncompleteTurns": false`.
1381
- - **`AskUserQuestion`** from the CLI is converted into plain text content rather than forwarded as a tool call — unless `"Question"` is in `proxyTools`, in which case it is routed through opencode's native `question` tool (see [AskUserQuestion](#askuserquestion)).
1382
- - **Wire-inactivity watchdog.** Once the CLI has produced any content, the stream closes gracefully if stdout goes silent for 60 seconds without a `result` message arriving. Resets on every line received, so long mid-turn pauses (Sonnet between text-end and the next tool_use, for example) are tolerated. On a user-initiated abort, the watchdog shortens to 5 seconds.
1383
- - **Context usage, not turn totals.** The CLI's `result` adds up every API call in a turn, and opencode reads a message's usage as how full the context is, so a tool-heavy turn looked several times its real size and triggered auto-compaction far below the window. The plugin reports the last API call's input and cache counts plus the turn's output instead. See [Per-turn stats](#per-turn-stats) for what that does to opencode's cost figure.
1384
- - **Lazy `cwd`.** The working directory is re-resolved at every request, so opencode's project-aware behavior works without restarting the plugin.
1385
- - **Variants survive merge.** opencode recalculates variant lists after the plugin loads; the plugin re-injects defaults into runtime config so your variants don't disappear.
1386
-
1387
- ## Logging
1388
-
1389
- Configure via `opencode.jsonc` (launch-method-independent) or env vars
1390
- (temporary override for a single process). The plugin has four orthogonal
1391
- knobs:
66
+ ## How this compares
1392
67
 
1393
- | Field | Values | Default | Effect |
68
+ | | opencode's native `anthropic` provider | This plugin | Proxy and token-reuse plugins |
1394
69
  |---|---|---|---|
1395
- | `file` | `true \| false` | `false` | Persist log entries to disk |
1396
- | `dir` | path string | `~/.local/share/opencode-claude-code/` | Custom file location |
1397
- | `mode` | `"silent" \| "debug"` | `"silent"` | TUI policy |
1398
- | `level` | `"debug" \| "info" \| "notice" \| "warn" \| "error"` | `"info"` | Minimum level to emit |
1399
-
1400
- Rails-style threshold: anything below `level` is dropped before either
1401
- destination decides what to do. `mode: "silent"` routes DEBUG/INFO/NOTICE
1402
- to file only and lets WARN/ERROR bubble in the TUI (they always do).
1403
- `mode: "debug"` additionally echoes every emitted level to the TUI (which
1404
- opencode surfaces as warning bubbles).
1405
-
1406
- `logging` is an ordinary provider option, so it goes under `provider.claude-code.options` like every other one. Keying it on the package name instead is the common mistake: opencode accepts that config without complaint and the plugin never reads it, so you get no log and no error.
1407
-
1408
- **Recommended dev setup** — capture audit trail to disk, keep TUI quiet:
1409
-
1410
- ```jsonc
1411
- {
1412
- "provider": {
1413
- "claude-code": {
1414
- "options": {
1415
- "logging": { "file": true }
1416
- }
1417
- }
1418
- }
1419
- }
1420
- ```
70
+ | **Authentication** | A Platform API key in opencode's auth store. | Whatever the official `claude` CLI holds. No token is read, stored or replayed. | The Claude OAuth session, used outside the official client. |
71
+ | **Terms-of-service status** | The ordinary API route. | Sanctioned: the official client does the authenticating. | Disallowed. Anthropic disallowed reusing subscription authentication for third-party Claude use in February 2026, and each of those projects carries its own disclaimer. |
72
+ | **Who runs Bash, Edit, Write** | opencode. | opencode, by default; `Read`, `Grep`, `Glob` run in Claude Code. | opencode. |
73
+ | **What it costs you** | Baseline. | A `claude` child per conversation under a cap of 16; Claude Code may compact its own context behind opencode's back. | One more moving part, plus the account risk above. |
1421
74
 
1422
- The snippets below abbreviate to the `logging` value alone; each one belongs at that same path.
75
+ The [full comparison](./docs/comparison.md) has nine rows, names the projects in the third column, and quotes their own READMEs.
1423
76
 
1424
- **Full firehose for deep debugging** (every DEBUG stream event captured):
77
+ ## Built from measurements
1425
78
 
1426
- ```jsonc
1427
- "logging": { "file": true, "level": "debug" }
1428
- ```
79
+ A dozen test files drive a fake `claude` through real turns: the stream parser, the proxy broker, both watchdogs, abort, respawn, account failover and the model fallback chain are exercised end to end. Every rule in [`AGENTS.md`](./AGENTS.md) names the probe, the version and the number that produced it, and the evidence lives in [`docs/agents-history.md`](./docs/agents-history.md). Two runtime dependencies. The [site](https://khalilgharbaoui.github.io/opencode-claude-code-plugin/) reports the live numbers, rebuilt daily.
1429
80
 
1430
- **Live TUI noise** (everything echoes to opencode's stderr → warning bubbles):
81
+ ## Documentation
1431
82
 
1432
- ```jsonc
1433
- "logging": { "file": true, "mode": "debug" }
1434
- ```
83
+ - **Start here:** [Introduction](./docs/introduction.md) · [Getting started](./docs/getting-started.md) · [opencode 2](./docs/opencode-2.md) · [Models](./docs/models.md) · [Billing](./docs/billing.md)
84
+ - **Configuration:** [Options](./docs/configuration/options.md) · [Environment variables](./docs/configuration/environment.md) · [Accounts](./docs/configuration/accounts.md) · [Subagents](./docs/configuration/subagents.md) · [Permissions](./docs/configuration/permissions.md) · [MCP](./docs/configuration/mcp.md) · [Skills](./docs/configuration/skills.md) · [Logging](./docs/configuration/logging.md)
85
+ - **Guides:** [Tool proxy](./docs/guides/tool-proxy.md) · [Background subagents](./docs/guides/background-subagents.md) · [`/btw`](./docs/guides/btw.md) · [`/claude-code-doctor`](./docs/guides/doctor.md) · [Per-turn stats](./docs/guides/turn-stats.md) · [Compaction and thinking](./docs/guides/compaction-and-thinking.md) · [Interactive transport](./docs/guides/interactive-transport.md) · [Other plugins](./docs/guides/compatibility.md)
86
+ - **Troubleshooting:** [Start from the symptom](./docs/troubleshooting/symptoms.md) · [Longer cases](./docs/troubleshooting/longer-cases.md) · [Which login bills what](./docs/troubleshooting/which-login-bills-what.md) · [Known limitations](./docs/troubleshooting/known-limitations.md)
87
+ - **Internals:** [How a turn works](./docs/internals/how-a-turn-works.md) · [How a proxied call ends](./docs/internals/how-a-proxied-call-ends.md) · [Scratch files and security](./docs/internals/scratch-files-and-security.md) · [Measurement culture](./docs/internals/measurement-culture.md) · [Testing](./docs/internals/testing.md) · [Development](./docs/internals/development.md) · [Releasing](./docs/internals/release.md)
1435
88
 
1436
- ### Env-var overrides
89
+ ### Where the old README sections went
1437
90
 
1438
- Set explicitly to override config for one process — useful for one-off
1439
- debugging without editing `opencode.jsonc`:
91
+ This README used to hold all of the above. Release notes and bookmarks point at its anchors, so:
1440
92
 
1441
- ```bash
1442
- OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode # file on
1443
- OPENCODE_CLAUDE_CODE_LOG_FILE=0 opencode # file off (overrides config:true)
1444
- OPENCODE_CLAUDE_CODE_LOG_DIR=/tmp/cc opencode # custom dir
1445
- OPENCODE_CLAUDE_CODE_LOG_LEVEL=debug opencode # capture every level
1446
- DEBUG=opencode-claude-code opencode # promote to mode:"debug"
1447
- ```
1448
-
1449
- Boolean env vars accept `1/true/on/yes` for on and `0/false/no/off` for
1450
- off; empty / unset falls through to config. Invalid `level` values fall
1451
- through to config.
1452
-
1453
- ### Startup diagnostics
1454
-
1455
- Once per process, right after the provider(s) register, the plugin logs a
1456
- single `NOTICE: claude-code plugin ready` line summarizing everything worth
1457
- knowing before you start debugging anything else:
1458
-
1459
- ```bash
1460
- OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode
1461
- grep "plugin ready" ~/.local/share/opencode-claude-code/plugin.log
1462
- ```
1463
-
1464
- ```json
1465
- {
1466
- "plugin": "0.11.1",
1467
- "opencode": "1.18.5",
1468
- "cwd": { "resolved": "/Users/you/code/app", "source": "process" },
1469
- "providers": ["claude-code-default", "claude-code-work"],
1470
- "accounts": ["default", "work"],
1471
- "proxyTools": ["Bash", "Edit", "Write", "WebFetch", "Task"],
1472
- "mcpServers": ["github", "slack"],
1473
- "permissionPresets": [
1474
- { "provider": "claude-code-default", "preset": "none", "applied": false, "overrides": [] },
1475
- {
1476
- "provider": "claude-code-work",
1477
- "preset": "read-only",
1478
- "applied": true,
1479
- "overrides": ["skipPermissions: forced to false; ..."]
1480
- }
1481
- ],
1482
- "interactiveTransport": false,
1483
- "anthropicApiKeyInEnv": false,
1484
- "claudeCli": { "path": "claude", "version": "2.1.211 (Claude Code)" }
1485
- }
1486
- ```
1487
-
1488
- Reading it:
1489
-
1490
- - **`cwd.source`** is which rule picked the working directory Claude will be
1491
- spawned in: `configured` (you pinned `options.cwd`), `process` (normal),
1492
- `captured` (`process.cwd()` was unusable and opencode's project directory
1493
- rescued it, the macOS GUI-launch case), or `unresolved` (neither worked).
1494
- The per-session tier that `opencode serve` uses is resolved per call and so
1495
- cannot appear here; this line mirrors the synchronous order only.
1496
- - **`claudeCli.version`** reading `not detected` means the `claude` binary at
1497
- that path didn't answer `--version`, which also disables version-gated
1498
- flags like `--thinking-display`.
1499
- - **`mcpServers`** is the on-disk merge, before opencode's runtime toggles
1500
- are applied (those aren't settled yet at startup).
1501
- - **`permissionPresets`** is one row per provider rather than a single value,
1502
- because a preset is a safety posture and two accounts can be configured with
1503
- different ones. `preset` is the configured name or `none`; `applied` is false
1504
- for `none` and for a name the plugin does not recognise, which applies
1505
- nothing at all; `overrides` are the operator settings the preset replaced,
1506
- the same lines logged at NOTICE when it was applied.
1507
- - **`opencode`** is read from the running opencode binary (`--version`), since
1508
- opencode still does not hand its version to plugins. It reads `unknown` when
1509
- opencode is run from source rather than as the packaged binary.
1510
-
1511
- This block is logged once, to a file that is off by default. For the same
1512
- fields plus live process and proxy state, without enabling logging, run
1513
- [`/claude-code-doctor`](#plugin-health-with-claude-code-doctor) in the session.
1514
-
1515
- ### Default behavior (no config, no env)
1516
-
1517
- Nothing persists; only WARN and ERROR bubble in the TUI. The plugin
1518
- doesn't accrete a log file on every user's disk by default — opt in when
1519
- you need to inspect auto-continue decisions, broker state, or other
1520
- plugin internals.
1521
-
1522
- ## Compatibility with other opencode plugins
1523
-
1524
- ### [opencode-dcp](https://github.com/Opencode-DCP/opencode-dynamic-context-pruning) (Dynamic Context Pruning)
1525
-
1526
- Partial support since v0.5.1. DCP runs in a useful degraded mode: its automatic strategies and slash commands work, while its own model-facing tools do not reach the model. Model-driven compression is still available, through this plugin's opt-in [`compress` proxy](#context-compression) rather than DCP's tool.
1527
-
1528
- | DCP feature | Status | Notes |
1529
- |---|---|---|
1530
- | `experimental.chat.messages.transform` (compression placeholders, dedup, error purge) | ✅ Works | Transforms run inside opencode before reaching this plugin. |
1531
- | `experimental.chat.system.transform` (context-limit nudges, iteration reminders) | ✅ Works in headless | Headless spawns forward system-role content via `--append-system-prompt-file`. Interactive mode intentionally omits opencode's forwarded system prompt and keeps only this plugin's CLI/AGENTS/continuation prompt. |
1532
- | `/dcp compress`, `/dcp sweep`, `/dcp manual`, `/dcp context`, `/dcp stats` slash commands | ✅ Works | Handled by opencode's `command.execute.before` hook, not the model. |
1533
- | Automatic `deduplication` + `purgeErrors` strategies | ✅ Works | Message-transform only, no model tool calls. |
1534
- | DCP's own autonomous `compress` / `distill` / `prune` tool calls | ⚠️ Opt-in | DCP registers those as opencode-native tools rather than through an MCP server, so the automatic MCP routing never saw them. Name one in [`proxyOpencodeTools`](#forwarding-opencode-s-own-tools) and it is forwarded: `proxyOpencodeTools: ["compress"]` makes `mcp__opencode_proxy__compress` run DCP's real tool. |
1535
- | Model-driven compression through this plugin's `compress` proxy | ⚠️ Opt-in | Add `"Compress"` to `proxyTools` and the plugin exposes `mcp__opencode_proxy__compress`, which gives the model a working way to compress its own context. It is not DCP's tool and does not use DCP's strategies. See [Context compression](#context-compression). |
1536
- | DCP's `<dcp-system-reminder>` context-limit nudges when no compress tool is reachable | ⚠️ Opt-in strip | Those reminders are anchored into messages, so each one is re-sent with every message that carries it. If you run without either compress route, `stripContextReminders: true` removes them. It turns itself off as soon as a `compress` tool is proxied. |
1537
-
1538
- So autonomous compression is available, and DCP's own implementation is now one of the options. Three routes, all opt-in:
1539
-
1540
- - `proxyOpencodeTools: ["compress"]` forwards **DCP's** tool, which compresses opencode's transcript using DCP's strategies.
1541
- - `proxyTools: [..., "Compress"]` exposes **this plugin's** tool, which resets the Claude Code session and carries a summary into the fresh one.
1542
- - Neither, and trigger DCP by hand with `/dcp compress`.
1543
-
1544
- The two compress different windows, so pick deliberately rather than enabling both; [Forwarding opencode's own tools](#forwarding-opencode-s-own-tools) explains what happens if you do. With neither enabled, the plugin's appended system prompt tells Claude that no such tool exists and to ignore instructions asking for it, which is the correct answer in that case.
1545
-
1546
- ---
1547
-
1548
- ## Troubleshooting
1549
-
1550
- Four checks answer almost everything. Run them in this order, and stop as soon as one of them explains what you are seeing.
1551
-
1552
- | Check | What it tells you |
93
+ | Old README anchor | Now at |
1553
94
  |---|---|
1554
- | `/claude-code-doctor` in the session | The plugin version actually loaded, the `claude` path and version, which providers and accounts registered, `proxyTools`, the `permissionPreset` per provider and what it replaced, the working directory and which rule picked it, every live `claude` child, and every pending proxy call. No model is called and nothing is billed. Start here. |
1555
- | `/claude-code-doctor bundle` in the session | The same report plus this process's recent `NOTICE`/`WARN`/`ERROR` log lines, redacted by allowlist so you can paste the lot into a public issue. **This is what to attach to a bug report.** See [Filing an issue](#filing-an-issue-claude-code-doctor-bundle). |
1556
- | `OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode`, then grep `~/.local/share/opencode-claude-code/plugin.log` | Whether the plugin loaded at all, and every warning it emitted. The log file is off by default, so turning it on needs a relaunch. The raw log is **not** safe to attach to an issue: it has no redaction guarantee and can hold whole system prompts. Use `/claude-code-doctor bundle` for that. |
1557
- | `claude --version` | Whether a version-gated feature can work at all. Version floors: 2.1.142 thinking summaries, 2.1.220 fast mode, 2.1.258 `/btw` and `--restricted`, 2.1.263 `--permission-prompts none`, 2.1.280 `claude-opus-5-5`. |
1558
- | `claude auth status`, or `CLAUDE_CONFIG_DIR=~/.claude-<name> claude auth status` | Which account is signed in, and whether its login is still valid. |
1559
-
1560
- ### Start from the symptom
1561
-
1562
- | What you see first | The one check | The fix |
1563
- |---|---|---|
1564
- | No `claude-code` provider or model in the picker at all | Is there a `plugin ready` line in the log? | None means the plugin never loaded, an older version means the package cache. See [Nothing in the picker](#nothing-in-the-picker-or-a-version-you-just-upgraded-to-is-missing). |
1565
- | `Model unavailable` for a model id you typed | The `providers` field of the ready block, or the same line in `/claude-code-doctor` | Use the provider id that line actually lists. With no `accounts` configured on opencode 2 the id is `claude-code`, so `claude-code-default/<model>` fails while the plugin is perfectly healthy (measured on opencode 2.0.16, 2026-09-27). Declaring [`accounts`](#multiple-claude-code-accounts) is what creates `claude-code-default`. |
1566
- | 400 `Third-party apps now draw from your extra usage…` | `/claude-code-doctor` for the account the conversation is on, then `claude auth status` for its plan | An account-level usage gate, not a plugin fault: extra usage is off, or the window is exhausted. Wait for the reset, or move to another configured account. This is one of the two error texts that open the [account failover](#account-failover) form, so with several accounts you get the form instead of the error. Enabling paid usage or changing authentication is a billing decision and nothing here makes it for you. |
1567
- | `Tool result name changed`, and the turn aborts, on opencode 2 | The `plugin` version in `/claude-code-doctor` | Fixed in 0.28.1: a CLI-executed tool's result used to reach opencode under a different name than its call, and opencode 2.0.16 aborts the turn on that mismatch, which broke every Claude-side MCP server call. Upgrade, then **fully quit and relaunch every opencode window**: plugin code is read once at process start, so a new package in a running window changes nothing. |
1568
- | A Claude Code hook you configured has no effect, and Claude never mentions it | The **Hooks Claude Code ran that failed** section of `/claude-code-doctor` | The hook exited non-zero, so Claude Code discarded its contribution and answered the turn anyway. The section gives the exit code and the hook's stderr. Fix or remove it in your own Claude Code settings; only `SessionStart` and `Setup` hooks are visible here, because the plugin does not pass `--include-hook-events`. |
1569
- | `plugin ready` is missing from the log | That the log file is actually on, since it is off by default | If it is on and the line is still absent, the plugin never loaded. Check the package is in `plugin` (1.x) or `plugins` (2.x), that a local checkout points at `dist/` on 2.x, and that you relaunched rather than opened a new session. `/claude-code-doctor` answers the same questions without enabling logging. |
1570
- | `Failed to authenticate: OAuth session expired`, one account, every turn failing in milliseconds | `CLAUDE_CONFIG_DIR=~/.claude-<name> claude auth status` for that account | Log it in again. The plugin writes a `▌ **claude account:**` note naming the account and the exact command, for example `CLAUDE_CONFIG_DIR=~/.claude-work claude auth login`, and offers the switch form when another account exists. Restart opencode afterwards: a switch made from that form lasts until opencode restarts. |
1571
- | A tool call reported as rejected although it really ran | The `plugin` version in `/claude-code-doctor` | Upgrade to 0.26.2 or newer. Two separate causes, both fixed: opencode 1.18.32 aborts the provider signal of every step that ends in tool calls and the plugin read that as you pressing stop (0.26.1), and a call waiting on an unanswered permission prompt was rejected at the flat 10-minute deadline, after which your late approval cancelled Claude's next call (0.26.2). A deadline now waits while opencode reports the session busy, so an unanswered prompt is never a reason to raise `proxyToolTimeoutMs`. |
1572
- | `proxy call still waiting` in the log, or a `task` that looks stuck | `/claude-code-doctor`, which lists every pending call with its tool, age and deadline | Usually nothing is wrong. See [A proxy call that will not finish](#a-proxy-call-that-will-not-finish). |
1573
- | `⚙ invalid` or `⚙ unknown` tool rows | Which tool name the row carries | `⚙ invalid todowrite` inside a subagent means that agent has no `permission.todowrite: "allow"`; see [Subagent todos](#subagent-todos). Any other name is a Claude tool this plugin version does not map for your CLI version: record the plugin version, the CLI version and the tool name, and report it. A permanently pending `⚙ unknown` row is the same problem in its older shape, an input delta for a call opencode never saw start. |
1574
- | A `-fast` model clearly ran at ordinary speed | Grep `plugin.log` for `fast mode` | Fast mode fails soft, so the plugin warns once per reason and names it; the CLI reports `fast_mode_state: "off"`. The usual cause is that usage credits are off (`/usage-credits` in an interactive `claude`). Also: a CLI below 2.1.220, a cooldown after a fast-mode rate limit, free tier or an organization that disabled it, `CLAUDE_CODE_DISABLE_FAST_MODE=1`, or a non-first-party route, since Bedrock, Vertex and Foundry are excluded. Until it is fixed, switch to the non-fast id so the picker's price matches your bill. |
1575
- | An MCP server's tools are simply absent | The WARN the plugin logs once per process at session start for each server Claude Code could not connect | Authenticate or repair that server where it is configured. `mcpServers` in the ready block is on-disk discovery, so a server can be listed there and still be unreachable. |
1576
- | A freshly published version does not appear | The `plugin` version in `/claude-code-doctor` against the version you expect | Remove the frozen cache entry and relaunch: see [Nothing in the picker](#nothing-in-the-picker-or-a-version-you-just-upgraded-to-is-missing). If npm itself does not list the version, a local security scanner with a minimum-package-age policy can be filtering it out of the reply, so read that tool's event log before blaming the registry. |
1577
- | `permissionPreset` is set but nothing about the session looks restricted | The `permissionPreset` row in `/claude-code-doctor`, for the provider the conversation is actually on | `none` there means the option never reached this provider: it belongs under `provider.<id>.options`, and with `accounts` configured each account is its own provider id. `readonly (unknown, nothing applied)` means the name is not one the plugin knows, so nothing was applied at all; the only name today is `read-only`. When it did apply, the **Permission preset overrides** block names every option it replaced. |
1578
- | `permissionPreset: "read-only"` is set, but reads are not confined or something still prompts | `claude --version` | The preset holds on any CLI, but two of its four layers are version-gated: `--restricted` needs 2.1.258 and `--permission-prompts none` needs 2.1.263. Below those it falls back to `--disallowedTools` plus the plugin's own denial of every permission request, and warns naming what is missing. Below 2.1.258 you lose the working-directory confinement on reads; below 2.1.263 the denial happens in the plugin instead of in the CLI, one layer instead of two. See [Read-only mode](#read-only-mode). |
1579
- | A reply that is only *"There's an issue with the selected model (…). It may not exist or you may not have access to it."* | Whether that model id is in the picker, and `claude -p --model <id> "hi"` | The CLI refused the model: it is retired, misspelled, or this account cannot use it. The result's `subtype` is `success`, so without a chain the turn finishes as an ordinary reply with that sentence as the answer. Fix the id in the agent's `forceModel` or in your picker, or declare a [fallback model chain](#fallback-model-chain) so the turn degrades to the next model instead of dying. |
1580
- | `▌ **model fallback:**` on a turn you expected to run on a specific model | The note itself, which names the model that failed and why | Working as configured: your [`fallbackModels`](#fallback-model-chain) chain moved the turn. `model_not_found` means fix the first id. `out of usage on this account` means that account's cap, and the reason you got a chain rather than the [account failover](#account-failover) form is that no other account was available to offer. The chain never changes account, only model. |
1581
- | A chain is declared but a refused model still kills the turn | Grep `plugin.log` for `fallback model refused: unknown model` | Every entry has to be a model id this plugin registers; an unknown one is skipped with that warning, and a chain whose entries are all unknown is an empty chain. The other empty-chain case is a list containing only the model the turn already runs on, which is dropped from its own chain. Compaction turns, title stubs and the interactive transport never fall back at all. |
1582
- | A config change did nothing | `/claude-code-doctor`, which reports the options in force | Provider options are read once at opencode startup. Quit every opencode window, `serve` and GUI processes included, and relaunch. A `/new` session is not enough. |
1583
- | A question form never renders and the turn hangs | `GET /question` on the same opencode server and workspace | See [A question form never renders](#a-question-form-never-renders-and-the-turn-hangs). |
1584
-
1585
- ### Longer cases
1586
-
1587
- #### Nothing in the picker, or a version you just upgraded to is missing
1588
-
1589
- The plugin's own startup line separates "never loaded" from "loaded and misconfigured":
1590
-
1591
- ```bash
1592
- OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode
1593
- grep "plugin ready" ~/.local/share/opencode-claude-code/plugin.log
1594
- ```
1595
-
1596
- One `NOTICE: claude-code plugin ready` entry per process reports the plugin version, the `claude` binary and version it found, the directory it will spawn in, and which providers registered. [Startup diagnostics](#startup-diagnostics) explains every field, and `/claude-code-doctor` prints the same fields plus live process state without enabling the log at all.
1597
-
1598
- **No line.** The plugin did not load. Confirm the package spec is in `plugin` (opencode 1.x) or `plugins` (2.x), that a local checkout points at the repository root on 1.x and at `dist/` on 2.x, and that you fully relaunched: plugins are loaded once, at process start.
1599
-
1600
- **A line naming an older version.** That is opencode's package cache. It resolves the `@latest` spec once and freezes the concrete version, so restarting never re-resolves the tag. Delete the entry and relaunch:
1601
-
1602
- ```bash
1603
- rm -rf ~/.cache/opencode/packages/@khalilgharbaoui/opencode-claude-code-plugin@latest
1604
- ```
1605
-
1606
- A `file://` install is different: it runs the checkout's `dist/`, so rebuild with `npm run build` and restart rather than deleting anything.
1607
-
1608
- **`claudeCli.version` reading `not detected`.** The binary at that path did not answer `--version`, which also silently disables every version-gated flag, including `--thinking-display summarized`, `--plugin-dir` and the fast-mode opt-in.
1609
-
1610
- #### A question form never renders and the turn hangs
1611
-
1612
- For a stalled call, inspect `GET /question` on the same opencode server and workspace. If no request exists, check awaited `tool.execute.before` hooks and custom tools replacing `question`, especially notification plugins: a hook opencode waits on runs *before* the tool, so the request cannot exist yet. If a request exists but no form appears, check session ownership, pending permissions, and event delivery. The separate detach/reattach issue [anomalyco/opencode#36604](https://github.com/anomalyco/opencode/issues/36604) remains open; [PR #36603](https://github.com/anomalyco/opencode/pull/36603) is closed without merging. Do not infer a universal platform or version failure from either symptom.
1613
-
1614
- #### A proxy call that will not finish
1615
-
1616
- A proxied call ends on an event rather than a clock ([how a proxied call ends](#how-a-proxied-call-ends)), so three log lines exist to keep the waiting visible. **None of them is a failure, and none of them ends a call:**
1617
-
1618
- - `proxy call still waiting, no deadline`, at WARN, five minutes in and every five minutes after, naming the tool, the call id, how long it has waited and what will end it. `task` and `task_batch` have no deadline by default, so this is exactly what a healthy long-running subagent looks like.
1619
- - `proxy call still waiting, deadline approaching`, once, at 60% of a deadline that does exist, carrying the time remaining and naming the option that would extend it. Deadlines under a minute are not announced at all.
1620
- - `proxy call past its deadline, but opencode is still serving it; waiting`, when the deadline passed while opencode reported the session busy: most often a permission prompt nobody has answered yet. It is rechecked every minute.
1621
-
1622
- `/claude-code-doctor` lists the same calls on demand, with ages and deadlines. Use it to tell a working subagent from a wedged one *before* changing any timeout, and read [per-tool proxy timeouts](#per-tool-proxy-timeouts) before setting one.
1623
-
1624
- ### Which login bills what
1625
-
1626
- The `claude` CLI decides this, not the plugin, and the plugin only reports it.
1627
-
1628
- - **An OAuth subscription login** (`claude auth login`) is the normal case: turns run on your Claude plan. The default transport here is headless `--print`, which is the Agent SDK path, and what that draws from is Anthropic's policy to set: read the dated note under [Billing](#billing) rather than assuming. The [interactive transport](#interactive-transport-experimental) drives the real TUI instead and bills as normal plan usage.
1629
- - **An API key in the environment.** `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` in the environment that launched opencode reaches the CLI, which prefers it over your subscription login and bills the Platform account pay as you go. This is the one route [`ignoreAnthropicApiKey`](#options-reference) can strip, and the plugin warns at startup whenever it sees one, whatever that option is set to.
1630
- - **An API key the CLI found by itself**, from its own `user`, `project` or `org` settings scopes or from an `apiKeyHelper`. That is the CLI's configuration rather than opencode's, so no plugin option removes it.
1631
- - **`apiKeySource` is the field that tells the truth.** The CLI reports it on the `system` init event of every session, and anything other than `oauth` (the subscription) or `none` means a key is in effect. The plugin warns once per process when that happens. An absent `ANTHROPIC_API_KEY` does not prove pay-as-you-go is off, because of the route above; `apiKeySource` does.
1632
- - **Bedrock and Vertex** are the other two things the CLI's authentication can be, and if it is one of them then neither a Claude subscription nor an Anthropic key is in play for that turn. Fast mode is first-party only, so it is excluded on Bedrock, on Vertex and on Foundry.
1633
-
1634
- With more than one account configured, an account that runs out mid-task ends the turn on a form instead of an error, and the pick is sticky for the limited account until its reset time: see [Account failover](#account-failover). The one cost worth knowing before you pick is that the conversation is replayed into a fresh session on the target account, because Claude transcripts live under each account's own `CLAUDE_CONFIG_DIR` and `--resume` cannot cross accounts.
1635
-
1636
- ---
1637
-
1638
- ## Known limitations
1639
-
1640
- - Tool inputs stream as they are constructed (Anthropic's `input_json_delta` is forwarded as `tool-input-delta`), but only for tool calls opencode actually sees. Calls the plugin deliberately does not forward, meaning proxy tools, CLI-internal `WebSearch`, `AskUserQuestion`, `ExitPlanMode`, the todo-ledger `Task*` family and Claude's other internal tools, have their deltas suppressed, because a delta for a tool opencode never saw start renders as a permanently pending `⚙ unknown` row.
1641
- - Raw chain-of-thought is not available. Claude 4 family models ship summarized thinking only. See [Extended thinking](#extended-thinking) for the full picture.
1642
- - Recommended Claude Code CLI: **2.1.142+**. Older CLIs work for everything else but skip the `--thinking-display` flag, so Claude Opus 4.7 turns may render empty Thinking rows. If something breaks after a Claude Code update, the CLI version is the first thing to check.
1643
- - **Foreground Task calls have no proxy deadline by default.** The plugin listens for the events that end a call instead of timing it (see [How a proxied call ends](#how-a-proxied-call-ends)), so a subagent runs to completion and a chat parked in one holds its `claude` worker until you abort, send another message, delete the chat, or the process goes away. Such a call warns that it is still waiting after five minutes and every five minutes after, so it is never silent. Add a wall-clock backstop via [`proxyToolTimeoutMs`](#per-tool-proxy-timeouts) if you want one. For independent work that should not block the turn at all, use `background: true` after enabling opencode's experimental background-subagent flag.
1644
- - **Subagent todos require explicit permission.** See [Subagent todos](#subagent-todos) for the rule and a working config.
1645
-
1646
- ---
1647
-
1648
- ## Development
1649
-
1650
- ```bash
1651
- bun install
1652
- bun run typecheck # tsc --noEmit
1653
- bun run test # tsx --test (unit suite)
1654
- bun run build # tsup -> dist/
1655
- ```
1656
-
1657
- Source layout:
1658
-
1659
- ```
1660
- src/
1661
- index.ts # opencode plugin entry, config + provider hooks
1662
- models.ts # default models + variants
1663
- accounts.ts # multi-account expansion (per-account CLAUDE_CONFIG_DIR + wrapper script)
1664
- claude-code-language-model.ts # AI-SDK provider that drives `claude`
1665
- message-builder.ts # AI-SDK prompt → Claude CLI user message
1666
- tool-mapping.ts # Claude tool name ↔ opencode tool name mapping; internal-tool skip list
1667
- proxy-mcp.ts # in-process MCP server for proxied tools
1668
- proxy-broker.ts # pending proxy-call broker between proxy-mcp and opencode tool execution
1669
- mcp-bridge.ts # opencode → Claude --mcp-config translator
1670
- session-manager.ts # LRU cache of CLI subprocesses
1671
- cli-version.ts # detect Claude CLI version, gate optional flags
1672
- runtime-status.ts # runtime introspection of opencode (MCP status, tool registry)
1673
- logger.ts # DEBUG=opencode-claude-code stderr logger
1674
- tmp.ts # per-plugin temp directory helper
1675
- cleanup-stale.ts # remove legacy unscoped install from opencode's plugin cache
1676
- types.ts # public option types
1677
- opencode-types.ts # mirrored opencode types
1678
- ```
1679
-
1680
- For runtime gotchas, the release flow, and the compatibility audit (last taken against **opencode 1.18.29**), see [`AGENTS.md`](./AGENTS.md).
1681
-
1682
- ## Publishing (maintainers)
1683
-
1684
- ```bash
1685
- npm version patch # or minor/major — bumps package.json + creates the tag
1686
- git push origin master --follow-tags
1687
- ```
1688
-
1689
- The GitHub Actions workflow at `.github/workflows/publish.yml` runs `npm publish --access public` on tag push. Since v0.6.2 it authenticates with **npm trusted publishing (OIDC)**, not a token: the job holds `id-token: write`, upgrades npm first because OIDC needs npm 11.5.1 or newer, and passes no `NODE_AUTH_TOKEN`. The trusted publisher is configured on npmjs.com against this repository and the `publish.yml` workflow filename, so a publish that fails on auth means that configuration, not an expired secret. There is no `NPM_TOKEN` in the workflow.
1690
-
1691
- ## Star History
1692
-
1693
- <a href="https://www.star-history.com/?repos=khalilgharbaoui%2Fopencode-claude-code-plugin&type=date&legend=top-left">
1694
- <picture>
1695
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=khalilgharbaoui/opencode-claude-code-plugin&type=date&theme=dark&legend=top-left&sealed_token=XBPNnYotm7Eti4lpRGsbKl_dsq6XGUtRkvCxE4UpQH2HM4LifiiTNV1hqjCOsivRZ-e2hFDohid8iERSP5XO5JdkNhHcuS2bLZFIdQIWZO1NldJLD2TjaaSYK6GJcnXYZHivkbiiynG7b8-V8z9LLn8Uo2ED15OWnUd3devehrMyKJJO_dtOW1ivZ3yJ" />
1696
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=khalilgharbaoui/opencode-claude-code-plugin&type=date&legend=top-left&sealed_token=XBPNnYotm7Eti4lpRGsbKl_dsq6XGUtRkvCxE4UpQH2HM4LifiiTNV1hqjCOsivRZ-e2hFDohid8iERSP5XO5JdkNhHcuS2bLZFIdQIWZO1NldJLD2TjaaSYK6GJcnXYZHivkbiiynG7b8-V8z9LLn8Uo2ED15OWnUd3devehrMyKJJO_dtOW1ivZ3yJ" />
1697
- <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=khalilgharbaoui/opencode-claude-code-plugin&type=date&legend=top-left&sealed_token=XBPNnYotm7Eti4lpRGsbKl_dsq6XGUtRkvCxE4UpQH2HM4LifiiTNV1hqjCOsivRZ-e2hFDohid8iERSP5XO5JdkNhHcuS2bLZFIdQIWZO1NldJLD2TjaaSYK6GJcnXYZHivkbiiynG7b8-V8z9LLn8Uo2ED15OWnUd3devehrMyKJJO_dtOW1ivZ3yJ" />
1698
- </picture>
1699
- </a>
95
+ | `#configuration`, `#options-reference` | [docs/configuration/options.md](./docs/configuration/options.md) |
96
+ | `#environment-variables` | [docs/configuration/environment.md](./docs/configuration/environment.md) |
97
+ | `#models`, `#fast-mode` | [docs/models.md](./docs/models.md) |
98
+ | `#multiple-claude-code-accounts`, `#account-failover` | [docs/configuration/accounts.md](./docs/configuration/accounts.md) |
99
+ | `#billing` | [docs/billing.md](./docs/billing.md) |
100
+ | `#troubleshooting` | [docs/troubleshooting/symptoms.md](./docs/troubleshooting/symptoms.md) |
101
+ | `#plugin-health-with-claude-code-doctor` | [docs/guides/doctor.md](./docs/guides/doctor.md) |
102
+ | `#selective-tool-proxy` | [docs/guides/tool-proxy.md](./docs/guides/tool-proxy.md) |
103
+ | `#credits` | [docs/credits.md](./docs/credits.md) |
1700
104
 
1701
105
  ## Credits
1702
106
 
1703
- This plugin absorbs work from its forks directly, cherry-picked with the original authorship preserved or reimplemented with the author named in the commit, rather than waiting on pull requests. The people behind the features you are using:
107
+ This plugin absorbs work from its forks directly, cherry-picked with the original authorship preserved or reimplemented with the author named in the commit. The people behind the features you are using, and what each built, are on the [Credits](./docs/credits.md) page; `git log --author` on this repo shows the preserved authorship.
1704
108
 
1705
- | Who | What | Where |
1706
- |---|---|---|
1707
- | [@galvani](https://github.com/galvani) (Jan Kozak) | Per-session working directory for `opencode serve`, so one server spawns each project's `claude` in the right place. Also found the stale `toolCallMap` re-emission three months before it was fixed here. | `9e02ce4`, `2238ed0` |
1708
- | [@HeikoAtGitHub](https://github.com/HeikoAtGitHub) | Stopped sending `AGENTS.md` to the model twice (opencode already forwards it). Independently diagnosed the 5-minute proxy wall. | `25260a4`, `42f426d` |
1709
- | [@bernardofortes](https://github.com/bernardofortes) (Bernardo Fortes) | `idleProcessTimeoutMs`, idle eviction of retained `claude` workers. | `a5f723a` |
1710
- | [@broskees](https://github.com/broskees) (Joseph Roberts) | Task proxy default-on (PR #18), the abort `interrupt` so Esc really stops the CLI, the skill bridge, `task_batch` for concurrent subagents (and the measurement that the CLI serialises MCP calls), the undici 300 s diagnosis of the proxy wall, the lifecycle release of proxied calls that made the `task` deadline unnecessary (PR #36), Claude Opus 5.5 with its fast-mode entry (PR #43), and the fix for turn-summed usage that made opencode auto-compact far below the window (PR #63). | PR #18, `68ed142`, PR #36, PR #43, PR #63 |
1711
- | [@jknlsn](https://github.com/jknlsn) (Jake Nelson) | Per-tool proxy timeouts, subagent dispatch steering, the question proxy, the start watchdog respawn. | `84f3db9`, `94980a6`, `47501d0`, `ffefc24` |
1712
- | [@CollieIsCute](https://github.com/CollieIsCute) (Collie Tsai) | The plan-mode approval bridge. | `8c5b583` |
1713
- | [@flupkede](https://github.com/flupkede) | The compress proxy tool design and the AI-SDK v4 image-part fix. | `4ac319f`, `60a6e9a` |
1714
- | [@CNQQC](https://github.com/CNQQC) | Cost units corrected to dollars per million tokens (PR #25). | PR #25 |
1715
- | [@willmcginnis](https://github.com/willmcginnis) | The proxy endpoint authentication (PR #28, GHSA-3mxm-w7gf-3c5x). | PR #28 |
1716
- | [@nic-lan](https://github.com/nic-lan) | The issue #29 diagnosis of subagent output lost across the CLI resume boundary, and the fix for unattended output replaying as one text block per delta (PR #35). | #29, PR #35 |
1717
- | [@acastro2](https://github.com/acastro2) (Alexandre Castro) | Found and fixed CLI tool results being emitted under a different name than their call, which made opencode 2 abort every turn that used a Claude-side MCP server (PR #46). | PR #46 |
1718
- | [@bangnh1](https://github.com/bangnh1) | Independently found and diagnosed the turn-summed usage that tripped auto-compaction after a single prompt, measured on opencode 2 (PR #62; the fix landed as PR #63), and fixed opencode 2's MCP config layout (`mcp.servers`, `disabled`, `providers.<id>.settings`) with opt-in Code Mode `execute` proxying (PR #67). | PR #62, PR #67 |
1719
- | [@JWebCoder](https://github.com/JWebCoder) (joao moura) | Diagnosed that auto-continue never fires on current CLIs (PR #15). | PR #15 |
109
+ Free and MIT-licensed. If the plugin saves you time, you can buy its maintainer a coffee:
1720
110
 
1721
- Commit hashes are on the contributors' forks where the work was cherry-picked; `git log --author` on this repo shows the preserved authorship.
111
+ <a href="https://www.buymeacoffee.com/khalilgharbaoui"><img src="site/public/buy-me-a-coffee.png" alt="Buy me a coffee" width="214" height="60"></a>
1722
112
 
1723
113
  ## License
1724
114
 
1725
- MIT. See [LICENSE](./LICENSE).
1726
-
1727
- Original work © `unixfox`. Fork modifications © Khalil Gharbaoui.
115
+ MIT. See [LICENSE](./LICENSE). Original work © `unixfox`. Fork modifications © Khalil Gharbaoui.