@khalilgharbaoui/opencode-claude-code-plugin 0.17.0 → 0.18.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
@@ -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. |
@@ -553,6 +553,20 @@ Notes:
553
553
 
554
554
  Fully restart opencode after upgrading to load the command and runtime changes. Other providers do not gain Claude's native side-question behavior from this command.
555
555
 
556
+ ## Configuration skill
557
+
558
+ The package includes a `claude-code-plugin` skill so your agent can configure it without asking you to navigate all its options. Ask, for example:
559
+
560
+ ```text
561
+ Use the claude-code-plugin skill to configure a work account and idle worker cleanup.
562
+ ```
563
+
564
+ It covers accounts, models and agent effort, proxy tools, permissions, MCP/skill bridging, timeouts, logging, upgrades and troubleshooting. It directs the agent to preserve JSONC comments, change only requested settings, validate the result, protect credentials and ask before paid probes or broader permissions.
565
+
566
+ The plugin registers the bundled directory with opencode's `skills.paths`, making it available to other providers too on supporting opencode versions. For ordinary headless Claude turns it also loads through Claude's native Skill tool as `opencode-skills:claude-code-plugin`, even when `bridgeOpencodeSkills` is off. This requires CLI `--plugin-dir` support; interactive transport, compaction and direct `doGenerate` calls do not load the native bridge.
567
+
568
+ No separate skill installation or copying is needed. It ships with each package version, so upgrading updates the reference. Fully restart opencode to load it. `test-configure-skill.ts` checks coverage of provider/logging options, model ids, proxy tools and environment variables; maintainers must update behavior and default guidance in the same change as the implementation.
569
+
556
570
  ## Skill bridge
557
571
 
558
572
  opencode and Claude Code use the same on-disk skill format, a `<name>/SKILL.md` whose frontmatter carries `name` and `description`, but they read from different directories. opencode looks in `.opencode/skills/` and `~/.config/opencode/skills/`; the Claude CLI looks in `~/.claude/skills/` and its own plugins. So opencode advertises your skills in the system prompt it forwards, the model calls `Skill("browser-automation")`, and Claude answers `Unknown skill`.
@@ -568,7 +582,7 @@ Claude can invoke them with the Skill tool or as `/opencode-skills:<name>`. `--p
568
582
 
569
583
  Discovery order, first match wins: `.opencode/skills/` walking up from the working directory, then `~/.opencode/skills/`, then `$OPENCODE_CONFIG_DIR/skills/`, then `~/.config/opencode/skills/`. A project skill shadows a global one of the same name. If the skill set is unchanged the staged directory is reused between spawns.
570
584
 
571
- It is **off by default** here, unlike on the fork it came from: every bridged skill is also listed in the system prompt opencode already forwards, so a large skill set is paid for twice on every turn. Turn it on when you see `Unknown skill`. It no-ops on the compaction path and on a Claude CLI without `--plugin-dir` (the plugin probes `claude --help` and logs a notice).
585
+ Bridging **your own skills is off by default** here, unlike on the fork it came from: every bridged skill is also listed in the system prompt opencode already forwards, so a large skill set is paid for twice on every turn. Turn it on when you see `Unknown skill`. The bundled configuration skill is loaded independently of this opt-in. The native bridge is not used for compaction or interactive transport, or on a Claude CLI without `--plugin-dir` (the plugin probes `claude --help` and logs a notice).
572
586
 
573
587
  This bridge was written by [@broskees](https://github.com/broskees) (Joseph Roberts) on his fork and absorbed here with credit; see [Credits](#credits).
574
588
 
@@ -646,6 +660,10 @@ Each chat keeps a long-lived `claude` subprocess so the model retains its native
646
660
 
647
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.
648
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
+
649
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.
650
668
 
651
669
  ### Approval as a real form (`planModeQuestion`, opt-in)
@@ -661,7 +679,7 @@ Set `planModeQuestion: true` to route the approval through opencode's native `qu
661
679
 
662
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.
663
681
 
664
- > **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).
665
683
 
