@khalilgharbaoui/opencode-claude-code-plugin 0.21.0 → 0.22.1

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
@@ -297,7 +297,7 @@ model: claude-code-work/claude-opus-5@work
297
297
  | `mcpConfig` | string \| string[] | – | Extra `--mcp-config` paths/JSON passed alongside the bridged config. |
298
298
  | `strictMcpConfig` | boolean | `false` | Pass `--strict-mcp-config` so Claude loads **only** the configured servers and ignores `~/.claude/settings.json`. |
299
299
  | `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`. |
300
- | `proxyOpencodeMcpTools` | boolean | `true` | **Currently has no effect on opencode 1.18.31: its discovery step finds nothing to route.** Measured on 2026-09-19 with five MCP servers connected, opencode's tool registry returned only built-in and plugin-declared tools, so the server-name match this option depends on matches nothing. Your MCP servers still work, through the direct `--mcp-config` bridge; what is lost is only the routing of those calls through opencode's permissions and rendering. Left on by default because it is harmless and will resume working if the registry regains them. To forward a specific non-MCP opencode tool meanwhile, use [`proxyOpencodeTools`](#options-reference). Original intent: route the MCP tools discovered from opencode through the in-process `opencode_proxy` server instead of bridging them straight into Claude's `--mcp-config`. With both layers pointed at the same server, direct bridging executes every call twice, once in Claude's own MCP child process and once in opencode; proxying keeps opencode as the single execution site while preserving its permission prompts and tool rows. Falls back to direct bridging when discovery is unavailable, so do not treat it as an exactly-once guarantee for write-capable tools. |
300
+ | `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. |
301
301
  | `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). |
