From b1abb7999b9f284c7b81a2692620025b3450cb02 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 15 Dec 2025 08:26:16 +0900 Subject: [PATCH] refactor(interactive-bash-blocker): replace regex blocking with environment configuration Instead of blocking commands via regex pattern matching (which caused false positives like 'startup', 'support'), now wraps all bash commands with: - CI=true - DEBIAN_FRONTEND=noninteractive - GIT_TERMINAL_PROMPT=0 - stdin redirected to /dev/null TUI programs (text editors, system monitors, etc.) are still blocked as they require full PTY. Other interactive commands now fail naturally when stdin is unavailable. Closes #55 via alternative approach. --- .../interactive-bash-blocker/constants.ts | 49 ++++------------- src/hooks/interactive-bash-blocker/index.ts | 52 +++++++++---------- src/hooks/interactive-bash-blocker/types.ts | 2 - 3 files changed, 37 insertions(+), 66 deletions(-) diff --git a/src/hooks/interactive-bash-blocker/constants.ts b/src/hooks/interactive-bash-blocker/constants.ts index 157a0e9..f77f33d 100644 --- a/src/hooks/interactive-bash-blocker/constants.ts +++ b/src/hooks/interactive-bash-blocker/constants.ts @@ -1,16 +1,16 @@ export const HOOK_NAME = "interactive-bash-blocker" -export const INTERACTIVE_FLAG_PATTERNS = [ - /\bgit\s+(?:rebase|add|stash|reset|checkout|commit|merge|revert|cherry-pick)\s+.*-i\b/, - /\bgit\s+(?:rebase|add|stash|reset|checkout|commit|merge|revert|cherry-pick)\s+.*--interactive\b/, - /\bgit\s+.*-p\b/, - /\bgit\s+add\s+.*--patch\b/, - /\bgit\s+stash\s+.*--patch\b/, - +export const NON_INTERACTIVE_ENV = { + CI: "true", + DEBIAN_FRONTEND: "noninteractive", + GIT_TERMINAL_PROMPT: "0", + GCM_INTERACTIVE: "never", + HOMEBREW_NO_AUTO_UPDATE: "1", +} + +export const ALWAYS_BLOCK_PATTERNS = [ /\b(?:vim?|nvim|nano|emacs|pico|joe|micro|helix|hx)\b/, - /^\s*(?:python|python3|ipython|node|bun|deno|irb|pry|ghci|erl|iex|lua|R)\s*$/, - /\btop\b(?!\s+\|)/, /\bhtop\b/, /\bbtop\b/, @@ -18,15 +18,6 @@ export const INTERACTIVE_FLAG_PATTERNS = [ /\bmore\b(?!\s+\|)/, /\bman\b/, /\bwatch\b/, - /\bssh\b(?!.*-[oTNf])/, - /\btelnet\b/, - /\bftp\b/, - /\bsftp\b/, - /\bmysql\b(?!.*-e)/, - /\bpsql\b(?!.*-c)/, - /\bmongo\b(?!.*--eval)/, - /\bredis-cli\b(?!.*[^\s])/, - /\bncurses\b/, /\bdialog\b/, /\bwhiptail\b/, @@ -39,32 +30,14 @@ export const INTERACTIVE_FLAG_PATTERNS = [ /\blazygit\b/, /\blazydocker\b/, /\bk9s\b/, - - /\bapt\s+(?:install|remove|upgrade|dist-upgrade)\b(?!.*-y)/, - /\bapt-get\s+(?:install|remove|upgrade|dist-upgrade)\b(?!.*-y)/, - /\byum\s+(?:install|remove|update)\b(?!.*-y)/, - /\bdnf\s+(?:install|remove|update)\b(?!.*-y)/, - /\bpacman\s+-S\b(?!.*--noconfirm)/, - /\bbrew\s+(?:install|uninstall|upgrade)\b(?!.*--force)/, - - /\bread\b(?!\s+.*<)/, - /\bselect\b.*\bin\b/, ] -export const STDIN_REQUIRING_COMMANDS = [ - "passwd", - "su", - "sudo -S", - "gpg --gen-key", - "ssh-keygen", -] - export const TMUX_SUGGESTION = ` [interactive-bash-blocker] -This command requires interactive input which is not supported in this environment. +This command requires a full interactive terminal (TUI) which cannot be emulated. -**Recommendation**: Use tmux for interactive commands. +**Recommendation**: Use tmux for TUI commands. Example with interactive-terminal skill: \`\`\` diff --git a/src/hooks/interactive-bash-blocker/index.ts b/src/hooks/interactive-bash-blocker/index.ts index a2338ec..974975a 100644 --- a/src/hooks/interactive-bash-blocker/index.ts +++ b/src/hooks/interactive-bash-blocker/index.ts @@ -1,44 +1,36 @@ import type { PluginInput } from "@opencode-ai/plugin" -import { - HOOK_NAME, - INTERACTIVE_FLAG_PATTERNS, - STDIN_REQUIRING_COMMANDS, - TMUX_SUGGESTION, -} from "./constants" +import { HOOK_NAME, NON_INTERACTIVE_ENV, ALWAYS_BLOCK_PATTERNS, TMUX_SUGGESTION } from "./constants" import type { BlockResult } from "./types" import { log } from "../../shared" export * from "./constants" export * from "./types" -function checkInteractiveCommand(command: string): BlockResult { +function checkTUICommand(command: string): BlockResult { const normalizedCmd = command.trim() - for (const pattern of INTERACTIVE_FLAG_PATTERNS) { + for (const pattern of ALWAYS_BLOCK_PATTERNS) { if (pattern.test(normalizedCmd)) { return { blocked: true, - reason: `Command contains interactive pattern`, + reason: `Command requires full TUI`, command: normalizedCmd, matchedPattern: pattern.source, } } } - for (const cmd of STDIN_REQUIRING_COMMANDS) { - if (normalizedCmd.includes(cmd)) { - return { - blocked: true, - reason: `Command requires stdin interaction: ${cmd}`, - command: normalizedCmd, - matchedPattern: cmd, - } - } - } - return { blocked: false } } +function wrapWithNonInteractiveEnv(command: string): string { + const envPrefix = Object.entries(NON_INTERACTIVE_ENV) + .map(([key, value]) => `${key}=${value}`) + .join(" ") + + return `${envPrefix} ${command} < /dev/null 2>&1 || ${envPrefix} ${command} 2>&1` +} + export function createInteractiveBashBlockerHook(ctx: PluginInput) { return { "tool.execute.before": async ( @@ -56,10 +48,10 @@ export function createInteractiveBashBlockerHook(ctx: PluginInput) { return } - const result = checkInteractiveCommand(command) + const result = checkTUICommand(command) if (result.blocked) { - log(`[${HOOK_NAME}] Blocking interactive command`, { + log(`[${HOOK_NAME}] Blocking TUI command`, { sessionID: input.sessionID, command: result.command, pattern: result.matchedPattern, @@ -68,7 +60,7 @@ export function createInteractiveBashBlockerHook(ctx: PluginInput) { ctx.client.tui .showToast({ body: { - title: "Interactive Command Blocked", + title: "TUI Command Blocked", message: `${result.reason}\nUse tmux or interactive-terminal skill instead.`, variant: "error", duration: 5000, @@ -78,11 +70,19 @@ export function createInteractiveBashBlockerHook(ctx: PluginInput) { throw new Error( `[${HOOK_NAME}] ${result.reason}\n` + - `Command: ${result.command}\n` + - `Pattern: ${result.matchedPattern}\n` + - TMUX_SUGGESTION + `Command: ${result.command}\n` + + `Pattern: ${result.matchedPattern}\n` + + TMUX_SUGGESTION ) } + + output.args.command = wrapWithNonInteractiveEnv(command) + + log(`[${HOOK_NAME}] Wrapped command with non-interactive environment`, { + sessionID: input.sessionID, + original: command, + wrapped: output.args.command, + }) }, } } diff --git a/src/hooks/interactive-bash-blocker/types.ts b/src/hooks/interactive-bash-blocker/types.ts index 565ad36..454ca46 100644 --- a/src/hooks/interactive-bash-blocker/types.ts +++ b/src/hooks/interactive-bash-blocker/types.ts @@ -1,6 +1,4 @@ export interface InteractiveBashBlockerConfig { - additionalPatterns?: string[] - allowPatterns?: string[] disabled?: boolean }