666
684
  Approval bridge contributed by [@CollieIsCute](https://github.com/CollieIsCute).
667
685
 
@@ -671,11 +689,17 @@ Approval bridge contributed by [@CollieIsCute](https://github.com/CollieIsCute).
671
689
 
672
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:
673
691
 
674
- ### 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
675
699
 
676
- > **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.
677
701
 
678
- 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.
679
703
 
680
704
  `proxyTools` replaces the default list rather than adding to it, so repeat the defaults you still want:
681
705
 
package/dist/index.d.ts CHANGED
@@ -78,6 +78,10 @@ type OpenCodeConfig = {
78
78
  models?: Record<string, unknown>;
79
79
  }>;
80
80
  agent?: Record<string, Record<string, unknown>>;
81
+ skills?: {
82
+ paths?: string[];
83
+ urls?: string[];
84
+ };
81
85
  };
82
86
  /**
83
87
  * Bus events surface to plugins. Shape mirrors what opencode core publishes
@@ -764,6 +768,8 @@ declare const DEFAULT_PROXY_TOOL_NAMES: string[];
764
768
  * so a user-defined command keeps opencode's normal behaviour end to end.
765
769
  */
766
770
  declare function registerSideQuestionCommand(config: OpenCodeConfig): boolean;
771
+ declare function _resetPlanModeWarningForTests(): void;
772
+ declare function warnIfPlanModeCannotExit(permissionMode: string | undefined): void;
767
773
  declare function createClaudeCode(settings?: ClaudeCodeProviderSettings): ClaudeCodeProvider;
768
774
  /**
769
775
  * Build models in OpenCode's config schema format (flat properties like
@@ -782,4 +788,4 @@ declare const _default: {
782
788
  server: OpenCodePlugin;
783
789
  };
784
790
 
785
- 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
@@ -447,10 +447,13 @@ function detectCliSupportsFlag(cliPath, flag) {
447
447
  if (cached) return cached;
448
448
  const promise = (async () => {
449
449
  try {
450
- const { stdout } = await execFileAsync(cliPath, ["--help"], {
450
+ const execution = execFileAsync(cliPath, ["--help"], {
451
451
  timeout: 5e3,
452
+ killSignal: "SIGKILL",
452
453
  maxBuffer: 4 * 1024 * 1024
453
454
  });
455
+ execution.child.stdin?.end();
456
+ const { stdout } = await execution;
454
457
  return stdout.includes(flag);
455
458
  } catch (err) {
456
459
  log.warn("failed to probe claude cli flag support", {
@@ -2252,7 +2255,7 @@ function buildCliArgs(opts) {
2252
2255
  if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
2253
2256
  args.push("--settings", JSON.stringify({ fastMode: true }));
2254
2257
  }
2255
- if (skipPermissions) {
2258
+ if (skipPermissions && permissionMode !== "plan") {
2256
2259
  args.push("--dangerously-skip-permissions");
2257
2260
  }
2258
2261
  return args;
@@ -3302,7 +3305,17 @@ import * as crypto2 from "crypto";
3302
3305
  import * as fs3 from "fs";
3303
3306
  import * as os2 from "os";
3304
3307
  import * as path4 from "path";
3308
+ import { fileURLToPath } from "url";
3305
3309
  var SKILL_PLUGIN_NAME = "opencode-skills";
3310
+ function bundledSkillsDir() {
3311
+ try {
3312
+ const here = fileURLToPath(import.meta.url);
3313
+ const dir = path4.resolve(path4.dirname(here), "..", "skills");
3314
+ return dirExists(dir) ? dir : null;
3315
+ } catch {
3316
+ return null;
3317
+ }
3318
+ }
3306
3319
  function dirExists(p) {
3307
3320
  try {
3308
3321
  return fs3.statSync(p).isDirectory();
@@ -3341,28 +3354,46 @@ function skillRoots(cwd) {
3341
3354
  if (xdg) push(path4.join(xdg, "opencode", "skills"));
3342
3355
  return roots;
3343
3356
  }
3357
+ function collectSkills(root, claimed, found) {
3358
+ let entries;
3359
+ try {
3360
+ entries = fs3.readdirSync(root, { withFileTypes: true });
3361
+ } catch {
3362
+ return;
3363
+ }
3364
+ for (const entry of entries) {
3365
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3366
+ const name = entry.name;
3367
+ if (name.startsWith(".")) continue;
3368
+ if (claimed.has(name)) continue;
3369
+ const dir = path4.join(root, name);
3370
+ if (!fileExists(path4.join(dir, "SKILL.md"))) continue;
3371
+ claimed.add(name);
3372
+ found.push({ name, dir });
3373
+ }
3374
+ }
3375
+ var byName = (a, b) => a.name.localeCompare(b.name);
3344
3376
  function discoverOpencodeSkills(cwd) {
3345
3377
  const found = [];
3346
3378
  const claimed = /* @__PURE__ */ new Set();
3347
- for (const root of skillRoots(cwd)) {
3348
- let entries;
3349
- try {
3350
- entries = fs3.readdirSync(root, { withFileTypes: true });
3351
- } catch {
3352
- continue;
3353
- }
3354
- for (const entry of entries) {
3355
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3356
- const name = entry.name;
3357
- if (name.startsWith(".")) continue;
3358
- if (claimed.has(name)) continue;
3359
- const dir = path4.join(root, name);
3360
- if (!fileExists(path4.join(dir, "SKILL.md"))) continue;
3361
- claimed.add(name);
3362
- found.push({ name, dir });
3363
- }
3364
- }
3365
- return found.sort((a, b) => a.name.localeCompare(b.name));
3379
+ for (const root of skillRoots(cwd)) collectSkills(root, claimed, found);
3380
+ return found.sort(byName);
3381
+ }
3382
+ function discoverBundledSkills() {
3383
+ const root = bundledSkillsDir();
3384
+ if (!root) return [];
3385
+ const found = [];
3386
+ collectSkills(root, /* @__PURE__ */ new Set(), found);
3387
+ return found.sort(byName);
3388
+ }
3389
+ function registerBundledSkillPath(config) {
3390
+ const dir = bundledSkillsDir();
3391
+ if (!dir) return false;
3392
+ config.skills ??= {};
3393
+ const paths = config.skills.paths ??= [];
3394
+ if (paths.some((p) => path4.resolve(p) === dir)) return false;
3395
+ paths.push(dir);
3396
+ return true;
3366
3397
  }
