@khalilgharbaoui/opencode-claude-code-plugin 0.35.1 → 0.36.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Khalil Gharbaoui
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -345,6 +345,8 @@ One piece of Claude Code trivia is worth stating plainly, because it is the oppo
345
345
 
346
346
  Like model and effort, the TTL is part of the Claude session key, so changing it respawns rather than sharing a process. Compaction is exempt.
347
347
 
348
+ Forking an opencode session normally throws that cache away: the fork arrives as a new session with the thread copied in and no Claude session behind it, so the whole conversation is re-rendered as text into the first message and paid for again as cache writes. Setting `forkSessions: true` in the provider options branches the parent's Claude conversation with `claude --resume <parent> --fork-session` instead. Measured on CLI 2.1.280 with haiku 4.5 over a ~13k-token thread, the forked turn wrote 814 cache tokens and read 39,710, against 22,355 written and 17,385 read for the replay: $0.0058 against $0.0467 for that one turn, with the parent's own transcript byte-identical afterwards. It is off by default because a resumed Claude conversation reuses the system prompt recorded on its first request, so a forked session answers under the parent's appended system prompt rather than the current turn's. Anything it cannot match exactly keeps today's replay: another account, an unknown or busy parent, a fork cut mid-conversation or taken mid tool call, a different cwd, model, agent, effort or cache TTL, compaction, and the interactive transport.
349
+
348
350
  To force an **account** rather than a model, pin the full string. This only applies if you declared [`accounts`](#multiple-claude-code-accounts) in the first place; with the default single-account setup there is nothing to pin. Both halves are needed, because the provider selects the account's config dir and the `@account` marker is what the model was registered under for that provider:
349
351
 
350
352
  ```yaml
@@ -483,6 +485,7 @@ reaches the CLI, so there is nothing there to fall back from.
483
485
  | `bridgeSkipNativeSkills` | boolean | `true` | Leave a skill unbridged when the Claude session already loads it from `$CLAUDE_CONFIG_DIR/skills`, the project's `.claude/skills`, or an installed plugin, so one skill does not reach the model twice. Matched by resolved path, by identical `SKILL.md`, or by name. `false` bridges everything and reinstates the duplicates. See [Skills Claude already has](#skills-claude-already-has). |
484
486
  | `logging` | object | all defaults | The plugin's own logger, four independent fields: `file` (boolean, default `false`), `dir` (string, default `~/.local/share/opencode-claude-code/`), `mode` (`"silent"` \| `"debug"`, default `"silent"`) and `level` (`"debug"` \| `"info"` \| `"notice"` \| `"warn"` \| `"error"`, default `"info"`). Goes under `provider.claude-code.options` like every other row here. See [Logging](#logging). |
485
487
  | `turnStats` | boolean | `false` | Append a one-line cost / duration / cache footer to each finished turn. See [Per-turn stats](#per-turn-stats). |
488
+ | `forkSessions` | boolean | `false` | Branch a forked opencode session off the parent's Claude conversation with `--fork-session` instead of replaying its history as text. See [The prompt cache an agent writes](#the-prompt-cache-an-agent-writes). |
486
489
  | `interactive` | boolean | `false` | **Experimental.** Drive the interactive `claude` TUI (subscription billing) instead of headless `--print`. Requires opencode running under Bun with PTY support; silently falls back to headless otherwise. The tool proxy, `permissionMode` and `/btw` are all unavailable on it, so read [What it does not support](#what-it-does-not-support) before enabling. Env: `CLAUDE_CODE_INTERACTIVE_TRANSPORT=1`. |
487
490
  | `interactiveBypass` | boolean | `false` | Deprecated/no-op with `interactive`: Claude Code's TUI shows a manual safety confirmation for `bypassPermissions`, so the plugin intentionally does not pass it. |
488
491
  | `interactiveAllowTools` | string[] | `["Bash", "Edit", "Write", "Read", "WebFetch"]` | With `interactive`: built-in tools pre-allowed without prompting (replaces the default list). MCP server wildcards (`mcp__<server>__*`) are always added from the bridged config. |
package/dist/index.d.ts CHANGED
@@ -420,6 +420,8 @@ interface ClaudeCodeConfig {
420
420
  bridgeSkipNativeSkills?: boolean;
421
421
  /** Append a one-line cost / duration / cache footer to each finished turn. */
422
422
  turnStats?: boolean;
423
+ /** Branch a forked opencode session off the parent's Claude conversation. */
424
+ forkSessions?: boolean;
423
425
  logging?: LoggingConfig;
424
426
  }
425
427
  interface LoggingConfig {
@@ -761,6 +763,37 @@ interface ClaudeCodeProviderSettings {
761
763
  * `permission_denials` always reach `providerMetadata`.
762
764
  */
763
765
  turnStats?: boolean;
766
+ /**
767
+ * When a new opencode session turns out to be a fork of one this provider
768
+ * already served, branch the parent's Claude conversation with
769
+ * `claude --resume <parent> --fork-session` instead of re-rendering the
770
+ * whole thread as text into the first message.
771
+ *
772
+ * Measured on Claude Code 2.1.280 with haiku 4.5 over a ~13k-token
773
+ * conversation: the forked turn wrote 814 cache tokens and read 39,710,
774
+ * against 22,355 written and 17,385 read for the replay of the same thread,
775
+ * which is $0.0058 against $0.0467 for that one turn. The parent's own
776
+ * transcript is byte-identical afterwards (checked by hash).
777
+ *
778
+ * Off by default, and the reason is measured rather than cautious: a
779
+ * resumed Claude conversation reuses the system prompt recorded on its
780
+ * FIRST request (`--system-prompt-snapshot`, default `on`), so a forked
781
+ * session is answered under the parent's appended system prompt, not this
782
+ * turn's. Probed directly: a parent seeded with "your codename is ZEBRA"
783
+ * and forked while passing "your codename is QUAIL" answered ZEBRA. The
784
+ * conversation the model sees is otherwise the real transcript rather than
785
+ * the replay's clipped rendering, which is strictly more faithful and
786
+ * strictly cheaper, so this is worth turning on for branch-heavy work.
787
+ *
788
+ * It is also conservative about when it fires. Everything below keeps
789
+ * today's replay, unchanged: a different account, a parent whose Claude
790
+ * session id is unknown or gone, a parent that is still busy, a fork cut
791
+ * mid-conversation or taken mid tool round trip, a different cwd, model,
792
+ * agent, effort or prompt-cache TTL, a compaction turn, the interactive
793
+ * transport, an account-failover switch, and a `claude` with no
794
+ * `--fork-session` flag.
795
+ */
796
+ forkSessions?: boolean;
764
797
  /**
765
798
  * Routing for Claude's built-in `WebSearch` tool.
766
799
  *
package/dist/index.js CHANGED
@@ -3931,6 +3931,190 @@ async function cancelBackgroundTask(input, options) {
3931
3931
  };
3932
3932
  }
3933
3933
 
3934
+ // src/session-fork.ts
3935
+ import { createHash as createHash3 } from "crypto";
3936
+ var fingerprints = /* @__PURE__ */ new Map();
3937
+ var fingerprintSeq = 0;
3938
+ var MAX_FORK_FINGERPRINTS = 64;
3939
+ function hashPart(hash, part) {
3940
+ const p = part;
3941
+ const type = typeof p?.type === "string" ? p.type : "unknown";
3942
+ switch (type) {
3943
+ case "text":
3944
+ hash.update("text\0");
3945
+ hash.update(String(p?.text ?? ""));
3946
+ break;
3947
+ case "tool-call":
3948
+ hash.update("call\0");
3949
+ hash.update(String(p?.toolName ?? ""));
3950
+ hash.update("\0");
3951
+ hash.update(stableJson(p?.input));
3952
+ break;
3953
+ case "tool-result":
3954
+ hash.update("result\0");
3955
+ hash.update(String(p?.toolName ?? ""));
3956
+ hash.update("\0");
3957
+ hash.update(stableJson(p?.output));
3958
+ break;
3959
+ case "file":
3960
+ case "image": {
3961
+ hash.update(type);
3962
+ hash.update("\0");
3963
+ hash.update(String(p?.mediaType ?? ""));
3964
+ hash.update("\0");
3965
+ const data = p?.data ?? p?.image;
3966
+ if (typeof data === "string") hash.update(data);
3967
+ else if (data instanceof Uint8Array) hash.update(data);
3968
+ else if (data instanceof URL) hash.update(data.href);
3969
+ else hash.update(stableJson(data));
3970
+ break;
3971
+ }
3972
+ default:
3973
+ hash.update("other\0");
3974
+ hash.update(type);
3975
+ break;
3976
+ }
3977
+ hash.update("");
3978
+ }
3979
+ function stableJson(value) {
3980
+ const seen = /* @__PURE__ */ new WeakSet();
3981
+ const walk = (node) => {
3982
+ if (node === null || typeof node !== "object") return node;
3983
+ if (seen.has(node)) return "[circular]";
3984
+ seen.add(node);
3985
+ if (Array.isArray(node)) return node.map(walk);
3986
+ const out = {};
3987
+ for (const key of Object.keys(node).sort()) {
3988
+ out[key] = walk(node[key]);
3989
+ }
3990
+ return out;
3991
+ };
3992
+ try {
3993
+ return JSON.stringify(walk(value)) ?? "null";
3994
+ } catch {
3995
+ return "[unserializable]";
3996
+ }
3997
+ }
3998
+ function messageDigest(msg) {
3999
+ const hash = createHash3("sha256");
4000
+ hash.update(msg.role);
4001
+ hash.update("\0");
4002
+ if (typeof msg.content === "string") {
4003
+ hash.update("text\0");
4004
+ hash.update(msg.content);
4005
+ hash.update("");
4006
+ } else if (Array.isArray(msg.content)) {
4007
+ for (const part of msg.content) hashPart(hash, part);
4008
+ }
4009
+ return hash.digest("hex").slice(0, 32);
4010
+ }
4011
+ function conversationDigests(prompt) {
4012
+ const out = [];
4013
+ for (const msg of prompt) {
4014
+ if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool") {
4015
+ continue;
4016
+ }
4017
+ out.push({
4018
+ role: msg.role,
4019
+ digest: messageDigest(msg)
4020
+ });
4021
+ }
4022
+ return out;
4023
+ }
4024
+ function recordForkFingerprint(sessionKey2, prompt, cliPath) {
4025
+ const entries = conversationDigests(prompt);
4026
+ if (entries.length === 0) return;
4027
+ fingerprints.delete(sessionKey2);
4028
+ fingerprints.set(sessionKey2, { entries, cliPath, seq: ++fingerprintSeq });
4029
+ while (fingerprints.size > MAX_FORK_FINGERPRINTS) {
4030
+ const oldest = fingerprints.keys().next();
4031
+ if (oldest.done) break;
4032
+ fingerprints.delete(oldest.value);
4033
+ }
4034
+ }
4035
+ function forgetForkFingerprint(sessionKey2) {
4036
+ fingerprints.delete(sessionKey2);
4037
+ }
4038
+ function forkSiblingSignature(sessionKey2) {
4039
+ const parts = sessionKey2.split("::");
4040
+ if (parts.length < 5) return null;
4041
+ if (parts[2] === "compaction") return null;
4042
+ const blanked = [...parts];
4043
+ blanked[3] = "*";
4044
+ return blanked.join("::");
4045
+ }
4046
+ function splitForkHistory(prompt) {
4047
+ const messages = prompt;
4048
+ let lastAssistant = -1;
4049
+ for (let i = messages.length - 1; i >= 0; i--) {
4050
+ if (messages[i].role === "assistant") {
4051
+ lastAssistant = i;
4052
+ break;
4053
+ }
4054
+ }
4055
+ if (lastAssistant < 0) return { history: [], forkable: false };
4056
+ for (let i = lastAssistant + 1; i < messages.length; i++) {
4057
+ const msg = messages[i];
4058
+ if (msg.role === "tool") return { history: [], forkable: false };
4059
+ if (!Array.isArray(msg.content)) continue;
4060
+ for (const part of msg.content) {
4061
+ if (part?.type === "tool-result") return { history: [], forkable: false };
4062
+ }
4063
+ }
4064
+ return {
4065
+ history: conversationDigests(messages.slice(0, lastAssistant + 1)),
4066
+ forkable: true
4067
+ };
4068
+ }
4069
+ function findForkParent(opts) {
4070
+ const signature = forkSiblingSignature(opts.sessionKey);
4071
+ if (!signature) return null;
4072
+ const { history, forkable } = splitForkHistory(opts.prompt);
4073
+ if (!forkable || history.length === 0) return null;
4074
+ let best = null;
4075
+ let bestLength = 0;
4076
+ let bestSeq = -1;
4077
+ for (const [key, fingerprint] of fingerprints) {
4078
+ if (key === opts.sessionKey) continue;
4079
+ if (fingerprint.cliPath !== opts.cliPath) continue;
4080
+ if (forkSiblingSignature(key) !== signature) continue;
4081
+ const recorded = fingerprint.entries;
4082
+ if (recorded.length === 0 || recorded.length > history.length) continue;
4083
+ if (!isPrefix(recorded, history)) continue;
4084
+ if (!tailIsReplyOnly(history, recorded.length)) continue;
4085
+ const claudeSessionId = opts.lookupClaudeSessionId(key);
4086
+ if (!claudeSessionId) continue;
4087
+ if (opts.isBusy(key)) continue;
4088
+ if (recorded.length > bestLength || recorded.length === bestLength && fingerprint.seq > bestSeq) {
4089
+ best = { parentKey: key, claudeSessionId, matched: recorded.length };
4090
+ bestLength = recorded.length;
4091
+ bestSeq = fingerprint.seq;
4092
+ }
4093
+ }
4094
+ if (best) {
4095
+ log.info("found a fork parent for this conversation", {
4096
+ sessionKey: opts.sessionKey,
4097
+ parentKey: best.parentKey,
4098
+ matchedMessages: best.matched,
4099
+ historyMessages: history.length
4100
+ });
4101
+ }
4102
+ return best;
4103
+ }
4104
+ function isPrefix(recorded, history) {
4105
+ for (let i = 0; i < recorded.length; i++) {
4106
+ if (recorded[i].role !== history[i].role) return false;
4107
+ if (recorded[i].digest !== history[i].digest) return false;
4108
+ }
4109
+ return true;
4110
+ }
4111
+ function tailIsReplyOnly(history, from) {
4112
+ for (let i = from; i < history.length; i++) {
4113
+ if (history[i].role === "user") return false;
4114
+ }
4115
+ return true;
4116
+ }
4117
+
3934
4118
  // src/permission-presets.ts
3935
4119
  var READ_ONLY_DISALLOWED_CLI_TOOLS = [
3936
4120
  "Bash",
@@ -4535,6 +4719,11 @@ function claudeSessionIsBusy(key) {
4535
4719
  if (activeProcesses.has(key)) return true;
4536
4720
  return getPendingProxyCalls(key).length > 0 || hasExitPlanModeQuestions(key);
4537
4721
  }
4722
+ function claudeSessionIsWriting(key) {
4723
+ const active = activeProcesses.get(key);
4724
+ if (active && isTurnInFlight(active)) return true;
4725
+ return getPendingProxyCalls(key).length > 0 || hasExitPlanModeQuestions(key);
4726
+ }
4538
4727
  function capClaudeSessions() {
4539
4728
  for (const key of [...claudeSessions.keys()]) {
4540
4729
  if (claudeSessions.size <= MAX_CLAUDE_SESSION_ENTRIES) return;
@@ -4558,6 +4747,7 @@ function deleteClaudeSessionId(key) {
4558
4747
  const claudeSessionId = claudeSessions.get(key);
4559
4748
  if (claudeSessionId) clearLedger(claudeSessionId);
4560
4749
  claudeSessions.delete(key);
4750
+ forgetForkFingerprint(key);
4561
4751
  }
4562
4752
  function effortSessionKey(baseKey, effort) {
4563
4753
  return effort ? `${baseKey}::effort=${effort}` : baseKey;
@@ -4694,6 +4884,25 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
4694
4884
  return ap;
4695
4885
  }
4696
4886
  function appendResumeIfNeeded(sessionKey2, cliArgs) {
4887
+ if (cliArgs.includes("--fork-session")) {
4888
+ const forkedSessionId = claudeSessions.get(sessionKey2);
4889
+ if (!forkedSessionId) return cliArgs;
4890
+ const resumed = [];
4891
+ for (let i = 0; i < cliArgs.length; i++) {
4892
+ if (cliArgs[i] === "--fork-session") continue;
4893
+ if (cliArgs[i] === "--resume") {
4894
+ resumed.push("--resume", forkedSessionId);
4895
+ i++;
4896
+ continue;
4897
+ }
4898
+ resumed.push(cliArgs[i]);
4899
+ }
4900
+ log.info("respawning a forked session on its own claude conversation", {
4901
+ sessionKey: sessionKey2,
4902
+ claudeSessionId: forkedSessionId
4903
+ });
4904
+ return resumed;
4905
+ }
4697
4906
  if (cliArgs.includes("--resume") || cliArgs.includes("--session-id")) {
4698
4907
  return cliArgs;
4699
4908
  }
@@ -4734,6 +4943,7 @@ function buildCliArgs(opts) {
4734
4943
  sessionKey: sessionKey2,
4735
4944
  skipPermissions,
4736
4945
  includeSessionId = true,
4946
+ forkFromClaudeSessionId,
4737
4947
  model,
4738
4948
  permissionMode,
4739
4949
  mcpConfig,
@@ -4783,6 +4993,8 @@ function buildCliArgs(opts) {
4783
4993
  const sessionId = claudeSessions.get(sessionKey2);
4784
4994
  if (sessionId && !activeProcesses.has(sessionKey2)) {
4785
4995
  args.push("--resume", sessionId);
4996
+ } else if (forkFromClaudeSessionId) {
4997
+ args.push("--resume", forkFromClaudeSessionId, "--fork-session");
4786
4998
  }
4787
4999
  }
4788
5000
  if (mcpConfig) {
@@ -5138,7 +5350,7 @@ async function handleBtwCommand(client, input, options = {}) {
5138
5350
  }
5139
5351
 
5140
5352
  // src/diagnostic-bundle.ts
5141
- import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
5353
+ import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
5142
5354
  import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
5143
5355
  import { homedir as homedir4 } from "os";
5144
5356
 
@@ -5208,6 +5420,8 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5208
5420
  "falling back to the next model in the chain",
5209
5421
  "fast mode is in cooldown after a rate limit; this turn runs at standard speed and is billed at standard Opus rates, not the fast price shown in the model picker.",
5210
5422
  "file part without data, skipping",
5423
+ "forkSessions is on but this claude CLI has no --fork-session; replaying the conversation instead",
5424
+ "forking the parent conversation's claude session instead of replaying it",
5211
5425
  "ignoring a failover reset time that is not in the future",
5212
5426
  "ignoring unknown proxyOpencodeTools entries",
5213
5427
  "ignoring unknown proxyTools entries",
@@ -5459,7 +5673,7 @@ function shape(value) {
5459
5673
  return `[redacted, ${size} chars]`;
5460
5674
  }
5461
5675
  function createIdHasher(salt = randomBytes2(16).toString("hex")) {
5462
- return (raw) => createHash3("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
5676
+ return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
5463
5677
  }
5464
5678
  function createRedactionContext(options = {}) {
5465
5679
  const home = options.home ?? homedir4();
@@ -11507,6 +11721,49 @@ var ClaudeCodeLanguageModel = class {
11507
11721
  if (failoverAnswer?.kind === "switch" && hasPriorConversation) {
11508
11722
  includeHistoryContext = true;
11509
11723
  }
11724
+ let forkFromClaudeSessionId;
11725
+ if (this.config.forkSessions === true && includeHistoryContext && !compactionMode && !useInteractive && failoverAnswer?.kind !== "switch") {
11726
+ const parent = findForkParent({
11727
+ sessionKey: sk,
11728
+ prompt: options.prompt,
11729
+ cliPath,
11730
+ lookupClaudeSessionId: getClaudeSessionId,
11731
+ // `claudeSessionIsWriting`, not `getActiveProcess`: this walks every
11732
+ // sibling key, and that one refreshes LRU order and cancels idle
11733
+ // timers on everything it touches. An idle live child is fine to
11734
+ // branch from; a transcript mid-write is not.
11735
+ isBusy: claudeSessionIsWriting
11736
+ });
11737
+ if (parent) {
11738
+ const supported = await Promise.race([
11739
+ detectCliSupportsFlag(cliPath, "--fork-session"),
11740
+ turnAbort.whenAborted.then(() => false)
11741
+ ]);
11742
+ if (turnAbort.aborted) {
11743
+ return abortedBeforeWork("probing --fork-session support");
11744
+ }
11745
+ if (supported) {
11746
+ forkFromClaudeSessionId = parent.claudeSessionId;
11747
+ includeHistoryContext = false;
11748
+ log.notice(
11749
+ "forking the parent conversation's claude session instead of replaying it",
11750
+ {
11751
+ sessionKey: sk,
11752
+ parentKey: parent.parentKey,
11753
+ matchedMessages: parent.matched
11754
+ }
11755
+ );
11756
+ } else {
11757
+ log.warn(
11758
+ "forkSessions is on but this claude CLI has no --fork-session; replaying the conversation instead",
11759
+ { cliPath }
11760
+ );
11761
+ }
11762
+ }
11763
+ }
11764
+ if (this.config.forkSessions === true && !compactionMode && !useInteractive) {
11765
+ recordForkFingerprint(sk, options.prompt, cliPath);
11766
+ }
11510
11767
  const { model: spawnModelId, fast: fastMode } = parseModelId(failover.modelId);
11511
11768
  const exitPlanModeQuestionResult = compactionMode ? null : consumeExitPlanModeQuestionResult(sk, options.prompt);
11512
11769
  if (exitPlanModeQuestionResult) {
@@ -11981,6 +12238,7 @@ var ClaudeCodeLanguageModel = class {
11981
12238
  state.cliArgs = buildCliArgs({
11982
12239
  sessionKey: sk,
11983
12240
  skipPermissions,
12241
+ forkFromClaudeSessionId,
11984
12242
  model: spawnModelId,
11985
12243
  permissionMode: self.config.permissionMode,
11986
12244
  mcpConfig: mcp.paths,
@@ -13211,6 +13469,7 @@ function createClaudeCode(settings = {}) {
13211
13469
  bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
13212
13470
  bridgeSkipNativeSkills: settings.bridgeSkipNativeSkills !== false,
13213
13471
  turnStats: settings.turnStats === true,
13472
+ forkSessions: settings.forkSessions === true,
13214
13473
  interactive: settings.interactive,
13215
13474
  interactiveBypass: settings.interactiveBypass,
13216
13475
  interactiveAllowTools: settings.interactiveAllowTools,