302
302
  | `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). |
303
303
  | `webSearch` | `"claude"` \| `"disabled"` \| `<tool>` | `"claude"` | Routing for Claude's built-in `WebSearch`. See [WebSearch routing](#websearch-routing). |
@@ -504,7 +504,7 @@ Without `"Task"` in `proxyTools`, Claude's built-in `Agent` tool stays enabled a
504
504
 
505
505
  ### Forwarding opencode's own tools
506
506
 
507
- `proxyTools` names the tools this plugin ships defs for, and MCP-backed opencode tools are routed automatically ([`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.
507
+ `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.
508
508
 
509
509
  `proxyOpencodeTools` is the explicit allowlist. Empty by default:
510
510
 
@@ -609,7 +609,7 @@ A proxied call ends when something happens to it, not when a clock runs out. The
609
609
 
610
610
  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.
611
611
 
612
- 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 are not reported this way, because their deadline already does it. 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.
612
+ 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.
613
613
 
614
614
  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.
615
615
 
package/dist/index.d.ts CHANGED
@@ -487,16 +487,28 @@ interface ClaudeCodeProviderSettings {
487
487
  /**
488
488
  * Route opencode MCP server tools through the in-process `opencode_proxy`
489
489
  * MCP server instead of bridging them directly into Claude CLI's
490
- * `--mcp-config`. With both layers configured for the same MCP server,
491
- * direct bridging causes each tool invocation to execute twice — once by
492
- * Claude CLI's own MCP child process and once by opencode. Routing through
493
- * the proxy keeps a single execution site (opencode) while preserving the
494
- * tool-call/result surface in opencode's UI and its permission prompts.
490
+ * `--mcp-config`. Routing through the proxy keeps a single execution site
491
+ * (opencode), so the call is permission-prompted and rendered as an
492
+ * opencode tool call instead of running inside Claude CLI's own MCP child.
495
493
  *
496
- * Defaults to `true`. Set to `false` to restore the prior direct-bridge
497
- * behavior (Claude CLI executes MCP tools itself; opencode also re-executes
498
- * — accept the duplication if you need Claude to invoke the tool without
499
- * an opencode round-trip).
494
+ * Defaults to `false`, and that is a change of default rather than of
495
+ * behaviour. It used to default to `true` while routing nothing at all:
496
+ * discovery read `client.tool.list()`, which enumerates opencode's tool
497
+ * registry (built-ins plus plugin-declared tools) and has never contained
498
+ * an MCP tool, so no def was ever built. Discovery now reads the model tool
499
+ * set opencode passes the provider, which is where MCP tools actually live,
500
+ * so the option works. Leaving it on by default would then have silently
501
+ * moved every existing user's MCP traffic off the direct bridge that is
502
+ * carrying it today, so switching over is the operator's call.
503
+ *
504
+ * Two things to know before enabling it:
505
+ *
506
+ * - It only affects the servers this plugin bridges. If the same server is
507
+ * also registered in Claude Code's own config, Claude reaches it directly
508
+ * and the proxy is bypassed. Pair this with `strictMcpConfig: true` so
509
+ * Claude sees only the config this plugin writes.
510
+ * - A routed call executes inside opencode with the calling agent's
511
+ * permissions, the same trade `proxyOpencodeTools` makes.
500
512
  */
501
513
  proxyOpencodeMcpTools?: boolean;
502
514
  /**
@@ -693,13 +705,24 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
693
705
  /** Resolve ProxyToolDef[] for the configured proxyTools names. */
694
706
  private resolvedProxyTools;
695
707
  /**
696
- * Resolve ProxyToolDef[] for opencode's MCP-bridged tools so they go
708
+ * Resolve ProxyToolDef[] for opencode's MCP-backed tools so they go
697
709
  * through the in-process proxy instead of being bridged into Claude CLI's
698
- * `--mcp-config`. Direct bridging causes double execution because both
699
- * Claude CLI's own MCP child and opencode hold their own connection to
700
- * the same server; routing through the proxy keeps a single execution
701
- * site (opencode). Returns null when the feature is disabled, the SDK
702
- * client is unavailable, or no MCP servers are configured.
710
+ * `--mcp-config`. Routing through the proxy keeps a single execution site
711
+ * (opencode), so the call is permission-prompted and rendered as an
712
+ * opencode tool call.
713
+ *
714
+ * Opt-in (`proxyOpencodeMcpTools: true`) and off by default. It used to
715
+ * default to true while finding nothing, because it discovered tools via
716
+ * `client.tool.list()`, which enumerates opencode's `ToolRegistry` and not
717
+ * the MCP tools merged into the model's tool set afterwards. Discovery now
718
+ * reads that merged set, the `tools` array opencode passes `doStream`, so
719
+ * the option does what it says. Turning it on by default at the same time
720
+ * would have silently moved every existing user's MCP traffic off the
721
+ * working direct bridge, so the default went to false instead: today's
722
+ * behaviour is preserved exactly and crossing over is the operator's call.
723
+ *
724
+ * Returns null when the feature is off or nothing matched, which leaves
725
+ * every server on the direct bridge.
703
726
  */
704
727
  private resolvedProxyMcpTools;
705
728
  /**
package/dist/index.js CHANGED
@@ -1303,6 +1303,47 @@ function resolveProxyOpencodeToolDefs(options) {
1303
1303
  }
1304
1304
  return out;
1305
1305
  }
1306
+ function resolveMcpProxyToolDefs(options) {
1307
+ const empty = { defs: [], coveredServers: /* @__PURE__ */ new Set() };
1308
+ const serverNames = options.serverNames ?? [];
1309
+ if (serverNames.length === 0) return empty;
1310
+ const tools = options.tools;
1311
+ if (!tools || tools.length === 0) return empty;
1312
+ const serversByLengthDesc = [...serverNames].sort((a, b) => b.length - a.length);
1313
+ const taken = options.taken ?? /* @__PURE__ */ new Set();
1314
+ const defs = [];
1315
+ const coveredServers = /* @__PURE__ */ new Set();
1316
+ const seen = /* @__PURE__ */ new Set();
1317
+ const collided = [];
1318
+ for (const tool of tools) {
1319
+ if (tool?.type !== void 0 && tool.type !== "function") continue;
1320
+ const name = typeof tool?.name === "string" ? tool.name.trim() : "";
1321
+ if (!name) continue;
1322
+ const matchedServer = serversByLengthDesc.find(
1323
+ (server2) => name === server2 || name.startsWith(`${server2}_`)
1324
+ );
1325
+ if (!matchedServer) continue;
1326
+ if (seen.has(name)) continue;
1327
+ if (taken.has(name)) {
1328
+ collided.push(name);
1329
+ continue;
1330
+ }
1331
+ seen.add(name);
1332
+ coveredServers.add(matchedServer);
1333
+ defs.push({
1334
+ name,
1335
+ description: typeof tool.description === "string" ? tool.description : "",
1336
+ inputSchema: tool.inputSchema && typeof tool.inputSchema === "object" ? tool.inputSchema : { type: "object", properties: {} }
1337
+ });
1338
+ }
1339
+ if (collided.length > 0) {
1340
+ log.warn(
1341
+ "MCP tool not routed through the proxy: another proxy tool already holds that name, and it keeps it",
1342
+ { collided }
1343
+ );
1344
+ }
1345
+ return { defs, coveredServers };
1346
+ }
1306
1347
  function readBody(req) {
1307
1348
  return new Promise((resolve5, reject) => {
1308
1349
  const chunks = [];
@@ -1392,9 +1433,12 @@ function writeJson(res, body) {
1392
1433
  // src/proxy-broker.ts
1393
1434
  import { EventEmitter as EventEmitter2 } from "events";
1394
1435
  var PROXY_STALL_WARNING_MS = 5 * 6e4;
1436
+ var PROXY_DEADLINE_WARNING_FRACTION = 0.6;
1437
+ var PROXY_DEADLINE_WARNING_MIN_MS = 6e4;
1395
1438
  function clearPendingTimers(pending) {
1396
1439
  if (pending.timer) clearTimeout(pending.timer);
1397
1440
  if (pending.stallTimer) clearInterval(pending.stallTimer);
1441
+ if (pending.deadlineWarnTimer) clearTimeout(pending.deadlineWarnTimer);
1398
1442
  }
1399
1443
  var pendingByCallId = /* @__PURE__ */ new Map();
1400
1444
  var callIdsBySession = /* @__PURE__ */ new Map();
@@ -1421,7 +1465,7 @@ function onPendingProxyCall(sessionKey2, handler) {
1421
1465
  emitter.on(name, handler);
1422
1466
  return () => emitter.off(name, handler);
1423
1467
  }
1424
- function queuePendingProxyCall(sessionKey2, call, timeoutOverrides, stallWarningMs = PROXY_STALL_WARNING_MS) {
1468
+ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides, stallWarningMs = PROXY_STALL_WARNING_MS, deadlineWarningMinMs = PROXY_DEADLINE_WARNING_MIN_MS) {
1425
1469
  const previous = pendingByCallId.get(call.id);
1426
1470
  if (previous) {
1427
1471
  clearPendingTimers(previous);
@@ -1464,6 +1508,24 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides, stallWarning
1464
1508
  });
1465
1509
  }, stallWarningMs) : null;
1466
1510
  stallTimer?.unref?.();
1511
+ const warnAtMs = Math.floor(deadlineMs * PROXY_DEADLINE_WARNING_FRACTION);
1512
+ const deadlineWarnTimer = deadlineMs >= deadlineWarningMinMs && deadlineMs > 0 && warnAtMs > 0 ? setTimeout(() => {
1513
+ const current = pendingByCallId.get(call.id);
1514
+ if (!current) return;
1515
+ const waitedMs = Date.now() - current.createdAt;
1516
+ log.warn("proxy call still waiting, deadline approaching", {
1517
+ sessionKey: current.sessionKey,
1518
+ toolCallId: current.toolCallId,
1519
+ toolName: current.toolName,
1520
+ waitedMs,
1521
+ deadlineMs,
1522
+ remainingMs: Math.max(0, deadlineMs - waitedMs),
1523
+ emitted: current.emitted === true,
1524
+ channelClosed: current.channel?.closed === true,
1525
+ note: "it will be rejected when the deadline passes; raise this tool's proxyToolTimeoutMs if the work is legitimately this long"
1526
+ });
1527
+ }, warnAtMs) : null;
1528
+ deadlineWarnTimer?.unref?.();
1467
1529
  const pending = {
1468
1530
  sessionKey: sessionKey2,
1469
1531
  toolCallId: call.id,
@@ -1474,6 +1536,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides, stallWarning
1474
1536
  deadlineMs,
1475
1537
  timer,
1476
1538
  stallTimer,
1539
+ deadlineWarnTimer,
1477
1540
  resolve: call.resolve,
1478
1541
  reject: call.reject
1479
1542
  };
@@ -3333,9 +3396,10 @@ function finishBridge(input) {
3333
3396
  };
3334
3397
  }
3335
3398
  const body = JSON.stringify({ mcpServers: servers }, null, 2);
3399
+ const bodyDigest = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
3336
3400
  const outPath = path3.join(
3337
3401
  pluginTmpDir(),
3338
- `mcp-${hash}.json`
3402
+ `mcp-${bodyDigest}.json`
3339
3403
  );
3340
3404
  try {
3341
3405
  if (!fileExists(outPath)) {
@@ -5920,43 +5984,46 @@ var ClaudeCodeLanguageModel = class {
5920
5984
  return picked.length > 0 ? picked : null;
5921
5985
  }
5922
5986
  /**
5923
- * Resolve ProxyToolDef[] for opencode's MCP-bridged tools so they go
5987
+ * Resolve ProxyToolDef[] for opencode's MCP-backed tools so they go
5924
5988
  * through the in-process proxy instead of being bridged into Claude CLI's
5925
- * `--mcp-config`. Direct bridging causes double execution because both
5926
- * Claude CLI's own MCP child and opencode hold their own connection to
5927
- * the same server; routing through the proxy keeps a single execution
5928
- * site (opencode). Returns null when the feature is disabled, the SDK
5929
- * client is unavailable, or no MCP servers are configured.
5989
+ * `--mcp-config`. Routing through the proxy keeps a single execution site
5990
+ * (opencode), so the call is permission-prompted and rendered as an
5991
+ * opencode tool call.
5992
+ *
5993
+ * Opt-in (`proxyOpencodeMcpTools: true`) and off by default. It used to
5994
+ * default to true while finding nothing, because it discovered tools via
5995
+ * `client.tool.list()`, which enumerates opencode's `ToolRegistry` and not
5996
+ * the MCP tools merged into the model's tool set afterwards. Discovery now
5997
+ * reads that merged set, the `tools` array opencode passes `doStream`, so
5998
+ * the option does what it says. Turning it on by default at the same time
5999
+ * would have silently moved every existing user's MCP traffic off the
6000
+ * working direct bridge, so the default went to false instead: today's
6001
+ * behaviour is preserved exactly and crossing over is the operator's call.
6002
+ *
6003
+ * Returns null when the feature is off or nothing matched, which leaves
6004
+ * every server on the direct bridge.
5930
6005
  */
5931
- async resolvedProxyMcpTools(allEnabledServerNames) {
5932
- if (this.config.proxyOpencodeMcpTools === false) return null;
6006
+ resolvedProxyMcpTools(allEnabledServerNames, modelTools, taken) {
6007
+ if (this.config.proxyOpencodeMcpTools !== true) return null;
5933
6008
  if (this.config.bridgeOpencodeMcp === false) return null;
5934
6009
  if (allEnabledServerNames.length === 0) return null;
5935
- const items = await fetchOpencodeToolList(
5936
- this.config.provider,
5937
- this.modelId,
5938
- this.config.cwd
5939
- );
5940
- if (!items || items.length === 0) return null;
5941
- const serversByLengthDesc = [...allEnabledServerNames].sort(
5942
- (a, b) => b.length - a.length
5943
- );
5944
- const out = [];
5945
- const seen = /* @__PURE__ */ new Set();
5946
- for (const item of items) {
5947
- const matchedServer = serversByLengthDesc.find(
5948
- (name) => item.id === name || item.id.startsWith(`${name}_`)
6010
+ const resolution = resolveMcpProxyToolDefs({
6011
+ serverNames: allEnabledServerNames,
6012
+ tools: modelTools,
6013
+ taken
6014
+ });
6015
+ if (resolution.defs.length === 0) {
6016
+ log.warn(
6017
+ "proxyOpencodeMcpTools is on but no MCP tool was found in opencode's tool set; those servers stay on the direct bridge this spawn",
6018
+ { servers: allEnabledServerNames, modelTools: modelTools?.length ?? 0 }
5949
6019
  );
5950
- if (!matchedServer) continue;
5951
- if (seen.has(item.id)) continue;
5952
- seen.add(item.id);
5953
- out.push({
5954
- name: item.id,
5955
- description: item.description ?? "",
5956
- inputSchema: item.parameters && typeof item.parameters === "object" ? item.parameters : { type: "object", properties: {} }
5957
- });
6020
+ return null;
5958
6021
  }
5959
- return out.length > 0 ? out : null;
6022
+ log.debug("routing opencode MCP tools through the proxy", {
6023
+ servers: [...resolution.coveredServers],
6024
+ tools: resolution.defs.map((def) => def.name)
6025
+ });
6026
+ return resolution;
5960
6027
  }
5961
6028
  /**
5962
6029
  * Live tool info derived from a single `client.tool.list()` fetch:
@@ -6419,7 +6486,7 @@ var ClaudeCodeLanguageModel = class {
6419
6486
  );
6420
6487
  const sk = effortSessionKey(baseKey, reasoningEffort);
6421
6488
  const compactionMode = this.isCompactionCall(options);
6422
- if (scope === "tools" && (this.resolvedProxyTools() || this.config.proxyOpencodeMcpTools !== false && this.config.bridgeOpencodeMcp !== false)) {
6489
+ if (scope === "tools" && (this.resolvedProxyTools() || this.config.proxyOpencodeMcpTools === true && this.config.bridgeOpencodeMcp !== false)) {
6423
6490
  return this.doGenerateViaStream(options);
6424
6491
  }
6425
6492
  if (compactionMode) {
@@ -7165,10 +7232,13 @@ ${plan}
7165
7232
  void 0,
7166
7233
  runtimeStatus
7167
7234
  );
7168
- const proxyMcpTools = await self.resolvedProxyMcpTools(
7169
- discovery.allEnabledServerNames
7235
+ const mcpResolution = self.resolvedProxyMcpTools(
7236
+ discovery.allEnabledServerNames,
7237
+ options.tools,
7238
+ new Set((resolvedProxy ?? []).map((def) => def.name))
7170
7239
  );
7171
- const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
7240
+ const proxyMcpTools = mcpResolution?.defs ?? null;
7241
+ const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
7172
7242
  const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
7173
7243
  const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
7174
7244
  const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
@@ -9095,7 +9165,7 @@ function createClaudeCode(settings = {}) {
9095
9165
  planModeQuestion: settings.planModeQuestion ?? false,
9096
9166
  webSearch: settings.webSearch,
9097
9167
  hotReloadMcp: settings.hotReloadMcp ?? true,
9098
- proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true,
9168
+ proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools === true,
9099
9169
  multiStepContinuation: settings.multiStepContinuation ?? true,
9100
9170
  autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
9101
9171
  compactionModel: settings.compactionModel,