3367
3398
  function linkSkill(source, target) {
3368
3399
  try {
@@ -3408,8 +3439,10 @@ function buildSkillPluginDir(skills) {
3408
3439
  return root;
3409
3440
  }
3410
3441
  async function resolveSkillPluginDirs(opts) {
3411
- if (!opts.enabled) return [];
3412
- const skills = discoverOpencodeSkills(opts.cwd);
3442
+ const user = opts.enabled ? discoverOpencodeSkills(opts.cwd) : [];
3443
+ const claimed = new Set(user.map((s) => s.name));
3444
+ const bundled = discoverBundledSkills().filter((s) => !claimed.has(s.name));
3445
+ const skills = [...user, ...bundled].sort(byName);
3413
3446
  if (skills.length === 0) return [];
3414
3447
  const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
3415
3448
  if (!supported) {
@@ -3424,6 +3457,7 @@ async function resolveSkillPluginDirs(opts) {
3424
3457
  log.info("bridged opencode skills into claude", {
3425
3458
  count: skills.length,
3426
3459
  names: skills.map((s) => s.name),
3460
+ bundled: bundled.map((s) => s.name),
3427
3461
  pluginDir: dir
3428
3462
  });
3429
3463
  return [dir];
@@ -7670,7 +7704,7 @@ import {
7670
7704
  } from "fs";
7671
7705
  import { homedir as homedir6 } from "os";
7672
7706
  import { join as join8, resolve as resolve4 } from "path";
7673
- import { fileURLToPath } from "url";
7707
+ import { fileURLToPath as fileURLToPath2 } from "url";
7674
7708
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
7675
7709
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
7676
7710
  var alreadyRan = false;
@@ -7702,7 +7736,7 @@ function userIntendsToUseUnscoped() {
7702
7736
  }
7703
7737
  function ourLoadedDir() {
7704
7738
  try {
7705
- const filePath = fileURLToPath(import.meta.url);
7739
+ const filePath = fileURLToPath2(import.meta.url);
7706
7740
  return realpathSync(resolve4(filePath, "..", ".."));
7707
7741
  } catch {
7708
7742
  return null;
@@ -7776,12 +7810,12 @@ import { execFile as execFile2 } from "child_process";
7776
7810
  import * as fs6 from "fs";
7777
7811
  import * as path8 from "path";
7778
7812
  import { promisify as promisify2 } from "util";
7779
- import { fileURLToPath as fileURLToPath2 } from "url";
7813
+ import { fileURLToPath as fileURLToPath3 } from "url";
7780
7814
  var cachedPluginVersion;
7781
7815
  function pluginVersion() {
7782
7816
  if (cachedPluginVersion) return cachedPluginVersion;
7783
7817
  try {
7784
- const here = path8.dirname(fileURLToPath2(import.meta.url));
7818
+ const here = path8.dirname(fileURLToPath3(import.meta.url));
7785
7819
  const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
7786
7820
  const version = JSON.parse(raw).version;
7787
7821
  cachedPluginVersion = typeof version === "string" ? version : "unknown";
@@ -7905,6 +7939,7 @@ function pickOpencodeDirectory(input) {
7905
7939
  return void 0;
7906
7940
  }
7907
7941
  var warnedAnthropicApiKey = false;
7942
+ var warnedPlanModeNoExit = false;
7908
7943
  var DEFAULT_PROXY_TOOL_NAMES = [
7909
7944
  "Bash",
7910
7945
  "Edit",
@@ -7936,6 +7971,18 @@ function warnIfAnthropicApiKey(ignore) {
7936
7971
  );
7937
7972
  }
7938
7973
  }
7974
+ function _resetPlanModeWarningForTests() {
7975
+ warnedPlanModeNoExit = false;
7976
+ }
7977
+ function warnIfPlanModeCannotExit(permissionMode) {
7978
+ if (permissionMode !== "plan") return;
7979
+ if (warnedPlanModeNoExit) return;
7980
+ warnedPlanModeNoExit = true;
7981
+ log.warn(
7982
+ '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.',
7983
+ { permissionMode, measuredOn: "claude-code 2.1.258" }
7984
+ );
7985
+ }
7939
7986
  function createClaudeCode(settings = {}) {
7940
7987
  if (settings.logging) {
7941
7988
  configureLogger({
@@ -7946,6 +7993,7 @@ function createClaudeCode(settings = {}) {
7946
7993
  });
7947
7994
  }
7948
7995
  warnIfAnthropicApiKey(settings.ignoreAnthropicApiKey);
7996
+ warnIfPlanModeCannotExit(settings.permissionMode);
7949
7997
  const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
7950
7998
  const providerName = settings.providerID ?? settings.name ?? "claude-code";
7951
7999
  const proxyTools = settings.proxyTools ?? [...DEFAULT_PROXY_TOOL_NAMES];
@@ -8177,6 +8225,7 @@ var server = async (input) => {
8177
8225
  return {
8178
8226
  config: async (config) => {
8179
8227
  if (registerSideQuestionCommand(config)) ownsSideQuestionCommand = true;
8228
+ registerBundledSkillPath(config);
8180
8229
  config.provider ??= {};
8181
8230
  await buildAgentRegistry(config);
8182
8231
  const expanded = await expandAccountProviders(config);
@@ -8252,6 +8301,7 @@ var index_default = {
8252
8301
  export {
8253
8302
  ClaudeCodeLanguageModel,
8254
8303
  DEFAULT_PROXY_TOOL_NAMES,
8304
+ _resetPlanModeWarningForTests,
8255
8305
  bridgeOpencodeMcp,
8256
8306
  claudeCodeProviders,
8257
8307
  configModelsForProvider,
@@ -8261,6 +8311,7 @@ export {
8261
8311
  getAgentRegistry,
8262
8312
  getDefaultSubagentModel,
8263
8313
  registerSideQuestionCommand,
8264
- resolveAgentModel
8314
+ resolveAgentModel,
8315
+ warnIfPlanModeCannotExit
8265
8316
  };
8266
8317
  //# sourceMappingURL=index.js.map