diff --git a/src/tools/interactive-bash/constants.ts b/src/tools/interactive-bash/constants.ts
index 67570e4c8..d9fd10853 100644
--- a/src/tools/interactive-bash/constants.ts
+++ b/src/tools/interactive-bash/constants.ts
@@ -11,6 +11,10 @@ export const BLOCKED_TMUX_SUBCOMMANDS = [
"pipep",
]
+export const PROHIBITED_TMUX_SUBCOMMANDS = [
+ "kill-server",
+]
+
export const INTERACTIVE_BASH_DESCRIPTION = `WARNING: This is TMUX ONLY. Pass tmux subcommands directly (without 'tmux' prefix).
Examples: new-session -d -s omo-dev, send-keys -t omo-dev "vim" Enter
diff --git a/src/tools/interactive-bash/tools.test.ts b/src/tools/interactive-bash/tools.test.ts
new file mode 100644
index 000000000..9c0d20249
--- /dev/null
+++ b/src/tools/interactive-bash/tools.test.ts
@@ -0,0 +1,42 @@
+///
+
+import { describe, expect, test } from "bun:test"
+import type { ToolContext } from "@opencode-ai/plugin/tool"
+import { interactive_bash } from "./tools"
+
+const mockContext = {
+ sessionID: "test-session",
+ messageID: "test-message",
+ agent: "test-agent",
+ directory: "/project",
+ worktree: "/project",
+ abort: new AbortController().signal,
+ metadata: () => {},
+ ask: async () => {},
+} satisfies ToolContext
+
+describe("interactive_bash", () => {
+ test("#given kill-server command #when executed #then returns a strong prohibition without running tmux", async () => {
+ // given
+ const args = { tmux_command: "kill-server" }
+
+ // when
+ const output = await interactive_bash.execute(args, mockContext)
+
+ // then
+ expect(output).toContain("Error: 'kill-server' is prohibited in interactive_bash.")
+ expect(output).toContain("NEVER EVER run tmux kill-server from interactive_bash.")
+ expect(output).toContain("Do not retry kill-server with Bash or any other tool.")
+ })
+
+ test("#given kill-server after tmux global options #when executed #then still prohibits it", async () => {
+ // given
+ const args = { tmux_command: "-L omo-socket kill-server" }
+
+ // when
+ const output = await interactive_bash.execute(args, mockContext)
+
+ // then
+ expect(output).toContain("Error: 'kill-server' is prohibited in interactive_bash.")
+ })
+})
diff --git a/src/tools/interactive-bash/tools.ts b/src/tools/interactive-bash/tools.ts
index 21a03cf7f..ba6eee2be 100644
--- a/src/tools/interactive-bash/tools.ts
+++ b/src/tools/interactive-bash/tools.ts
@@ -1,9 +1,16 @@
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
import { isCmuxCompatEnvironment } from "../../shared/tmux/cmux-detect"
-import { BLOCKED_TMUX_SUBCOMMANDS, DEFAULT_TIMEOUT_MS, INTERACTIVE_BASH_DESCRIPTION } from "./constants"
+import {
+ BLOCKED_TMUX_SUBCOMMANDS,
+ DEFAULT_TIMEOUT_MS,
+ INTERACTIVE_BASH_DESCRIPTION,
+ PROHIBITED_TMUX_SUBCOMMANDS,
+} from "./constants"
import { getCachedTmuxPath } from "./tmux-path-resolver"
+const GLOBAL_TMUX_OPTIONS_WITH_ARGS = new Set(["-L", "-S", "-f", "-c", "-T"])
+
function resolveTmuxExecutable(tmuxPath: string): string[] {
if (!isCmuxCompatEnvironment()) {
return [tmuxPath]
@@ -59,6 +66,84 @@ export function tokenizeCommand(cmd: string): string[] {
return tokens
}
+function findSubcommandIndex(parts: string[]): number {
+ let index = 0
+ while (index < parts.length) {
+ const part = parts[index] ?? ""
+
+ if (part === "--") {
+ return index + 1 < parts.length ? index + 1 : -1
+ }
+
+ if (GLOBAL_TMUX_OPTIONS_WITH_ARGS.has(part)) {
+ index += 2
+ continue
+ }
+
+ if (part.startsWith("-")) {
+ index++
+ continue
+ }
+
+ return index
+ }
+
+ return -1
+}
+
+function getTargetSessionName(parts: string[]): string {
+ const sessionIdx = parts.findIndex(p => p === "-t" || p.startsWith("-t"))
+ if (sessionIdx === -1) {
+ return "omo-session"
+ }
+
+ const sessionToken = parts[sessionIdx] ?? ""
+ const nextToken = parts[sessionIdx + 1]
+ if (sessionToken === "-t" && nextToken) {
+ return nextToken
+ }
+
+ if (sessionToken.startsWith("-t")) {
+ return sessionToken.slice(2)
+ }
+
+ return "omo-session"
+}
+
+function buildBlockedTmuxCommandMessage(command: string, parts: string[]): string {
+ const sessionName = getTargetSessionName(parts)
+
+ return `Error: '${command}' is blocked in interactive_bash.
+
+**USE BASH TOOL INSTEAD:**
+
+\`\`\`bash
+# Capture terminal output
+tmux capture-pane -p -t ${sessionName}
+
+# Or capture with history (last 1000 lines)
+tmux capture-pane -p -t ${sessionName} -S -1000
+\`\`\`
+
+The Bash tool can execute these commands directly. Do NOT retry with interactive_bash.`
+}
+
+function buildProhibitedTmuxCommandMessage(command: string): string {
+ return `Error: '${command}' is prohibited in interactive_bash.
+
+NEVER EVER run tmux kill-server from interactive_bash.
+
+It terminates the entire tmux server, destroying every tmux session and pane that the user, Codex, or other agents may be using.
+
+Use scoped cleanup only:
+
+\`\`\`bash
+tmux kill-session -t
+\`\`\`
+
+If you created an omo-* session, kill only that exact session. Do not retry kill-server with Bash or any other tool.`
+}
+
export const interactive_bash: ToolDefinition = tool({
description: INTERACTIVE_BASH_DESCRIPTION,
args: {
@@ -74,31 +159,16 @@ export const interactive_bash: ToolDefinition = tool({
return "Error: Empty tmux command"
}
- const subcommand = parts[0].toLowerCase()
+ const subcommandIndex = findSubcommandIndex(parts)
+ const rawSubcommand = subcommandIndex === -1 ? "" : parts[subcommandIndex]
+ const subcommand = rawSubcommand.toLowerCase()
+
+ if (PROHIBITED_TMUX_SUBCOMMANDS.includes(subcommand)) {
+ return buildProhibitedTmuxCommandMessage(rawSubcommand)
+ }
+
if (BLOCKED_TMUX_SUBCOMMANDS.includes(subcommand)) {
- const sessionIdx = parts.findIndex(p => p === "-t" || p.startsWith("-t"))
- let sessionName = "omo-session"
- if (sessionIdx !== -1) {
- if (parts[sessionIdx] === "-t" && parts[sessionIdx + 1]) {
- sessionName = parts[sessionIdx + 1]
- } else if (parts[sessionIdx].startsWith("-t")) {
- sessionName = parts[sessionIdx].slice(2)
- }
- }
-
- return `Error: '${parts[0]}' is blocked in interactive_bash.
-
-**USE BASH TOOL INSTEAD:**
-
-\`\`\`bash
-# Capture terminal output
-tmux capture-pane -p -t ${sessionName}
-
-# Or capture with history (last 1000 lines)
-tmux capture-pane -p -t ${sessionName} -S -1000
-\`\`\`
-
-The Bash tool can execute these commands directly. Do NOT retry with interactive_bash.`
+ return buildBlockedTmuxCommandMessage(rawSubcommand, parts)
}
const proc = spawnWithWindowsHide([...resolveTmuxExecutable(tmuxPath), ...parts], {