@khalilgharbaoui/opencode-claude-code-plugin 0.64.0 → 0.65.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khalilgharbaoui/opencode-claude-code-plugin",
3
- "version": "0.64.0",
3
+ "version": "0.65.0",
4
4
  "description": "Claude Code CLI provider plugin for opencode",
5
5
  "homepage": "https://opencode-claude-code-plugin.dev/",
6
6
  "funding": {
@@ -29,7 +29,7 @@
29
29
  "build": "tsup",
30
30
  "dev": "tsup --watch",
31
31
  "typecheck": "tsc --noEmit",
32
- "test": "OPENCODE_CLAUDE_CODE_LOG_FILE=0 XDG_STATE_HOME=$(mktemp -d) tsx --test test/*.test.ts",
32
+ "test": "OPENCODE_CLAUDE_CODE_LOG_FILE=0 OPENCODE_CLAUDE_CODE_MODEL_PROBE=0 XDG_STATE_HOME=$(mktemp -d) tsx --test test/*.test.ts",
33
33
  "generate:log-messages": "tsx scripts/generate-log-messages.ts"
34
34
  },
35
35
  "dependencies": {
@@ -23,7 +23,8 @@ Three routes reach Claude from opencode, and they are not interchangeable:
23
23
  opencode's auth store, pay as you go, everything running in opencode's own runtime.
24
24
  2. **This plugin**: the official `claude` CLI authenticates, so opencode inherits
25
25
  whatever that CLI holds (subscription login, API key, Bedrock, Vertex). It never
26
- reads, stores or replays a token, which is why it is a sanctioned route. What it
26
+ reads, stores or replays a token. Anthropic has not approved or endorsed it, so
27
+ never describe it as approved or sanctioned. What it
27
28
  costs: a `claude` child per conversation, Claude Code's own context compaction and
28
29
  clearing happening behind opencode's back, a locally synthesised session title
29
30
  instead of a model-written one, `/compact` on its own short-lived spawn, no todo
@@ -34,6 +35,14 @@ Three routes reach Claude from opencode, and they are not interchangeable:
34
35
  February 2026, and each of those projects says so in its own README (a "community
35
36
  workaround", "no claims regarding compliance", "could be suspended or terminated").
36
37
 
38
+ A fourth route arrived in opencode 2.2.3: opencode's own **Claude Agent** provider
39
+ (`claude-agent`), which also drives the user's own `claude` (2.1.248+) and never
40
+ reads the login. It is opencode 2 only, runs every tool in opencode, leaves the
41
+ `~/.claude` setup (CLAUDE.md, hooks, skills, MCP servers, memory, auto-compaction)
42
+ out, and serves one login. It is a fair choice for a user on 2.2.3+ with one account
43
+ who wants exactly that; this plugin is the one for opencode 1.x, several accounts,
44
+ the user's own Claude Code setup, fast mode, `/btw`, the sidebar and the doctor.
45
+
37
46
  Never recommend route 3, never help migrate a user onto it, and never enable
38
47
  `interactive: true` to dodge a billing or access restriction. If a user asks why this
39
48
  plugin is slower or heavier than a proxy, the answer is route 2's cost list above, plus
@@ -112,6 +121,7 @@ Defaults below describe normal headless opencode use when the key is absent.
112
121
  | Option | Type | Default | What it does |
113
122
  |---|---|---|---|
114
123
  | `cliPath` | string | `"claude"` | Executable, not a shell command with flags. Use an absolute path for a non-PATH install. The opencode config hook supplies this default; only direct `createClaudeCode()` use falls back to `CLAUDE_CLI_PATH`. Account providers wrap it; never select a generated wrapper yourself. |
124
+ | `cliModelCatalog` | boolean | `true` | Register the models as the installed `claude` describes them. Once per CLI version a background probe (one `claude --print --bare --model <id>` per catalog id, about 16 spawns and 3 s on 2.1.314, sent `initialize` and `get_context_usage` and never a message, with no credential in its env: measured no API request) records which models the CLI knows, the context window it runs each in and the effort levels it accepts, in `$XDG_STATE_HOME/opencode-claude-code-plugin/cli-models.json` (`0600`, atomic, keyed on the real binary and its version). Registration reads that file and never waits for the probe: a fresh answer applies at once on opencode 2 (it reloads the providers) and from the next start on opencode 1. What changes: windows and effort variants follow the CLI, and a model the CLI's own picker offers that the catalog lacks is added at an unknown (zero) price, usable by `forceModel`/`defaultSubagentModel`. Costs and `(N×)` stay the catalog's. A deadline kill writes nothing; a refusal is remembered for a day. `false` registers the built-in catalog and runs no probe. Doctor section: **Model catalog from the Claude CLI**. Log: `read the model catalog from the claude cli` (INFO). |
115
125
  | `accounts` | string[] | unset | Unset keeps provider `claude-code`. Any array, including `[]`, expands to `claude-code-default` plus normalized, deduplicated names. Non-default accounts use `~/.claude-<name>`; default uses the CLI's normal environment/auth. |
116
126
  | `accountFailover` | `"ask"` / `"auto"` / `"off"` | `"off"` | **Opt-in: only an explicit `"ask"` opens the switch form**, so unset and `"off"` behave identically. `"auto"`, also opt-in, makes the same switch without a form, moving the same request to the next eligible account in configured order inside the same turn (see "Automatic failover" below). `"ask"` ends a usage-limited turn on opencode's native `question` form listing the other configured accounts, and continues the task on the pick inside the same opencode turn. Only ever fires with more than one account configured. The pick is sticky for the LIMITED account until the limit's reset time (or until opencode restarts when the CLI reported none), so it covers every session on that account and subagents follow their parent; child sessions are never shown the form. Leaving it unanswered waits and costs nothing. `stop`, a dismissal, or text that is not one of the offered accounts ends the turn the way a limited turn ends without the form. Triggered only by a rejected `rate_limit_event`, one of the two known account-limit error texts, or one of the five account-level failure kinds the CLI names on its own error reply (`authentication_failed`, `oauth_org_not_allowed`, `account_on_hold`, `verification_required`, `billing_error`); never by a generic failure. Never on compaction turns; both transports (the TUI's limit reply carries the same `error: "rate_limit"` kind). A switch carries the Claude conversation to the target account's config dir and resumes it there (`crossAccountResume`, default on) and only replays the thread as text when that copy is refused; either way, MCP servers configured only in the limited account's Claude profile are gone. With the default `"off"`, a limited turn ends on one `▌ **usage limit:**` note instead (see "When an account runs out of usage"). |
117
127
  | `accountGroups` | object | – | **Off unless set**, and unset means every account behaves as one group, so nothing changes on an install that does not set it. A map from an account name in `accounts` to a group name, e.g. `{"work": "work"}`; every account not named, `default` included, is in one implicit group. With it set the plugin only moves a conversation between accounts in the SAME group, which covers every way one can move: the by-hand switch (picking a model under another account's provider in the same opencode session), the `accountFailover` form, the `"auto"` switch, the override either one sets, and `crossAccountResume`. A switch ACROSS groups sends the other account nothing at all, neither the Claude transcript nor a text replay of the thread (the replay is the same history by another route), starts fresh with only the current user message, and writes one `▌ **account group:**` note naming both accounts and both groups. The source account keeps the conversation and its transcript, so switching back continues it. The way BACK is guarded on the same lookup: that account still has its own Claude session, so the turn is an ordinary one, and the catch-up that would tell that session what it missed while the conversation ran elsewhere (`<missed_conversation>`) is withheld, with nothing else about the turn changed; a thread that was edited or undone in between still drops its session but is not replayed either, and writes the same note. The switch form offers only same-group accounts, the `"auto"` switch moves only to one, and with none left either behaves as a single-account install; `describeOtherAccounts` in the usage-limit and account-block notes is filtered the same way. An account name that is not configured is ignored with one WARN, as is a non-object value or a non-string group. Compaction is exempt and a child session follows its parent (under `"auto"` a child can move itself, which moves the parent with it, within the group). Shown resolved (never echoed) in the startup block and `/claude-code-doctor` as `accountGroups`, names only. Log: `starting fresh on this account: it is in another account group` (NOTICE, reason `another-account-group`), or `not replaying what this claude session missed: the conversation continued on an account in another group` for a withheld catch-up (NOTICE, same reason). It guards what the plugin moves on its own; it is not a permission system. |
@@ -150,6 +160,8 @@ Defaults below describe normal headless opencode use when the key is absent.
150
160
  | `proxyOpencodeMcpTools` | boolean | `false` | Route opencode's MCP-backed tools through opencode's executor instead of Claude's own `--mcp-config` child, so each call is permission-prompted and rendered as an opencode tool row. Default changed `true` to `false` here, with no behaviour change: at `true` it routed nothing, because discovery read opencode's tool registry, which never contains MCP tools. Discovery now reads the model tool set opencode passes the provider, verified live on opencode 1.18.31 / Claude Code 2.1.263. **Tell the user to set `strictMcpConfig: true` alongside it**: a server also present in Claude Code's own config is reached directly and the proxy is bypassed, which looks exactly like the option doing nothing. A routed call runs with the calling agent's permissions. Per server: a tool in the agent's tool set means proxied; `connected` in opencode with none of its tools in the agent's set means left out of that spawn entirely (this is what keeps an `explore` subagent from starting its own copy of every MCP server and being handed servers opencode withheld from it); anything else keeps the direct bridge. One INFO line, `proxyOpencodeMcpTools: MCP servers outside this agent's tool set`, names the `withheld` and `bridged` lists. With the option off every enabled server is bridged into every spawn, subagents included: suggest turning it on when subagents start slowly or get MCP servers they should not have. Inert with `bridgeOpencodeMcp: false`, which leaves no bridged server list to match names against. Do not promise exactly-once side effects across failures, retries or opencode versions; verify routing before using write-capable tools. |
151
161
  | `proxyOpencodeTools` | string[] | `[]` | Forward explicitly named opencode tools (case-insensitive): V1 resolves registry ids; V2 resolves the current model tool snapshot and its actual JSON Schema, including synthesized Code Mode `execute`, without re-exposing tools absent from that snapshot. Covers plugin-declared tools such as DCP's `compress` and V2 Code Mode. Same broker as other proxies; collisions and unknown names warn. Explicit allowlist only, because calls run in opencode with the agent's permissions. `execute` grants access to the session's whole Code Mode catalog, not just MCP, and is refused by the read-only preset. |
152
162
  | `stripContextReminders` | boolean | `false` | Strip opencode-dcp `<dcp-system-reminder>` blocks from user/assistant message text, including the fresh-session rebuild. Only when no `compress` is proxied via `proxyTools` or `proxyOpencodeTools`; reachable compress makes it inert. Resolved from config, so a configured-but-unregistered name still counts as reachable. Leaves opencode's own `<system-reminder>` blocks alone. |
163
+ | `alwaysLoadProxyTools` | boolean or string[] | `["bash", "edit", "write"]` | Proxy tools served with `_meta["anthropic/alwaysLoad"]`, so Claude Code puts them in every request instead of behind `ToolSearch` and the first proxied call of a process skips one API request. Measured on 2.1.314 (h #g276): a proxied `echo` took 2 calls instead of 3, 32% fewer prompt tokens, 41% lower cost; a turn with no tool call carried 531 more prompt tokens. `true` loads every proxy tool, `proxyOpencodeMcpTools` tools included (warn the user: many schemas in every prompt is what `ENABLE_TOOL_SEARCH=0` cost, 2.5 to 4 times); `false` defers them all (the behaviour before h #g276); a list names proxy tools, case-insensitive. `task` and `task_batch` are left deferred together on purpose. An older CLI ignores the field. |
164
+ | `verbatimPrompts` | boolean | `true` | Stamp `client_composed: true` on every text envelope the headless transport writes (user messages, forwarded messages, late proxy results, the auto-continue nudge; never a `tool_result` envelope), so Claude Code delivers the text as written: no slash-command dispatch and no `@path` expansion on the message's last text block. Measured on 2.1.314: without it `/model claude-sonnet-5-5` switched the process to Sonnet with no model call and `/cost` returned the CLI's usage screen. Hooks, `CLAUDE.md` and the skill listing still arrive; the `ultrathink` keyword reminder does not, so suggest an effort variant. `/btw` and `/claude-code-doctor` are unaffected. Exception: a message whose only text block starts with `/name` for the user's own command or skill (a `commands/<name>.md`, `commands/<name>/SKILL.md` or `skills/<name>/SKILL.md` under the Claude config dir or a `.claude` from the cwd up, or any reported colon name such as a plugin's `/plugin:cmd`) goes out unstamped and runs as typed, once a `claude` in that cwd has reported the name in its init frame; the first message in a cwd stays stamped (a model-invocable command still runs through the model's `Skill` call; a `disable-model-invocation: true` one does not). Built-ins stay stamped. Interactive transport not covered. Set `false` only when the user wants Claude Code's own `/` commands or `@` mentions typed through opencode. |
153
165
  | `multiStepContinuation` | boolean | `true` | Append a system-prompt hint to chain tool calls in one turn instead of stopping between subtasks. |
154
166
  | `autoContinueIncompleteTurns` | boolean or `"smart"` | `"smart"` | `true`/`"smart"` continue a turn truncated at `max_tokens`, bounded by 8 attempts and 10 minutes, and otherwise run the keyword heuristic only when stop reason is missing. Every other stop reason, plus error, abort or latched question, stops it. Current measured CLIs always report a reason, so truncation is the only case that resumes in practice. Also gates the `▌ **no reply:**` note written when a turn finishes cleanly with no text and no tool call; `false` turns off the note as well as the continuation. |
155
167
  | `compactionModel` | string | `"claude-haiku-4-5"` | `/compact` uses a fresh short-lived process on the selected transport without the usual bridge/proxy/skill wiring. On the PTY it disables built-in tools, uses an empty strict MCP config, submits summary instructions with the transcript as text, and closes the TUI after its answer. Nonblank `CLAUDE_CODE_COMPACTION_MODEL` wins. This is inference and can be billed. |
@@ -159,7 +171,8 @@ Defaults below describe normal headless opencode use when the key is absent.
159
171
  | `turnStats` | boolean | `false` | Append one `▌ **stats:**` line to each finished turn: cost, wall duration, CLI turn count, input/output/cache-read/cache-write tokens, and a permission-denial count when the turn had any, taken from the CLI's own `result`. Never on a compaction turn or a turn that ended in error. Its own text part, stripped from transcripts rebuilt for the CLI, so the model never sees it. The same numbers are logged at INFO regardless, and `modelUsage` plus `permission_denials` always reach `providerMetadata`. The cost is THIS turn's: the CLI reports the Claude session's running total, and the footer prints the difference from that session's previous total (the INFO line carries both, `costUsd` and `turnCostUsd`). The first turn of a session resumed after an opencode restart, or of a fork, prints no cost, because `--resume` restores the running total and there is nothing to subtract; the next turn has it again. Not a billing guarantee. |
160
172
  | `forkSessions` | boolean | `false` | When a new opencode session turns out to be a fork of one this provider already served, branch the parent's Claude conversation with `claude --resume <parent> --fork-session` instead of re-rendering the whole thread as text into the first message. Measured on CLI 2.1.280 with haiku 4.5 over a ~13k-token thread: 814 cache tokens written and 39,710 read, against 22,355 written and 17,385 read for the replay, so $0.0058 against $0.0467 for that turn; the parent's transcript is byte-identical afterwards. Neither opencode major tells a provider that a session is a fork, so the parent is found by matching this prompt's history against what each sibling session key was last asked to continue. Off by default because a resumed Claude conversation reuses the system prompt recorded on its FIRST request (`--system-prompt-snapshot`, default `on`), so a forked session answers under the parent's appended system prompt rather than this turn's; measured directly, a parent seeded with codename ZEBRA and forked while passing QUAIL answered ZEBRA. Falls back to the replay, unchanged, for: another account, an unknown or released parent session id, a busy parent (live process, proxied call in flight, unanswered plan-mode question), a fork cut mid-conversation, a fork taken mid tool round trip, a different cwd / model / agent / effort / prompt-cache TTL, compaction, an account-failover switch, and a `claude` whose `--help` does not advertise `--fork-session`. Recording costs nothing while the option is off. |
161
173
  | `resumeAfterRestart` | boolean | `true` | After an opencode restart, resume the conversation's Claude session (`--resume`) instead of replaying the thread as text. Persists session id + conversation digest per session key in `$XDG_STATE_HOME/opencode-claude-code-plugin/claude-sessions.json` (0600, 256 entries, 30 days) after each successful turn. Resumes only on the same session key and binary, with the transcript on disk and the history equal to the recorded conversation plus Claude's reply; anything else (edit, revert, compaction, account switch, open tool round trip) replays. opencode-dcp's `<dcp-message-id>` references (and its `@12@` compact form) are not part of the digest: dcp renumbers them on every opencode restart, so after an undo the same message came back under another number and the conversation was refused as `conversation-changed`. Records carry a digest `version`; one without it (written by an older plugin) is compared the old way. Not on compaction. Log: `resuming the claude session from before the restart` (NOTICE) or `not resuming ...` with a reason (INFO). `false` disables reading and writing. |
162
- | `resumeAcrossModelChanges` | boolean | `true` | When the operator changes a conversation's model or reasoning effort mid-conversation, continue the same Claude session (`--resume <id> --model <new>`) instead of replaying the thread as text. Both are in the session key, so changing either used to start a fresh Claude conversation (issue #91); neither needs one, because `--model` and `CLAUDE_CODE_EFFORT_LEVEL` are spawn-time and the CLI applies either to a resumed transcript (measured on 2.1.288). The session is MOVED off the old key, so one Claude conversation has exactly one owner. Refused on every term `resumeAfterRestart` is refused on (another account, a transcript that is gone, a conversation that changed) plus two more: a sibling whose transcript may still be written to (that turn is served on the sibling instead and the change applies at the next turn that finds it idle, h #g271), and anything that is not the same cwd, request scope, opencode session, agent and prompt-cache TTL. Not on compaction, not on an account-failover switch. It reads the record `resumeAfterRestart` writes, so `resumeAfterRestart: false` turns it off too. Log: `continuing this conversation's claude session under the new model or effort` (NOTICE). |
174
+ | `liveModelSwitch` | boolean | `true` | Apply a model or reasoning effort change to the conversation's RUNNING `claude` with the CLI's own control requests (`set_model`, `apply_flag_settings {effortLevel}`) instead of stopping it and spawning another with `--resume`. Measured on 2.1.288 and 2.1.314 (h #g278): the conversation is kept, the next request goes out under the new model or effort, and prompt cache reads and writes match the respawn's; what it saves is the respawn, 4.6 to 6.6 s to `system`/`init` on a real install, against 0.5 to 0.7 s for `set_model` and about 25 to 140 ms for an effort change. So that an effort change can apply, a headless spawn on a CLI that can switch (2.1.288+) passes `--effort <level>` and drops `CLAUDE_CODE_EFFORT_LEVEL` from the child's env: the variable beats `apply_flag_settings` silently (a `success` answer and an unchanged request). Only on an IDLE headless child of the same conversation whose key differs in nothing but the model name (same `@account`, same fast mode) and the effort; an effort changes live only between two explicit levels. A change that lands mid-turn still finishes that turn on the old settings (h #g271) and applies at the next turn. Never across accounts, never on the interactive transport (no control channel; it keeps respawning), never on compaction. A refused or unanswered request (`CLAUDE_CODE_LIVE_SWITCH_TIMEOUT_MS`, default 10000) stops the child and falls back to the respawn. Needs `resumeAcrossModelChanges`. `false` restores the respawn and the env-var effort exactly. Log: `switched the live claude process to the new model or effort instead of respawning it` (NOTICE), `could not switch the live claude process; respawning it instead` (NOTICE, with `step` and `reason`). |
175
+ | `resumeAcrossModelChanges` | boolean | `true` | When the operator changes a conversation's model or reasoning effort mid-conversation, continue the same Claude session (`--resume <id> --model <new>`) instead of replaying the thread as text. Both are in the session key, so changing either used to start a fresh Claude conversation (issue #91); neither needs one, because `--model` and `CLAUDE_CODE_EFFORT_LEVEL` are spawn-time and the CLI applies either to a resumed transcript (measured on 2.1.288). The session is MOVED off the old key, so one Claude conversation has exactly one owner. Refused on every term `resumeAfterRestart` is refused on (another account, a transcript that is gone, a conversation that changed) plus two more: a sibling whose transcript may still be written to (that turn is served on the sibling instead and the change applies at the next turn that finds it idle, h #g271), and anything that is not the same cwd, request scope, opencode session and agent. An agent's prompt-cache TTL, `maxTurns` and `maxBudgetUsd` are spawn settings like the effort, so a change to one carries the conversation too, after a restart as well (h #g275); `--max-budget-usd` counts per process, so the carried process starts the new cap from zero, as any `--resume` does. Not on compaction, not on an account-failover switch. It reads the record `resumeAfterRestart` writes, which is also what carries the first change after an opencode restart (h #g274), so `resumeAfterRestart: false` turns it off too. Log: `continuing this conversation's claude session under the new model or effort` (NOTICE). |
163
176
  | `crossAccountResume` | boolean | `true` | On any account switch (a by-hand pick of another account's model in the same session, the switch form, and the switch back when the override expires), and only between accounts in the same `accountGroups` group, continue the same Claude conversation on the other account instead of replaying the thread as text. An account is a `CLAUDE_CONFIG_DIR` and a conversation is one `projects/<encoded cwd>/<id>.jsonl` inside it, so the switch copies that file into the target account's own projects dir (0600, dir 0700) and keeps the session id for the `--resume` both transports already build. Measured on CLI 2.1.288: a copied transcript resumes under another config dir with its context intact, the CLI resolves a session by FILENAME (not the `sessionId` in the records), it appends to the copy it found rather than to the cwd's path, the source comes out byte-identical, and no record holds a user, org or account id. Never overwrites: an older copy already at the target path (what a switch back finds) makes the carry take a fresh session id, and the live copy is the longest one. Refuses on nothing to carry, a non-file at the target path, or a failed copy, and each refusal falls back to the replay. Not on compaction, and inert with one account. Log: `carried this conversation's claude transcript to the other account` or `replaying this conversation as text on the other account` with a reason (both NOTICE). What is NOT verified live is a different Anthropic login answering another's conversation; `false` keeps the replay. |
164
177
  | `usageWarnThresholds` | number[] | `[50, 75, 90, 95]` | Percent levels of a usage window at which a SERVED turn ends with one `▌ **usage:**` note (its own text part, stripped from rebuilt transcripts): account, window, utilization, reset in local time. Read from every headless `rate_limit_event` (not only a rejected one), whose `utilization` is a FRACTION (`0.9` is 90%) and is ABSENT on an `allowed` event with headroom, which is unknown, never zero, and crosses nothing. The same event's `unifiedWindows` carries every window's own utilization and reset (measured on CLI 2.1.293), and each listed window is recorded, so the 7-day window has readings and notes while the 5-hour one limits. Readings persist per account per window in `$XDG_STATE_HOME/opencode-claude-code-plugin/account-usage.json` (`0600`, names, numbers and times only), and so does the dedup: each threshold is said once per account per window across reused processes, sessions and restarts, again only after the window's reset. A crossing in a `/compact` turn or a child session is not said there and is said by the next ordinary turn on the account. Never on `doGenerate`, never turns a served turn into a failure. `[]` turns the notes off but keeps the readings. The single-account name is `default`. Interactive transport: no event, so only a limit reply is recorded (`limited`). Log: `claude account usage crossed a warning threshold` (NOTICE). |
165
178
  | `usageSwitchAt` | number / `false` | `90` | Percent at which work leaves an account BEFORE the wall, when another `failoverCandidates` account (same `accountGroups` group) exists and is not itself at that level per the store. `accountFailover: "auto"`: sets the existing account override (keyed on the account, until the window's reset; in memory, lost on restart) so the NEXT turn spawns on the other account, carried like a limit switch; the note says `New turns move to the "<account>" account until HH:MM`. `"ask"` / `"off"`: nothing moves and no form (the form only ends a FAILED turn); the note names the other account to pick a model from. The turn that crossed finishes as served. Also a threshold in its own right, so the switch is said exactly once per window. `false` or `0` never. Log: `claude account is near its usage limit; new turns move to another account until the window resets` (WARN). |
@@ -220,7 +233,7 @@ their secret values. Arbitrary MCP `{env:NAME}` placeholders are outside this li
220
233
  |---|---|
221
234
  | `CLAUDE_CLI_PATH` | Direct factory fallback for absent `cliPath`. Normal opencode registration supplies `"claude"`; set the option explicitly there. |
222
235
  | `CLAUDE_CONFIG_DIR` | CLI auth/settings/session directory. Non-default account wrappers override it; the default account inherits it if set, on both transports. Never set it to the default `~/.claude` by hand: on CLI 2.1.288 an explicit value, even that one, makes `claude` report itself logged out. Login is a user-approved interactive action, never a diagnostic probe. |
223
- | `CLAUDE_CODE_EFFORT_LEVEL` | Shell-level CLI effort. Request variant/agent effort wins on a normal spawn. Compaction omits request/agent effort, but still inherits the shell env. |
236
+ | `CLAUDE_CODE_EFFORT_LEVEL` | Shell-level CLI effort. Request variant/agent effort wins on a normal spawn. Compaction omits request/agent effort, but still inherits the shell env. A headless spawn that can switch live (`liveModelSwitch`, CLI 2.1.288+) with an effort requested passes `--effort` and removes this variable from the child, because it would silently beat a live effort change (h #g278). |
224
237
  | `CLAUDE_CODE_PROMPT_CACHE_TTL` | Shell-level CLI prompt cache TTL for the main conversation. An agent's `cacheTtl` (or `defaultSubagentCacheTtl`) wins on that agent's spawn; with neither set the plugin writes nothing and the shell value, or the CLI's own default, stands. |
225
238
  | `CLAUDE_CODE_DISABLE_THINKING` | CLI-owned, conventionally `1` to disable thinking. Plugin leaves it intact and suppresses its own thinking flags/summary defaults if enabled. |
226
239
  | `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING` | CLI-owned adaptive-thinking control. Either disable variable suppresses the plugin's own thinking flags/summary defaults, not just adaptive flags. Empty/`0`/`false`/`no`/`off` are false, case-insensitive. |
@@ -231,9 +244,11 @@ their secret values. Arbitrary MCP `{env:NAME}` placeholders are outside this li
231
244
  | `CLAUDE_CODE_INTERACTIVE_BYPASS` | Deprecated no-op, like `interactiveBypass`. |
232
245
  | `CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS` | Set to `0` for the interactive TUI unless the operator set it, so a proxied call past 120 s stays in the foreground as on headless. |
233
246
  | `CLAUDE_CODE_START_WATCHDOG_MS` | Positive integer ms before a headless start or proxy-result continuation is considered silent; default 90000 for missing/invalid/nonpositive values. First expiry respawns, second errors. Bookkeeping-only output is not progress. A process that has not sent its first `system`/`init` yet gets at least its `MCP_TIMEOUT` plus 30 s (150 s by default), since the CLI sends `init` only after its MCP servers connect or time out; the log's `beforeInit` says which delay fired. Keep within timer range; do not lower for routine config checks. |
247
+ | `CLAUDE_CODE_LIVE_SWITCH_TIMEOUT_MS` | Positive integer ms one `set_model` or `apply_flag_settings` may take before a live model or effort switch gives up, stops the child and falls back to the respawn; default 10000 for missing/invalid/nonpositive values. Measured answers: 0.5 to 0.7 s and 20 to 140 ms. |
234
248
  | `CLAUDE_CODE_RESULT_FALLBACK_MS` | Positive integer ms of stdout silence, after the CLI has produced output, before the turn is closed with no `result`; default 60000 for missing/invalid/nonpositive values. The close is announced in the reply as a `▌ **stream timeout:**` note, which is stripped from any rebuilt transcript. An aborted turn gets no note. |
235
249
  | `OPENCODE_CLAUDE_CODE_LOG_FILE` | Overrides `logging.file`: trimmed `0/false/no/off` are false; any other nonempty value is true; empty falls back to config. Prefer `1` or `0`. |
236
250
  | `OPENCODE_CLAUDE_CODE_LOG_DIR` | Overrides `logging.dir`. |
251
+ | `OPENCODE_CLAUDE_CODE_MODEL_PROBE` | `0` or `false` stops the background model probe for this environment (the `npm test` script sets it); the stored answer is still registered. `cliModelCatalog: false` is the config switch and also ignores the stored answer. |
237
252
  | `OPENCODE_CLAUDE_CODE_LOG_LEVEL` | Overrides `logging.level`. Invalid values fall through to config. |
238
253
  | `DEBUG` | A value containing `opencode-claude-code` promotes `logging.mode` to debug, not `logging.level`. Preserve other debug namespaces. |
239
254
  | `OPENCODE_CLAUDE_CODE_PLUGIN_NO_CLEANUP=1` | Skip the removal of a stale unscoped `opencode-claude-code-plugin` install from opencode's package cache. |
@@ -295,10 +310,14 @@ Same package, same config. 2.x's native key is `plugins` (plural), but it still
295
310
  { "plugins": ["@khalilgharbaoui/opencode-claude-code-plugin"] }
296
311
  ```
297
312
 
298
- - Check the major first with `opencode --version`. `plugin` is read by both majors (recorded in `docs/agents-history.md` #g39); `plugins` is read by 2.x only.
313
+ - Check the major first with `opencode --version`. `plugin` is read by both majors (recorded in `docs/agents-history.md` #g39); `plugins` is read by 2.x only. On 2.x, `opencode plugin add @khalilgharbaoui/opencode-claude-code-plugin` writes the `plugins` entry and installs the package.
314
+ - With no `accounts`, 2.x names the provider `Claude Code` (id `claude-code`); 1.x gives it no name and shows the id. Either way a model's full id is `claude-code/<model>`, and `opencode run -m claude-code/claude-haiku-5-5 "reply with ok"` is a one-line check from a terminal.
315
+ - On 2.1.4, `opencode plugin list` prints `No plugins found` even while this plugin is loaded. Never read that as "not installed": check with `/claude-code-doctor` or `opencode models | grep claude-code`.
299
316
  - Provider settings, lowest precedence first: `provider.claude-code.options` (1.x's spelling, still read on 2.x), `provider.claude-code.settings`, `providers.claude-code.settings` (2.x's own), and the plugin entry's own `options`, which wins over all three. Any option of this plugin can sit in any of them; the plugin entry is the usual home for `accounts`: `{"package": "@khalilgharbaoui/opencode-claude-code-plugin", "options": {"accounts": ["work"]}}`.
300
317
  - A local checkout is loaded by pointing `plugins` at its **`dist`** directory, never the repository root: 2.x resolves a configured plugin path as `<dir>/server` or `<dir>/index`.
301
318
  - Known 2.x differences: a `task` call can carry its own `model` (2.x's `subagent` tool takes one; 1.x's `task` does not), there is no todo panel (2.x has no `todowrite` tool, so the sidebar's progress bar is always the time estimate), `bridgeMcpOauthTokens` finds nothing (2.x's plugin API can activate a stored credential but not read one), and Claude's `EnterPlanMode`, `ExitPlanMode` and `NotebookEdit` calls get no tool row (2.x rejects an unknown tool name). Everything else behaves as on 1.x, including `/btw` answered inside the running turn, a permission prompt not counting against a proxied call's deadline, a stopped turn releasing its parked proxied calls, and warnings shown as toasts: the plugin reads busy and idle off opencode 2's event stream (`session.execution.*`) and sends warnings over its plugin RPC channel (measured on 2.0.22 and 2.0.31). Do not set `hostApi`; the 2.x entrypoint sets it, and forcing it on 1.x breaks every proxied tool call.
319
+ - `/connect` on 2.x lists each provider (`Claude Code`, or one per account) under its own name with one method, *Claude Code login (checked with claude auth status)*. Connecting stores no token: the plugin runs `claude auth status` through that provider's own binary or account wrapper, toasts the login method (subscription, API key, Bedrock, Vertex, ...) or the one command that fixes it (`claude auth login`, or `CLAUDE_CONFIG_DIR=~/.claude-<account> claude auth login`), and marks the connection *Sign in required* when the CLI is missing or logged out. It is optional (models work without it) and has no option. Before this, the entry was the raw id with a "Manually enter API Key" method whose key nothing reads; never tell a user to store a key there. Needs CLI 2.1.288 or later for the check; an older CLI gets a "could not check" toast and the connection is left as it was.
320
+ - 2.x would run a failed turn again up to ten times (each retry a new `claude` resuming the conversation and resending the prompt). The plugin declines every retry for its providers, with no option: the CLI retries API errors itself, and usage limits, max turns and refused models were never retried. An error shows once; resend after fixing the cause.
302
321
  - Every form the plugin raises is answered through 2.x's own `Form` surface rather than a question tool, which changes nothing the operator does. The account-switch form and the plan-mode approval were measured live on 2.0.22, and the `subagentDispatch` form (including `Customise…`, `Per type…` accounts and `Same as last time` across a restart) on 2026-10-08. The by-hand account carry and the `accountGroups` guard were measured live there the same day: both behave exactly as on 1.x.
303
322
 
304
323
  #### V2 MCP and Code Mode
@@ -874,16 +893,18 @@ The proxy's loopback endpoint has bearer, Host, Origin and Content-Type guards.
874
893
  Never weaken them, publish its token or relax the generated MCP file's `0600` mode.
875
894
  Restart all old processes after a security upgrade; changing files cannot patch them.
876
895
 
877
- Proxying a tool costs **one extra Claude Code API call per `claude` process**: a proxied
878
- tool is an MCP tool, Claude Code 2.1.280 defers MCP tools behind `ToolSearch`, so the
879
- model spends a request finding it before the first proxied call. Built-ins are never
896
+ Proxying a tool that is not always loaded costs **one extra Claude Code API call per
897
+ `claude` process**: a proxied tool is an MCP tool, Claude Code 2.1.280 defers MCP tools
898
+ behind `ToolSearch`, so the model spends a request finding it before the first proxied
899
+ call. `bash`, `edit` and `write` are always loaded by default (`alwaysLoadProxyTools`) and
900
+ skip it. Built-ins are never
880
901
  deferred. Measured on 2.1.280 (`docs/agents-history.md`, `#g166`): 3 API calls with
881
902
  `Bash` proxied against 2 without, and it is paid once per process, not per call (two
882
903
  commands measured 4 against 3, one `ToolSearch` either way). It is not caused by a large
883
904
  tool list. Never suggest `ENABLE_TOOL_SEARCH=0` to avoid it: that inlines every tool
884
- definition into each prompt and measured 2.5 to 4 times the cost. Say the extra call is
885
- inherent to the CLI's MCP flow and weigh it against the permission prompts and audit log
886
- proxying buys.
905
+ definition into each prompt and measured 2.5 to 4 times the cost. For a hot tool the
906
+ user proxies beyond the default three, add it to `alwaysLoadProxyTools`; otherwise weigh
907
+ the one call against the permission prompts and audit log proxying buys.
887
908
 
888
909
  ### Make a provider read-only
889
910
 
@@ -1177,12 +1198,17 @@ which is redacted by allowlist (see "Filing an issue" below).
1177
1198
 
1178
1199
  A published version does not reach a running opencode. First distinguish an npm pin,
1179
1200
  npm latest resolution, and a local `file://` install. Preserve a pin unless the user
1180
- requested changing it. An `@latest` install is frozen in opencode's package cache at
1181
- `~/.cache/opencode/packages/@khalilgharbaoui/opencode-claude-code-plugin@latest/`, and a
1201
+ requested changing it. An `@latest` install is frozen in opencode's package cache, and a
1182
1202
  plain restart never re-resolves it: removing that one directory and then fully
1183
- relaunching is what picks a new version up. Inspect the actual cache location and
1184
- package identity and get approval before removing only that stale package directory,
1185
- never the whole cache or auth/session directories. Respect platform/XDG paths
1203
+ relaunching is what picks a new version up. The directory differs per major:
1204
+ `~/.cache/opencode/packages/@khalilgharbaoui/opencode-claude-code-plugin@latest/` on
1205
+ opencode 1.x, `~/.cache/opencode/npm/@khalilgharbaoui/opencode-claude-code-plugin@latest/`
1206
+ on opencode 2.x. On opencode 2.1.4 `opencode plugin check` and `opencode plugin update`
1207
+ do not see this plugin, so they cannot refresh it. A registry proxy or security agent
1208
+ with a minimum package age can hide a new release for hours; opencode then caches the
1209
+ older version it resolved, so remove the entry again once the release is visible.
1210
+ Inspect the actual cache location and package identity and get approval before removing
1211
+ only that stale package directory, never the whole cache or auth/session directories. Respect platform/XDG paths
1186
1212
  (`XDG_CACHE_HOME` moves it). Then fully relaunch every opencode window, including serve
1187
1213
  and GUI processes. A `file://` install uses the checkout's
1188
1214
  `dist/`: rebuild with `npm run build`, not cache deletion. On opencode 2, from plugin
@@ -1201,7 +1227,9 @@ Registered ids: `claude-haiku-4-5`, `claude-haiku-5-5`, `claude-sonnet-4-5`, `cl
1201
1227
  `claude-sonnet-5`, `claude-sonnet-5-5`, `claude-opus-4-5`, `claude-opus-4-6`,
1202
1228
  `claude-opus-4-7`, `claude-opus-4-8`, `claude-opus-4-8-fast`, `claude-opus-5`,
1203
1229
  `claude-opus-5-fast`, `claude-opus-5-5`, `claude-opus-5-5-fast`, `claude-fable-5`,
1204
- `claude-fable-5-1`, `claude-mythos-5`, `claude-mythos-5-1`.
1230
+ `claude-fable-5-1`, `claude-mythos-5`, `claude-mythos-5-1`. With `cliModelCatalog` on
1231
+ (the default), a model the installed CLI's own picker offers that this list lacks is
1232
+ registered too, named from the CLI, at an unknown (zero) price.
1205
1233
 
1206
1234
  ### Variants and costs
1207
1235
 
@@ -1209,8 +1237,12 @@ Registered ids: `claude-haiku-4-5`, `claude-haiku-5-5`, `claude-sonnet-4-5`, `cl
1209
1237
  1× haiku 4.5,
1210
1238
  2× sonnet 5 and 5.5, 3× sonnet 4.5/4.6, 4× opus 5.5, 5× other opus, 8× fast-mode opus 5.5, 10× fable, mythos
1211
1239
  and fast-mode opus 5 / 4.8. It is display only.
1212
- - Every model except Haiku 4.5 has reasoning variants `low`, `medium`, `high`, `xhigh`,
1213
- `max`, picked in opencode's model selector. A variant becomes
1240
+ - Reasoning variants are the effort levels Claude Code accepts for that model, because the
1241
+ CLI silently runs a level a model lacks as `high` (read out of 2.1.314): none for Haiku
1242
+ 4.5 and Sonnet 4.5, `low`/`medium`/`high` for Opus 4.5, `low`/`medium`/`high`/`max` for
1243
+ Sonnet 4.6 and Opus 4.6, and all five (`low`, `medium`, `high`, `xhigh`, `max`) for
1244
+ everything newer. With `cliModelCatalog` on they follow the installed CLI's own list.
1245
+ They are picked in opencode's model selector. A variant becomes
1214
1246
  `CLAUDE_CODE_EFFORT_LEVEL` on the spawned CLI unless an agent effort wins. For direct
1215
1247
  AI-SDK calls, `ClaudeCodeCallOptions.reasoningEffort` supports the same levels plus
1216
1248
  `minimal` (mapped to `low`); it is not a provider startup option.
@@ -1242,8 +1274,10 @@ Registered ids: `claude-haiku-4-5`, `claude-haiku-5-5`, `claude-sonnet-4-5`, `cl
1242
1274
  - Registry costs are USD per million tokens, not subscription quota or a billing
1243
1275
  guarantee. Fast entries have fast pricing; other entries use standard rates. There
1244
1276
  is no above-200K tier in this registry; do not invent `cost.tiers` or
1245
- `cost.experimentalOver200K`. 4.5 models have 200K context/64K output, later registered
1246
- models have 1M/128K. Recheck vendor pricing/access separately when changing models.
1277
+ `cost.experimentalOver200K`. 4.5 models have 200K context/64K output; Sonnet 4.6 and
1278
+ Opus 4.6 have 200K/128K, because Claude Code runs them in a 200K window (their 1M
1279
+ window is the `[1m]` id, which is not registered); later models have 1M/128K. With
1280
+ `cliModelCatalog` on the window is the one the installed CLI reports. Recheck vendor pricing/access separately when changing models.
1247
1281
 
1248
1282
  ## Verify and diagnose
1249
1283
 
@@ -1451,12 +1485,14 @@ Prefer the doctor: it needs no logging change and no restart.
1451
1485
  | A config change did nothing | Options are read at startup; another opencode window is still running the old process | Fully quit every opencode window and relaunch |
1452
1486
  | A `▌ **restart opencode:**` note at the top of a reply | Correct and acted on by the user, not by you: this opencode process loaded an older plugin build than the one now on disk, so a fix they installed is not running here. opencode reads a plugin once, at process start | Quit every opencode window (serve and GUI processes included) and relaunch. The note appears once per conversation, names the loaded version, the on-disk version and when the build was loaded, and is stripped from any transcript rebuilt for the CLI |
1453
1487
  | A bug the user is sure was fixed is back | Same cause as the row above, before they have seen the note: the window answering them predates the fix | The `plugin build` row of `/claude-code-doctor`, which reads the disk unthrottled: `current`, `on disk <version>`, `rebuilt on disk` or `could not be read`. Compare its loaded version with the version they installed before investigating the bug at all |
1454
- | New plugin version or model not in the picker after upgrading | Frozen `@latest` in opencode's package cache | Remove the cache dir (recipe "Upgrade the plugin") and relaunch |
1488
+ | New plugin version or model not in the picker after upgrading | Frozen `@latest` in opencode's package cache (`packages/` on 1.x, `npm/` on 2.x), or a minimum-package-age proxy hiding the release so opencode cached an older one | Remove the cache dir for the major in use (recipe "Upgrade the plugin") and relaunch |
1489
+ | `opencode plugin list` prints `No plugins found` on opencode 2 | Expected on 2.1.4: that command, `plugin check` and `plugin update` do not see this plugin | Check with `/claude-code-doctor` or `opencode models \| grep claude-code`; if either answers, the plugin is loaded |
1455
1490
  | No `claude-code` provider or model in the picker at all | The plugin never loaded, or it loaded and the CLI was not usable | Check for a `plugin ready` line first: absent means not loaded (wrong `plugin`/`plugins` key, a 2.x local install not pointing at `dist/`, or no full relaunch), present with `claudeCli.version: not detected` means the binary did not answer `--version`, which also disables every version-gated flag |
1456
1491
  | `Model unavailable` for a model id the user typed | The provider id is not what they assumed | Use the id the ready block's `providers` field lists. With no `accounts` configured on opencode 2 the id is `claude-code`, so `claude-code-default/<model>` fails while the plugin is healthy (measured on opencode 2.0.16, 2026-09-27). Declaring `accounts` is what creates `claude-code-default`; on 1.x with accounts the ids are `claude-code-default` / `claude-code-<name>` and never a bare `claude-code` |
1457
1492
  | `Tool result name changed`, turn aborts, on opencode 2 | Before 0.28.1 a CLI-executed tool's result reached opencode under a different name than its call, and 2.0.16 aborts the turn on the mismatch, breaking every Claude-side MCP server call | Upgrade to 0.28.1+ **and fully relaunch every opencode window**; plugin code is read once at process start, so upgrading the package under a running window changes nothing |
1458
1493
  | A `SessionStart` hook the user configured has no visible effect | It exited non-zero and Claude Code discarded its contribution; the turn still succeeded | Read the **Hooks Claude Code ran that failed** section of `/claude-code-doctor` for the exit code and the hook's stderr. It is their Claude Code settings to fix, not the plugin's |
1459
1494
  | `plugin ready` missing from the log | Logging is off (the default), or the plugin genuinely did not load | Confirm `OPENCODE_CLAUDE_CODE_LOG_FILE=1` and a relaunch before concluding anything. `/claude-code-doctor` answers the same questions with no logging change |
1495
+ | opencode 2's `/connect` shows a Claude Code provider with *Sign in required* | `claude auth status` said that provider's CLI is missing or logged out; the toast after connecting named which and the command | Run the command the toast names in a terminal (`claude auth login`, or with that account's `CLAUDE_CONFIG_DIR`), then connect again. Connecting is optional: models work without it |
1460
1496
  | "Failed to authenticate: OAuth session expired", one account, turns failing in milliseconds | That account's CLI login lapsed | `claude auth status` for it, then log in again with the command the `▌ **claude account:**` note prints (`CLAUDE_CONFIG_DIR=<that account's dir> claude auth login`). Restart opencode after: a switch taken from the failover form lasts until restart. Login is a user action, never a diagnostic probe |
1461
1497
  | A tool call reported as rejected although it ran | Two fixed causes: opencode 1.18.32 aborts the provider signal of every step ending in tool calls, read as an operator stop (0.26.1); and a call waiting on an unanswered permission prompt was rejected at the flat 10-minute deadline, after which the late approval cancelled Claude's next call (0.26.2) | Upgrade to 0.26.2+ and relaunch. Do NOT raise `proxyToolTimeoutMs` for this: a deadline now waits while opencode reports the session busy |
1462
1498
  | `proxy call still waiting` in the log, or a `task` that looks stuck | Expected: `task`/`task_batch` carry no default deadline, and the line is a status report | `/claude-code-doctor` lists pending calls with tool, age and deadline. Tell a working subagent from a wedged one there before proposing any timeout change; see the note under "Proxy tool names" |