@khalilgharbaoui/opencode-claude-code-plugin 0.13.1 → 0.13.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
@@ -299,6 +299,14 @@ recovery step for harnesses that defer MCP tool schemas. Both apply per Claude
299
299
  process at spawn, and provider options are read once at opencode startup, so
300
300
  `proxyTools` changes need a full opencode restart.
301
301
 
302
+ ### Proxy endpoint security
303
+
304
+ The proxy is a small HTTP MCP server on an ephemeral loopback port, and calling it runs Bash, Edit and Write through opencode's executor. Since 0.13.2 it requires a 256-bit bearer token, generated per server and handed to Claude in the `headers` block of the `0600` MCP config file the plugin writes. Requests are also rejected unless the `Host` header matches the bound `127.0.0.1:<port>` authority, no `Origin` header is present, and the content type is `application/json`.
305
+
306
+ **Upgrade if you are on 0.13.1 or earlier.** Before this, any local process could post to that port and execute commands as you, and a web page you visited could do the same blind, without reading the response. Reported by @willmcginnis in [#28](https://github.com/khalilgharbaoui/opencode-claude-code-plugin/pull/28).
307
+
308
+ Nothing to configure. If proxied tools ever stop working after a Claude Code upgrade, check the plugin log for `proxy-mcp rejected a request`, which names which guard failed.
309
+
302
310
  ### Closing a tool with no proxy
303
311
 
304
312
  `proxyTools` only reaches built-ins the plugin can replace. A built-in with no opencode equivalent, `NotebookEdit` today and whatever Claude Code ships next, stays enabled and unmediated no matter what you put in that list. `extraDisallowedTools` names them directly:
package/dist/index.d.ts CHANGED
@@ -31,8 +31,8 @@ type OpenCodeModel = {
31
31
  video: boolean;
32
32
  pdf: boolean;
33
33
  };
34
- interleaved: boolean | {
35
- field: "reasoning_content" | "reasoning_details";
34
+ interleaved: boolean | string | {
35
+ field: "reasoning" | "reasoning_content" | "reasoning_text" | string;
36
36
  };
37
37
  };
38
38
  cost: {
package/dist/index.js CHANGED
@@ -2541,10 +2541,50 @@ var DEFAULT_PROXY_TOOLS = [
2541
2541
  async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides, interceptors) {
2542
2542
  const calls = new EventEmitter3();
2543
2543
  const pending = /* @__PURE__ */ new Map();
2544
+ const authToken = crypto2.randomBytes(32).toString("hex");
2545
+ const expectedAuth = Buffer.from(`Bearer ${authToken}`);
2546
+ let boundAuthority = "";
2547
+ function authOk(req) {
2548
+ const got = req.headers.authorization;
2549
+ if (typeof got !== "string") return false;
2550
+ const candidate = Buffer.from(got);
2551
+ if (candidate.length !== expectedAuth.length) return false;
2552
+ return crypto2.timingSafeEqual(candidate, expectedAuth);
2553
+ }
2554
+ function reject(req, res, statusCode, reason) {
2555
+ log.notice("proxy-mcp rejected a request", {
2556
+ statusCode,
2557
+ reason,
2558
+ method: req.method,
2559
+ hasAuthorization: typeof req.headers.authorization === "string"
2560
+ });
2561
+ res.statusCode = statusCode;
2562
+ res.setHeader("Connection", "close");
2563
+ res.on("finish", () => {
2564
+ req.socket?.destroy();
2565
+ });
2566
+ res.end();
2567
+ }
2544
2568
  const server2 = createServer(async (req, res) => {
2545
2569
  if (req.method !== "POST" || !req.url?.startsWith("/mcp")) {
2546
- res.statusCode = 404;
2547
- res.end();
2570
+ reject(req, res, 404, "not a POST to /mcp");
2571
+ return;
2572
+ }
2573
+ if (req.headers.host !== boundAuthority) {
2574
+ reject(req, res, 403, "host header is not the bound authority");
2575
+ return;
2576
+ }
2577
+ if (req.headers.origin !== void 0) {
2578
+ reject(req, res, 403, "origin header present");
2579
+ return;
2580
+ }
2581
+ const contentType = String(req.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
2582
+ if (contentType !== "application/json") {
2583
+ reject(req, res, 415, "content-type is not application/json");
2584
+ return;
2585
+ }
2586
+ if (!authOk(req)) {
2587
+ reject(req, res, 401, "missing or invalid bearer token");
2548
2588
  return;
2549
2589
  }
2550
2590
  let requestId = null;
@@ -2636,13 +2676,13 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2636
2676
  });
2637
2677
  let timer = null;
2638
2678
  const result = await new Promise(
2639
- (resolve4, reject) => {
2679
+ (resolve4, reject2) => {
2640
2680
  const entry = {
2641
2681
  id: callId,
2642
2682
  toolName,
2643
2683
  input,
2644
2684
  resolve: resolve4,
2645
- reject
2685
+ reject: reject2
2646
2686
  };
2647
2687
  pending.set(callId, entry);
2648
2688
  const deadlineMs = resolveProxyCallTimeoutMs(
@@ -2658,7 +2698,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2658
2698
  toolName,
2659
2699
  deadlineMs
2660
2700
  });
2661
- reject(buildProxyTimeoutError(toolName, deadlineMs));
2701
+ reject2(buildProxyTimeoutError(toolName, deadlineMs));
2662
2702
  }, deadlineMs);
2663
2703
  calls.emit("call", entry);
2664
2704
  }
@@ -2717,10 +2757,10 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2717
2757
  }
2718
2758
  }
2719
2759
  });
2720
- await new Promise((resolve4, reject) => {
2721
- server2.once("error", reject);
2760
+ await new Promise((resolve4, reject2) => {
2761
+ server2.once("error", reject2);
2722
2762
  server2.listen(0, "127.0.0.1", () => {
2723
- server2.off("error", reject);
2763
+ server2.off("error", reject2);
2724
2764
  resolve4();
2725
2765
  });
2726
2766
  });
@@ -2729,7 +2769,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2729
2769
  server2.close();
2730
2770
  throw new Error("Failed to bind proxy MCP server");
2731
2771
  }
2732
- const url = `http://127.0.0.1:${addr.port}/mcp`;
2772
+ boundAuthority = `127.0.0.1:${addr.port}`;
2773
+ const url = `http://${boundAuthority}/mcp`;
2733
2774
  log.info("proxy-mcp server started", {
2734
2775
  url,
2735
2776
  tools: tools.map((t) => t.name)
@@ -2739,6 +2780,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2739
2780
  url,
2740
2781
  serverName: SERVER_NAME,
2741
2782
  tools,
2783
+ authToken,
2742
2784
  calls,
2743
2785
  configPath() {
2744
2786
  if (configFilePath) return configFilePath;
@@ -2748,6 +2790,10 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2748
2790
  [SERVER_NAME]: {
2749
2791
  type: "http",
2750
2792
  url,
2793
+ // Claude CLI replays these headers on every request to this
2794
+ // server, which is what lets the handler above reject anyone
2795
+ // who did not read this 0600 file.
2796
+ headers: { Authorization: `Bearer ${authToken}` },
2751
2797
  timeout: resolveProxyClientCeilingMs(timeoutOverrides)
2752
2798
  }
2753
2799
  }