@khalilgharbaoui/opencode-claude-code-plugin 0.33.0 → 0.33.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
@@ -21,7 +21,7 @@ Three ways to reach Claude from opencode. They differ in who authenticates, who
21
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
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
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. | 17 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. |
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
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
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
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. |
@@ -102,7 +102,7 @@ The same package runs on opencode 1.x and 2.x, and nothing changes for 1.x. The
102
102
  }
103
103
  ```
104
104
 
105
- Your existing `provider.claude-code.options` block keeps working, because opencode 2 still reads 1.x config files. Its native spelling is `provider.claude-code.settings`, and plugin-level settings such as `accounts` may also go in the plugin entry itself: `{"package": "@khalilgharbaoui/opencode-claude-code-plugin", "options": {"accounts": ["work"]}}`.
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
106
 
107
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
108
 
@@ -470,7 +470,7 @@ reaches the CLI, so there is nothing there to fall back from.
470
470
  | `strictMcpConfig` | boolean | `false` | Pass `--strict-mcp-config` so Claude loads **only** the configured servers and ignores `~/.claude/settings.json`. |
471
471
  | `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 or disabled becomes visible without restarting opencode or opening a new chat. Eviction waits for pending proxy calls, never happening mid tool-call, and the session id is preserved for `--resume`. 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`. |
472
472
  | `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. |
473
- | `proxyOpencodeTools` | string[] | `[]` | Forward named opencode tools through the proxy by their registry id, for tools another opencode plugin declares directly and that therefore belong to no MCP server (opencode-dcp's `compress`). Explicit allowlist; 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 rather than taking it over. See [Forwarding opencode's own tools](#forwarding-opencode-s-own-tools). |
473
+ | `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). |
474
474
  | `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). |
475
475
  | `webSearch` | `"claude"` \| `"disabled"` \| `<tool>` | `"claude"` | Routing for Claude's built-in `WebSearch`. See [WebSearch routing](#websearch-routing). |
476
476
  | `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. |
@@ -1063,16 +1063,39 @@ Claude Code ships a built-in `WebSearch` tool. The `webSearch` option controls w
1063
1063
 
1064
1064
  ## MCP bridge
1065
1065
 
1066
- If `bridgeOpencodeMcp` is true (the default), the plugin reads your opencode config's `mcp` block, translates it into Claude's MCP schema, writes it to a temp file, and passes that to `claude --mcp-config`. So whatever MCP servers you've already configured in opencode become available to Claude with no extra setup.
1066
+ 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.
1067
1067
 
1068
- ### Discovery order (highest to lowest priority)
1068
+ ### Discovery and precedence
1069
1069
 
1070
- 1. `OPENCODE_CONFIG` env var (file path)
1071
- 2. `OPENCODE_CONFIG_DIR` env var
1072
- 3. Walk up from the current `cwd` looking for `opencode.jsonc`, `opencode.json`, `config.json`, or a `.opencode/` directory
1073
- 4. Global `$XDG_CONFIG_HOME/opencode` or `~/.config/opencode`
1070
+ 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.
1074
1071
 
1075
- Later sources override earlier ones **by server name**, so a project-level MCP server replaces a global one with the same id.
1072
+ 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.
1073
+
1074
+ ### V2 Code Mode
1075
+
1076
+ 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.
1077
+
1078
+ To explicitly opt into Code Mode through opencode instead, use these provider settings (headless transport):
1079
+
1080
+ ```json
1081
+ {
1082
+ "providers": {
1083
+ "claude-code": {
1084
+ "settings": {
1085
+ "proxyOpencodeTools": ["execute"],
1086
+ "bridgeOpencodeMcp": false,
1087
+ "strictMcpConfig": true
1088
+ }
1089
+ }
1090
+ }
1091
+ }
1092
+ ```
1093
+
1094
+ 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.
1095
+
1096
+ `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.
1097
+
1098
+ 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.
1076
1099
 
1077
1100
  ### Translation
1078
1101
 
