@khalilgharbaoui/opencode-claude-code-plugin 0.18.0 → 0.18.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
@@ -267,7 +267,7 @@ model: claude-code-appical/claude-opus-5@appical
267
267
  | `proxyTools` | string[] | `["Bash", "Edit", "Write", "WebFetch", "Task"]` | Claude built-in tools to route through opencode's executor + permission UI. Opt-in extras: `"Question"`, `"Compress"`. See [Selective tool proxy](#selective-tool-proxy). |
268
268
  | `extraDisallowedTools` | string[] | – | Extra Claude built-ins to switch off with `--disallowedTools`, on top of what `proxyTools` implies. Claude's names, e.g. `["NotebookEdit"]`. See [Closing a tool with no proxy](#closing-a-tool-with-no-proxy). |
269
269
  | `proxyToolTimeoutMs` | `Record<string, number>` | – | Per-tool proxy call deadline in ms, keyed by proxy tool name (`bash`, `task`, …). Defaults: 10 min flat, `task` → 60 min. For `bash`, the call's own `input.timeout` is honoured on top (`max(resolved, input.timeout)`). See [Selective tool proxy](#selective-tool-proxy). |
270
- | `planModeQuestion` | boolean | `false` | Route `ExitPlanMode` approval through opencode's native `question` tool instead of a text "(yes/no)" prompt. Off because opencode's question form is currently broken upstream. See [Plan mode](#plan-mode). |
270
+ | `planModeQuestion` | boolean | `false` | Route `ExitPlanMode` approval through opencode's native `question` tool instead of a text "(yes/no)" prompt. Opt-in; verify the form works in your installation first. See [Plan mode](#plan-mode). |
271
271
  | `controlRequestBehavior` | `allow` \| `deny` | `allow` | Default response when `skipPermissions: false` and Claude sends a `can_use_tool` control request. |
272
272
  | `controlRequestToolBehaviors` | `Record<string, "allow" \| "deny">` | – | Per-tool override for `can_use_tool`. Example: `{ "Bash": "deny", "Read": "allow" }`. |
273
273
  | `controlRequestDenyMessage` | string | built-in message | Message returned to Claude on a deny. |
@@ -660,6 +660,10 @@ Each chat keeps a long-lived `claude` subprocess so the model retains its native
660
660
 
661
661
  Set `permissionMode: "plan"` to forward `--permission-mode plan` to Claude. The plugin handles `ExitPlanMode` specially — instead of forwarding it as a tool call, it converts it to a confirmation prompt that flows through opencode normally.
662
662
 
663
+ > **Plan mode never permits edits, and you do not have to configure anything for that.** The CLI lets `--dangerously-skip-permissions` override `--permission-mode plan` outright, and `skipPermissions` defaults to `true`, so until this was fixed anyone asking for plan mode silently got full write access (measured on CLI 2.1.258: the run wrote a file on request without a prompt). The plugin now drops the skip flag whenever `permissionMode` is `"plan"`; every other mode governs prompting, which is what that flag is for, so those still pass it.
664
+ >
665
+ > Two things to know. Nothing releases plan mode mid-session: headless Claude Code is not offered an `ExitPlanMode` tool, so approving a plan in chat does not unlock writes, and leaving plan mode means changing the config and restarting opencode. The plugin warns about this once at startup. And the CLI still writes its own plan document under `~/.claude*/plans/`, which is its own feature and outside your workspace; your files and commands are untouched.
666
+
663
667
  By default that prompt is text: the plan is rendered as markdown, followed by `**Do you want to proceed with this plan?** (yes/no)`, and you answer in your next message.
664
668
 
665
669
  ### Approval as a real form (`planModeQuestion`, opt-in)
@@ -675,7 +679,7 @@ Set `planModeQuestion: true` to route the approval through opencode's native `qu
675
679
 
676
680
  The plan is still rendered, but the turn then ends on `tool-calls` and opencode runs its own `question` tool, so approval is a form rather than prose. Your answer is fed back to the CLI as the `tool_result` for the original `ExitPlanMode` call, which is what actually unlocks plan mode on the Claude side. A "yes" typed as ordinary text never does that. Anything other than picking `yes` (including custom text) comes back as rejection feedback the model is told to act on.
677
681
 
678
- > **Leave this off for now.** It depends on the same opencode `question` form that is [broken upstream](#with-question-in-proxytools-currently-blocked-upstream--leave-it-off): with it on, a plan approval hangs until you interrupt the turn. On opencode builds with no `question` registry entry at all the plugin silently keeps the text path (look for `plan-mode question gate` in the log). Re-test when [anomalyco/opencode#36603](https://github.com/anomalyco/opencode/pull/36603) merges.
682
+ > **This cannot currently fire on the default headless transport, so leaving it off costs you nothing.** The form it delivers through works (see [AskUserQuestion](#askuserquestion)), but headless `--print` does not offer the model an `ExitPlanMode` tool at all on CLI 2.1.258, and the bridge keys on that tool call. Measured three ways: asked directly for its tool list in plan mode, the CLI returned `Agent, Bash, Edit, ListAgents, Read, ReportFindings, ScheduleWakeup, Skill, ToolSearch, Workflow, Write` and nothing else; asked to do work it said "I'm unable to exit plan mode from within the tool set available to me"; and a full probe through this plugin with `planModeQuestion: true` produced no `ExitPlanMode` anywhere in `plugin.log` while the model asked for approval in prose. The name is still known to the CLI (`--disallowedTools ExitPlanMode` validates silently, where a bogus name warns), so this reads as headless dormancy rather than removal, the same shape as the [`AskUserQuestion` fallback](#askuserquestion). The text path below is what you actually get, and it works. Re-run those probes on a newer CLI before assuming the bridge is reachable. On opencode builds with no `question` registry entry the plugin silently keeps the text path (look for `plan-mode question gate` in the log).
679
683
 
680
684
  Approval bridge contributed by [@CollieIsCute](https://github.com/CollieIsCute).
681
685
 
@@ -685,11 +689,17 @@ Approval bridge contributed by [@CollieIsCute](https://github.com/CollieIsCute).
685
689
 
686
690
  opencode ships a built-in `question` tool (`packages/opencode/src/tool/question.ts`) that renders a real TUI form with options and a custom-answer field — near-identical to Claude Code's `AskUserQuestion` (`multiSelect` → `multiple`). The plugin can route `AskUserQuestion` through it so the prompt becomes an actual form instead of plain text. Two modes:
687
691
 
688
- ### With `"Question"` in `proxyTools` (currently blocked upstream — leave it off)
692
+ ### With `"Question"` in `proxyTools` (opt-in)
693
+
694
+ > **Correction, September 6, 2026: this is no longer blocked, and earlier releases of this README were wrong about why.** The missing form was attributed to an upstream TUI regression. The real cause was local: a notification plugin awaited macOS `alerter` dismissal inside `tool.execute.before`, so the question tool never started. Native providers load that same global plugin, which is why their identical failure did not isolate the TUI. With the hook made non-blocking, the form renders, and the full path through this plugin is verified: on plugin 0.18.0 / Claude Code 2.1.258 / opencode 1.18.29, Claude called `mcp__opencode_proxy__question`, the request appeared in `GET /question`, the reply completed the tool, and Claude's answer contained a token it could only have read from the tool result. Confirmed in a real terminal too: with `"Question"` enabled and opencode relaunched, the proxied call rendered as a TUI form and the clicked answers came back into the turn.
695
+ >
696
+ > `"Question"` is still opt-in, because turning it on disables Claude's own `AskUserQuestion` (see the fallback below) and that trade should be deliberate. If your form does not render, see [question troubleshooting](#question-troubleshooting) before assuming an upstream bug.
697
+
698
+ #### Question troubleshooting
689
699
 
690
- > **Known upstream breakage (opencode 1.15.x through at least 1.18.5).** opencode's `question` TUI form does not render, so the tool blocks until you interrupt the turn. This is not specific to this plugin: native providers hit it identically, and a `--pure` headless server drives the same question end to end successfully (`question.asked` → `GET /question` → `POST /question/{id}/reply` → tool completes), which isolates the fault to the TUI. Tracked upstream as [anomalyco/opencode#36604](https://github.com/anomalyco/opencode/issues/36604) with fix [PR #36603](https://github.com/anomalyco/opencode/pull/36603) (unmerged). Until that lands, enabling `"Question"` trades the working fallback below for a hang. The instructions here describe the intended behavior for when it is fixed.
700
+ For a stalled call, inspect `GET /question` on the same opencode server and workspace. If no request exists, check awaited `tool.execute.before` hooks and custom tools replacing `question`, especially notification plugins: a hook opencode waits on runs *before* the tool, so the request cannot exist yet. If a request exists but no form appears, check session ownership, pending permissions, and event delivery. The separate detach/reattach issue [anomalyco/opencode#36604](https://github.com/anomalyco/opencode/issues/36604) remains open; [PR #36603](https://github.com/anomalyco/opencode/pull/36603) is closed without merging. Do not infer a universal platform or version failure from either symptom.
691
701
 
692
- Add `"Question"` to `proxyTools` and grant `permission.question: allow` to the calling agent. Claude's built-in `AskUserQuestion` is disabled via `--disallowedTools`, and the plugin exposes `mcp__opencode_proxy__question` in its place. The model calls the proxy, opencode renders the form, and the operator's answers come back as arrays of selected labels. On builds that lack the `question` registry entry the def is silently dropped at spawn (version gate), and the deny/markdown fallback below applies instead.
702
+ Add `"Question"` to `proxyTools`. Claude's built-in `AskUserQuestion` is disabled via `--disallowedTools`, and the plugin exposes `mcp__opencode_proxy__question` in its place. A primary agent needs no permission entry (verified on opencode 1.18.29 with no `permission` block at all); if a subagent's form is refused, grant it `permission.question: "allow"` on that agent, the same way [subagent todos](#subagent-todos) need `todowrite`. The model calls the proxy, opencode renders the form, and the operator's answers come back as arrays of selected labels. On builds that lack the `question` registry entry the def is silently dropped at spawn (version gate), and the deny/markdown fallback below applies instead.
693
703
 
694
704
  `proxyTools` replaces the default list rather than adding to it, so repeat the defaults you still want:
695
705
 
@@ -779,7 +789,7 @@ The plugin respects the standard Claude Code thinking env vars. If you set them
779
789
  ## Quirks worth knowing
780
790
 
781
791
  - **Empty text blocks are dropped.** Claude sometimes opens a `content_block_start` for text but never sends a delta. The plugin no longer emits the empty block (which was triggering Anthropic 400s like `cache_control cannot be set for empty text blocks`).
782
- - **Smart incomplete-turn continuation.** By default, the plugin keeps the current opencode stream open and feeds Claude CLI a small internal continuation message when Claude emits a `result` after reasoning/tool activity without a useful visible answer. It still stops normally on final-looking answers, questions, blockers, errors, aborts, or internal safety-budget exhaustion. Disable with `"autoContinueIncompleteTurns": false`.
792
+ - **Smart incomplete-turn continuation.** By default, the plugin keeps the current opencode stream open and feeds Claude CLI a small internal continuation message when Claude emits a `result` after reasoning/tool activity without a useful visible answer. It still stops normally on final-looking answers, questions, blockers, errors, aborts, or internal safety-budget exhaustion. It also resumes an answer the model was cut off mid-sentence: a `max_tokens` stop means truncation rather than completion, so the turn continues instead of ending on half a sentence, capped at 8 attempts and 10 minutes. Every other stop reason is taken at face value. Disable with `"autoContinueIncompleteTurns": false`.
783
793
  - **`AskUserQuestion`** from the CLI is converted into plain text content rather than forwarded as a tool call — unless `"Question"` is in `proxyTools`, in which case it is routed through opencode's native `question` tool (see [AskUserQuestion](#askuserquestion)).
784
794
  - **Wire-inactivity watchdog.** Once the CLI has produced any content, the stream closes gracefully if stdout goes silent for 60 seconds without a `result` message arriving. Resets on every line received, so long mid-turn pauses (Sonnet between text-end and the next tool_use, for example) are tolerated. On a user-initiated abort, the watchdog shortens to 5 seconds.
785
795
  - **Per-iteration usage.** When the CLI internally retries with tools, the plugin only counts the last iteration's usage so opencode's context accounting stays accurate.
package/dist/index.d.ts CHANGED
@@ -768,6 +768,8 @@ declare const DEFAULT_PROXY_TOOL_NAMES: string[];
768
768
  * so a user-defined command keeps opencode's normal behaviour end to end.
769
769
  */
770
770
  declare function registerSideQuestionCommand(config: OpenCodeConfig): boolean;
771
+ declare function _resetPlanModeWarningForTests(): void;
772
+ declare function warnIfPlanModeCannotExit(permissionMode: string | undefined): void;
771
773
  declare function createClaudeCode(settings?: ClaudeCodeProviderSettings): ClaudeCodeProvider;
772
774
  /**
773
775
  * Build models in OpenCode's config schema format (flat properties like
@@ -786,4 +788,4 @@ declare const _default: {
786
788
  server: OpenCodePlugin;
787
789
  };
788
790
 
789
- export { type AgentRecord, type ClaudeCodeConfig, ClaudeCodeLanguageModel, type ClaudeCodeProvider, type ClaudeCodeProviderSettings, type ClaudeStreamMessage, DEFAULT_PROXY_TOOL_NAMES, type OpenCodeHooks, type OpenCodeModel, type OpenCodePlugin, bridgeOpencodeMcp, claudeCodeProviders, configModelsForProvider, createClaudeCode, _default as default, defaultModels, getAgentRegistry, getDefaultSubagentModel, registerSideQuestionCommand, resolveAgentModel };
791
+ export { type AgentRecord, type ClaudeCodeConfig, ClaudeCodeLanguageModel, type ClaudeCodeProvider, type ClaudeCodeProviderSettings, type ClaudeStreamMessage, DEFAULT_PROXY_TOOL_NAMES, type OpenCodeHooks, type OpenCodeModel, type OpenCodePlugin, _resetPlanModeWarningForTests, bridgeOpencodeMcp, claudeCodeProviders, configModelsForProvider, createClaudeCode, _default as default, defaultModels, getAgentRegistry, getDefaultSubagentModel, registerSideQuestionCommand, resolveAgentModel, warnIfPlanModeCannotExit };
package/dist/index.js CHANGED
@@ -2255,7 +2255,7 @@ function buildCliArgs(opts) {
2255
2255
  if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
2256
2256
  args.push("--settings", JSON.stringify({ fastMode: true }));
2257
2257
  }
2258
- if (skipPermissions) {
2258
+ if (skipPermissions && permissionMode !== "plan") {
2259
2259
  args.push("--dangerously-skip-permissions");
2260
2260
  }
2261
2261
  return args;
@@ -4533,6 +4533,9 @@ var AUTO_CONTINUE_MAX_ELAPSED_MS = 10 * 60 * 1e3;
4533
4533
  var AUTO_CONTINUE_NO_PROGRESS_LIMIT = 2;
4534
4534
  var PROXY_RESULT_BOUNDARY_GRACE_MS = 250;
4535
4535
  var AUTO_CONTINUE_PROMPT = "Continue the task from where you stopped. Do not summarize; keep working until the requested task is complete, you need clarification, or you hit a real blocker.";
4536
+ function isTruncationStopReason(stopReason) {
4537
+ return stopReason === "max_tokens" || stopReason === "max_output_tokens";
4538
+ }
4536
4539
  function normalizeVisibleText(text) {
4537
4540
  return text.replace(/\s+/g, " ").trim();
4538
4541
  }
@@ -4619,6 +4622,16 @@ function shouldAutoContinueIncompleteTurn(state, snapshot) {
4619
4622
  if (state.aborted) return { continue: false, reason: "aborted" };
4620
4623
  if (state.sawAskUserQuestion) return { continue: false, reason: "question" };
4621
4624
  if (snapshot.stopReason) {
4625
+ if (isTruncationStopReason(snapshot.stopReason)) {
4626
+ if (state.attempts >= AUTO_CONTINUE_MAX_ATTEMPTS) {
4627
+ return { continue: false, reason: "max-attempts" };
4628
+ }
4629
+ const truncatedAt = snapshot.now ?? Date.now();
4630
+ if (truncatedAt - state.startedAt > AUTO_CONTINUE_MAX_ELAPSED_MS) {
4631
+ return { continue: false, reason: "max-elapsed" };
4632
+ }
4633
+ return { continue: true, reason: "truncated" };
4634
+ }
4622
4635
  return {
4623
4636
  continue: false,
4624
4637
  reason: snapshot.stopReason.replace(/_/g, "-")
@@ -7939,6 +7952,7 @@ function pickOpencodeDirectory(input) {
7939
7952
  return void 0;
7940
7953
  }
7941
7954
  var warnedAnthropicApiKey = false;
7955
+ var warnedPlanModeNoExit = false;
7942
7956
  var DEFAULT_PROXY_TOOL_NAMES = [
7943
7957
  "Bash",
7944
7958
  "Edit",
@@ -7970,6 +7984,18 @@ function warnIfAnthropicApiKey(ignore) {
7970
7984
  );
7971
7985
  }
7972
7986
  }
7987
+ function _resetPlanModeWarningForTests() {
7988
+ warnedPlanModeNoExit = false;
7989
+ }
7990
+ function warnIfPlanModeCannotExit(permissionMode) {
7991
+ if (permissionMode !== "plan") return;
7992
+ if (warnedPlanModeNoExit) return;
7993
+ warnedPlanModeNoExit = true;
7994
+ log.warn(
7995
+ 'permissionMode "plan" is enforced: claude cannot edit files or run commands, and --dangerously-skip-permissions is deliberately not passed so it stays that way. Headless Claude Code is not offered an ExitPlanMode tool, so nothing releases plan mode mid-session; approving a plan in chat does not unlock writes. Leaving plan mode means changing the config and restarting opencode.',
7996
+ { permissionMode, measuredOn: "claude-code 2.1.258" }
7997
+ );
7998
+ }
7973
7999
  function createClaudeCode(settings = {}) {
7974
8000
  if (settings.logging) {
7975
8001
  configureLogger({
@@ -7980,6 +8006,7 @@ function createClaudeCode(settings = {}) {
7980
8006
  });
7981
8007
  }
7982
8008
  warnIfAnthropicApiKey(settings.ignoreAnthropicApiKey);
8009
+ warnIfPlanModeCannotExit(settings.permissionMode);
7983
8010
  const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
7984
8011
  const providerName = settings.providerID ?? settings.name ?? "claude-code";
7985
8012
  const proxyTools = settings.proxyTools ?? [...DEFAULT_PROXY_TOOL_NAMES];
@@ -8287,6 +8314,7 @@ var index_default = {
8287
8314
  export {
8288
8315
  ClaudeCodeLanguageModel,
8289
8316
  DEFAULT_PROXY_TOOL_NAMES,
8317
+ _resetPlanModeWarningForTests,
8290
8318
  bridgeOpencodeMcp,
8291
8319
  claudeCodeProviders,
8292
8320
  configModelsForProvider,
@@ -8296,6 +8324,7 @@ export {
8296
8324
  getAgentRegistry,
8297
8325
  getDefaultSubagentModel,
8298
8326
  registerSideQuestionCommand,
8299
- resolveAgentModel
8327
+ resolveAgentModel,
8328
+ warnIfPlanModeCannotExit
8300
8329
  };
8301
8330
  //# sourceMappingURL=index.js.map