@@ -1636,7 +1659,7 @@ This plugin absorbs work from its forks directly, cherry-picked with the origina
1636
1659
  | [@willmcginnis](https://github.com/willmcginnis) | The proxy endpoint authentication (PR #28, GHSA-3mxm-w7gf-3c5x). | PR #28 |
1637
1660
  | [@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 |
1638
1661
  | [@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 |
1639
- | [@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). | PR #62 |
1662
+ | [@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 |
1640
1663
  | [@JWebCoder](https://github.com/JWebCoder) (joao moura) | Diagnosed that auto-continue never fires on current CLIs (PR #15). | PR #15 |
1641
1664
 
1642
1665
  Commit hashes are on the contributors' forks where the work was cherry-picked; `git log --author` on this repo shows the preserved authorship.
package/dist/index.d.ts CHANGED
@@ -1230,6 +1230,7 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
1230
1230
  private doStreamForHost;
1231
1231
  }
1232
1232
 
1233
+ type McpHostApi = "v1" | "v2";
1233
1234
  interface BridgedMcp {
1234
1235
  /** Path to the temp file containing the translated `--mcp-config`. */
1235
1236
  path: string;
@@ -1270,7 +1271,7 @@ type RuntimeMcpStatus = Record<string, string>;
1270
1271
  * return its path + a stable hash. Returns null when no enabled MCP servers
1271
1272
  * remain after the merge + overlay.
1272
1273
  */
1273
- declare function bridgeOpencodeMcp(cwd: string, runtimeStatus?: RuntimeMcpStatus, excludeServers?: ReadonlySet<string>): BridgedMcp | null;
1274
+ declare function bridgeOpencodeMcp(cwd: string, runtimeStatus?: RuntimeMcpStatus, excludeServers?: ReadonlySet<string>, hostApi?: McpHostApi): BridgedMcp | null;
1274
1275
 
1275
1276
  type AgentRecord = {
1276
1277
  mode?: string;
package/dist/index.js CHANGED
@@ -2434,7 +2434,9 @@ function resolveDisallowedTools(options) {
2434
2434
  function resolveProxyOpencodeToolDefs(options) {
2435
2435
  const requested = options.requested ?? [];
2436
2436
  if (requested.length === 0) return [];
2437
- const items = options.items;
2437
+ const items = options.modelTools !== void 0 ? options.modelTools.flatMap(
2438
+ (tool) => (tool.type === void 0 || tool.type === "function") && typeof tool.name === "string" ? [{ id: tool.name, description: tool.description, parameters: tool.inputSchema }] : []
2439
+ ) : options.items;
2438
2440
  if (!items) {
2439
2441
  log.warn(
2440
2442
  "proxyOpencodeTools is set but opencode's tool registry did not answer; forwarding nothing this spawn",
@@ -2460,12 +2462,20 @@ function resolveProxyOpencodeToolDefs(options) {
2460
2462
  unknown.push(name);
2461
2463
  continue;
2462
2464
  }
2465
+ if (item.id === "execute" && options.allowCodeExecution === false) {
2466
+ log.warn("Code Mode execute not forwarded: read-only permission preset");
2467
+ continue;
2468
+ }
2463
2469
  if (taken.has(item.id)) {
2464
2470
  collided.push(item.id);
2465
2471
  continue;
2466
2472
  }
2467
2473
  if (seen.has(item.id)) continue;
2468
2474
  seen.add(item.id);
2475
+ if (item.id === "execute" && (!item.parameters || typeof item.parameters !== "object" || Array.isArray(item.parameters) || !Object.keys(item.parameters).length)) {
2476
+ log.warn("Code Mode execute not forwarded: no model-visible input schema");
2477
+ continue;
2478
+ }
2469
2479
  out.push({
2470
2480
  name: item.id,
2471
2481
  description: typeof item.description === "string" ? item.description : "",
@@ -2502,6 +2512,7 @@ function resolveMcpProxyToolDefs(options) {
2502
2512
  if (tool?.type !== void 0 && tool.type !== "function") continue;
2503
2513
  const name = typeof tool?.name === "string" ? tool.name.trim() : "";
2504
2514
  if (!name) continue;
2515
+ if (name === "execute") continue;
2505
2516
  const matchedServer = serversByLengthDesc.find(
2506
2517
  (server2) => name === server2 || name.startsWith(`${server2}_`)
2507
2518
  );
@@ -4709,24 +4720,24 @@ function globalConfigDir() {
4709
4720
  const xdg = process.env.XDG_CONFIG_HOME ?? path4.join(os2.homedir(), ".config");
4710
4721
  return path4.join(xdg, "opencode");
4711
4722
  }
4712
- function loadGlobalConfig() {
4723
+ function loadGlobalConfig(hostApi = "v1") {
4713
4724
  const dir = globalConfigDir();
4714
4725
  let merged = {};
4715
4726
  for (const name of FILE_NAMES.slice().reverse()) {
4716
4727
  const file = path4.join(dir, name);
4717
4728
  if (!fileExists(file)) continue;
4718
4729
  const parsed = readAndParse(file);
4719
- if (parsed) merged = deepMerge(merged, parsed);
4730
+ if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
4720
4731
  }
4721
4732
  return merged;
4722
4733
  }
4723
- function loadProjectFilesInDir(dir) {
4734
+ function loadProjectFilesInDir(dir, hostApi = "v1") {
4724
4735
  let merged = {};
4725
4736
  for (const name of PROJECT_FILE_NAMES) {
4726
4737
  const file = path4.join(dir, name);
4727
4738
  if (!fileExists(file)) continue;
4728
4739
  const parsed = readAndParse(file);
4729
- if (parsed) merged = deepMerge(merged, parsed);
4740
+ if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
4730
4741
  }
4731
4742
  return merged;
4732
4743
  }
@@ -4813,8 +4824,29 @@ function translateServer(name, spec) {
4813
4824
  }
4814
4825
  function extractMcpBlock(config) {
4815
4826
  const mcp = config.mcp;
4816
- if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) return {};
4817
- return mcp;
4827
+ if (!isPlainObject(mcp)) return {};
4828
+ if ("servers" in mcp && !isPlainObject(mcp.servers)) return {};
4829
+ const native = isPlainObject(mcp.servers) && typeof mcp.servers.type !== "string" && typeof mcp.servers.enabled !== "boolean" && typeof mcp.servers.disabled !== "boolean";
4830
+ const block = native ? mcp.servers : mcp;
4831
+ return Object.fromEntries(
4832
+ Object.entries(block).filter(([name, spec]) => isPlainObject(spec) && (native || name !== "timeout" || typeof spec.type === "string")).map(([name, spec]) => {
4833
+ const server2 = spec;
4834
+ return [name, {
4835
+ ...server2,
4836
+ ...typeof server2.disabled === "boolean" ? { enabled: !server2.disabled } : {}
4837
+ }];
4838
+ })
4839
+ );
4840
+ }
4841
+ function mergeConfigLayer(target, source, hostApi) {
4842
+ const merged = deepMerge(target, source);
4843
+ if (hostApi === "v2" && isPlainObject(source.mcp)) {
4844
+ merged.mcp = {
4845
+ ...isPlainObject(merged.mcp) ? merged.mcp : {},
4846
+ servers: { ...extractMcpBlock(target), ...extractMcpBlock(source) }
4847
+ };
4848
+ }
4849
+ return merged;
4818
4850
  }
4819
4851
  function mergeMcp(target, source) {
4820
4852
  const out = { ...target };
@@ -4832,12 +4864,12 @@ function mergeMcp(target, source) {
4832
4864
  }
4833
4865
  return out;
4834
4866
  }
4835
- function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
4867
+ function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers, hostApi = "v1") {
4836
4868
  const {
4837
4869
  servers: merged,
4838
4870
  enabledServerNames: allEnabledServerNames,
4839
4871
  hash
4840
- } = mergeOpencodeMcp(cwd, runtimeStatus);
4872
+ } = mergeOpencodeMcp(cwd, runtimeStatus, hostApi);
4841
4873
  const servers = {};
4842
4874
  const bridgedServerNames = [];
4843
4875
  for (const [name, spec] of Object.entries(merged)) {
@@ -4857,10 +4889,10 @@ function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
4857
4889
  excludeServers
4858
4890
  });
4859
4891
  }
4860
- function opencodeConfigLayers(cwd) {
4861
- const worktree = detectWorktree(cwd);
4892
+ function opencodeConfigLayers(cwd, hostApi = "v1") {
4893
+ const worktree = hostApi === "v2" ? void 0 : detectWorktree(cwd);
4862
4894
  const layers = [];
4863
- layers.push(loadGlobalConfig());
4895
+ layers.push(loadGlobalConfig(hostApi));
4864
4896
  const explicitConfig = process.env.OPENCODE_CONFIG;
4865
4897
  if (explicitConfig && fileExists(explicitConfig)) {
4866
4898
  const parsed = readAndParse(explicitConfig);
@@ -4882,22 +4914,30 @@ function opencodeConfigLayers(cwd) {
4882
4914
  }
4883
4915
  }
4884
4916
  for (const dir of projectDirs.slice().reverse()) {
4885
- layers.push(loadProjectFilesInDir(dir));
4917
+ layers.push(loadProjectFilesInDir(dir, hostApi));
4886
4918
  }
4887
- for (const dir of dotOpencodeDirs(cwd, worktree)) {
4888
- layers.push(loadProjectFilesInDir(dir));
4919
+ if (hostApi === "v2") {
4920
+ const dirs = walkUp({ start: cwd, targets: [".opencode"], predicate: dirExists }).reverse();
4921
+ const extra = process.env.OPENCODE_CONFIG_DIR;
4922
+ if (extra && dirExists(extra)) layers.push(loadProjectFilesInDir(extra, hostApi));
4923
+ for (const dir of dirs) layers.push(loadProjectFilesInDir(dir, hostApi));
4924
+ } else {
4925
+ for (const dir of dotOpencodeDirs(cwd, worktree)) {
4926
+ layers.push(loadProjectFilesInDir(dir));
4927
+ }
4889
4928
  }
4890
4929
  return layers;
4891
4930
  }
4892
- function loadMergedOpencodeConfig(cwd) {
4931
+ function loadMergedOpencodeConfig(cwd, hostApi = "v1") {
4893
4932
  let merged = {};
4894
- for (const layer of opencodeConfigLayers(cwd)) merged = deepMerge(merged, layer);
4933
+ for (const layer of opencodeConfigLayers(cwd, hostApi)) merged = mergeConfigLayer(merged, layer, hostApi);
4895
4934
  return merged;
4896
4935
  }
4897
- function mergeOpencodeMcp(cwd, runtimeStatus) {
4936
+ function mergeOpencodeMcp(cwd, runtimeStatus, hostApi = "v1") {
4898
4937
  let merged = {};
4899
- for (const layer of opencodeConfigLayers(cwd)) {
4900
- merged = mergeMcp(merged, extractMcpBlock(layer));
4938
+ for (const layer of opencodeConfigLayers(cwd, hostApi)) {
4939
+ const servers = extractMcpBlock(layer);
4940
+ merged = hostApi === "v2" ? { ...merged, ...servers } : mergeMcp(merged, servers);
4901
4941
  }
4902
4942
  if (runtimeStatus) {
4903
4943
  for (const name of Object.keys(merged)) {
@@ -5037,7 +5077,8 @@ function collectStartupDiagnostics(providers, opencodeVersion) {
5037
5077
  const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
5038
5078
  let mcpServers = [];
5039
5079
  try {
5040
- mcpServers = mergeOpencodeMcp(cwd.resolved).enabledServerNames;
5080
+ const hostApi = firstOption(providers, "hostApi") === "v2" || opencodeVersion?.replace(/^v/, "").startsWith("2.") ? "v2" : "v1";
5081
+ mcpServers = mergeOpencodeMcp(cwd.resolved, void 0, hostApi).enabledServerNames;
5041
5082
  } catch (err) {
5042
5083
  log.debug("startup diagnostics could not read MCP config", {
5043
5084
  error: err instanceof Error ? err.message : String(err)
@@ -7609,6 +7650,13 @@ Structured questions in this environment go through exactly one tool: \`mcp__ope
7609
7650
  - If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__question\`), then call it by its FULL name.
7610
7651
  - Do NOT call bare \`question\` \u2014 that is not a tool. Always use the full \`mcp__opencode_proxy__question\` name when invoking it.
7611
7652
  - Claude Code's built-in \`AskUserQuestion\` is disabled in this environment; the proxy is the only way to ask structured questions.`;
7653
+ function codeModeProxyHint(active) {
7654
+ return active ? `## opencode Code Mode
7655
+
7656
+ Instructions referring to the execute tool mean \`mcp__opencode_proxy__execute\` in this Claude Code session. Use its full name; if deferred, load it with ToolSearch (\`select:mcp__opencode_proxy__execute\`). Pass the code argument unchanged, using the provided catalog's exact \`search(...)\` and \`tools[...]\` signatures inside that code. The catalog tools are not separate Claude tools. Calls execute in opencode under this session's permissions.` : `## opencode Code Mode unavailable
7657
+
7658
+ The forwarded opencode catalog may mention execute, but this session does not expose an execute proxy. Do not invent an execute tool or claim a catalog call ran. For MCP servers available directly in Claude Code, discover their actual MCP tools with ToolSearch instead. If no matching tool exists, report that limitation; a shell/CLI call is not a test of MCP.`;
7659
+ }
7612
7660
  var CLAUDE_CLI_CONTEXT_NOTE = `## Runtime environment: Claude Code CLI
7613
7661
 
7614
7662
  You are running via the Claude Code CLI (not a direct API call). This affects context management:
@@ -8094,7 +8142,7 @@ function effectiveMcpConfig(config, cwd, proxyConfigPath, runtimeStatus, exclude
8094
8142
  let bridgedHash = null;
8095
8143
  let allEnabledServerNames = [];
8096
8144
  if (config.bridgeOpencodeMcp !== false) {
8097
- const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers);
8145
+ const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers, config.hostApi);
8098
8146
  if (bridged) {
8099
8147
  if (bridged.path) paths.push(bridged.path);
8100
8148
  bridgedHash = bridged.hash;
@@ -8154,7 +8202,7 @@ function resolvedProxyMcpTools(config, allEnabledServerNames, modelTools, taken)
8154
8202
  });
8155
8203
  if (resolution.defs.length === 0) {
8156
8204
  log.warn(
8157
- "proxyOpencodeMcpTools is on but no MCP tool was found in opencode's tool set; those servers stay on the direct bridge this spawn",
8205
+ "proxyOpencodeMcpTools is on but no MCP tool was found in opencode's tool set; those servers stay on the direct bridge this spawn" + (config.hostApi === "v2" && modelTools?.some((tool) => tool.name === "execute") ? ". For V2 Code Mode, explicitly allowlist execute in proxyOpencodeTools and use bridgeOpencodeMcp: false with strictMcpConfig: true" : ""),
8158
8206
  { servers: allEnabledServerNames, modelTools: modelTools?.length ?? 0 }
8159
8207
  );
8160
8208
  return null;
@@ -10692,6 +10740,8 @@ var ClaudeCodeLanguageModel = class {
10692
10740
  const opencodeToolDefs = resolveProxyOpencodeToolDefs({
10693
10741
  requested: self.config.proxyOpencodeTools,
10694
10742
  items: liveToolInfo.items,
10743
+ modelTools: self.config.hostApi === "v2" ? options.tools ?? [] : void 0,
10744
+ allowCodeExecution: self.config.permissionPreset !== "read-only",
10695
10745
  taken: new Set(
10696
10746
  [...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
10697
10747
  (t) => t.name
@@ -10737,7 +10787,8 @@ var ClaudeCodeLanguageModel = class {
10737
10787
  ...extractSystemMessages(options.prompt),
10738
10788
  ...taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
10739
10789
  ...backgroundSubagentsSupported ? [BACKGROUND_SUBAGENT_HINT] : [],
10740
- ...questionProxyActive ? [QUESTION_PROXY_HINT] : []
10790
+ ...questionProxyActive ? [QUESTION_PROXY_HINT] : [],
10791
+ ...self.config.hostApi === "v2" && options.tools?.some((t) => t.name === "execute") ? [codeModeProxyHint(opencodeToolDefs.some((t) => t.name === "execute"))] : []
10741
10792
  ],
10742
10793
  {
10743
10794
  compressEnabled: pluginCompressEnabled,
@@ -11545,7 +11596,8 @@ function createV1ClientShim(ctx) {
11545
11596
  client.tool = {
11546
11597
  // V1: `{ data: [{ id, description, parameters }] }`. V2 returns the tools
11547
11598
  // themselves, with an Effect schema for input rather than JSON Schema, so
11548
- // `parameters` is empty; only `proxyOpencodeTools` reads it.
11599
+ // `parameters` is empty. V2 proxyOpencodeTools uses the per-turn model
11600
+ // snapshot instead, which also includes synthesized Code Mode execute.
11549
11601
  list: async () => {
11550
11602
  const tools = await toolList.call(ctx.tool);
11551
11603
  const data = (tools ?? []).flatMap((tool) => {
@@ -11726,9 +11778,11 @@ function plainObject(value) {
11726
11778
  }
11727
11779
  function configuredSeedSettings(config, pluginOptions) {
11728
11780
  const provider = plainObject(plainObject(config.provider)?.[BASE_PROVIDER_ID]);
11781
+ const nativeProvider = plainObject(plainObject(config.providers)?.[BASE_PROVIDER_ID]);
11729
11782
  return {
11730
11783
  ...plainObject(provider?.options) ?? {},
11731
11784
  ...plainObject(provider?.settings) ?? {},
11785
+ ...plainObject(nativeProvider?.settings) ?? {},
11732
11786
  ...pluginOptions ?? {}
11733
11787
  };
11734
11788
  }
@@ -12376,7 +12430,7 @@ var index_default = {
12376
12430
  setup: createV2Setup({
12377
12431
  createProvider: createClaudeCode,
12378
12432
  defaultProxyTools: DEFAULT_PROXY_TOOL_NAMES,
12379
- loadConfig: loadMergedOpencodeConfig,
12433
+ loadConfig: (directory) => loadMergedOpencodeConfig(directory, "v2"),
12380
12434
  buildAgentRegistry: (config) => buildAgentRegistry(config)
12381
12435
  })
12382
12436
  };