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.
This commit is contained in:
YeonGyu-Kim
2025-12-15 08:26:16 +09:00
parent 8618d57d95
commit b1abb7999b
3 changed files with 37 additions and 66 deletions

View File

@@ -1,16 +1,16 @@
export const HOOK_NAME = "interactive-bash-blocker" export const HOOK_NAME = "interactive-bash-blocker"
export const INTERACTIVE_FLAG_PATTERNS = [ export const NON_INTERACTIVE_ENV = {
/\bgit\s+(?:rebase|add|stash|reset|checkout|commit|merge|revert|cherry-pick)\s+.*-i\b/, CI: "true",
/\bgit\s+(?:rebase|add|stash|reset|checkout|commit|merge|revert|cherry-pick)\s+.*--interactive\b/, DEBIAN_FRONTEND: "noninteractive",
/\bgit\s+.*-p\b/, GIT_TERMINAL_PROMPT: "0",
/\bgit\s+add\s+.*--patch\b/, GCM_INTERACTIVE: "never",
/\bgit\s+stash\s+.*--patch\b/, HOMEBREW_NO_AUTO_UPDATE: "1",
}
export const ALWAYS_BLOCK_PATTERNS = [
/\b(?:vim?|nvim|nano|emacs|pico|joe|micro|helix|hx)\b/, /\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*$/, /^\s*(?:python|python3|ipython|node|bun|deno|irb|pry|ghci|erl|iex|lua|R)\s*$/,
/\btop\b(?!\s+\|)/, /\btop\b(?!\s+\|)/,
/\bhtop\b/, /\bhtop\b/,
/\bbtop\b/, /\bbtop\b/,
@@ -18,15 +18,6 @@ export const INTERACTIVE_FLAG_PATTERNS = [
/\bmore\b(?!\s+\|)/, /\bmore\b(?!\s+\|)/,
/\bman\b/, /\bman\b/,
/\bwatch\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/, /\bncurses\b/,
/\bdialog\b/, /\bdialog\b/,
/\bwhiptail\b/, /\bwhiptail\b/,
@@ -39,32 +30,14 @@ export const INTERACTIVE_FLAG_PATTERNS = [
/\blazygit\b/, /\blazygit\b/,
/\blazydocker\b/, /\blazydocker\b/,
/\bk9s\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/, /\bselect\b.*\bin\b/,
] ]
export const STDIN_REQUIRING_COMMANDS = [
"passwd",
"su",
"sudo -S",
"gpg --gen-key",
"ssh-keygen",
]
export const TMUX_SUGGESTION = ` export const TMUX_SUGGESTION = `
[interactive-bash-blocker] [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: Example with interactive-terminal skill:
\`\`\` \`\`\`

View File

@@ -1,42 +1,34 @@
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { import { HOOK_NAME, NON_INTERACTIVE_ENV, ALWAYS_BLOCK_PATTERNS, TMUX_SUGGESTION } from "./constants"
HOOK_NAME,
INTERACTIVE_FLAG_PATTERNS,
STDIN_REQUIRING_COMMANDS,
TMUX_SUGGESTION,
} from "./constants"
import type { BlockResult } from "./types" import type { BlockResult } from "./types"
import { log } from "../../shared" import { log } from "../../shared"
export * from "./constants" export * from "./constants"
export * from "./types" export * from "./types"
function checkInteractiveCommand(command: string): BlockResult { function checkTUICommand(command: string): BlockResult {
const normalizedCmd = command.trim() const normalizedCmd = command.trim()
for (const pattern of INTERACTIVE_FLAG_PATTERNS) { for (const pattern of ALWAYS_BLOCK_PATTERNS) {
if (pattern.test(normalizedCmd)) { if (pattern.test(normalizedCmd)) {
return { return {
blocked: true, blocked: true,
reason: `Command contains interactive pattern`, reason: `Command requires full TUI`,
command: normalizedCmd, command: normalizedCmd,
matchedPattern: pattern.source, matchedPattern: pattern.source,
} }
} }
} }
for (const cmd of STDIN_REQUIRING_COMMANDS) { return { blocked: false }
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) { export function createInteractiveBashBlockerHook(ctx: PluginInput) {
@@ -56,10 +48,10 @@ export function createInteractiveBashBlockerHook(ctx: PluginInput) {
return return
} }
const result = checkInteractiveCommand(command) const result = checkTUICommand(command)
if (result.blocked) { if (result.blocked) {
log(`[${HOOK_NAME}] Blocking interactive command`, { log(`[${HOOK_NAME}] Blocking TUI command`, {
sessionID: input.sessionID, sessionID: input.sessionID,
command: result.command, command: result.command,
pattern: result.matchedPattern, pattern: result.matchedPattern,
@@ -68,7 +60,7 @@ export function createInteractiveBashBlockerHook(ctx: PluginInput) {
ctx.client.tui ctx.client.tui
.showToast({ .showToast({
body: { body: {
title: "Interactive Command Blocked", title: "TUI Command Blocked",
message: `${result.reason}\nUse tmux or interactive-terminal skill instead.`, message: `${result.reason}\nUse tmux or interactive-terminal skill instead.`,
variant: "error", variant: "error",
duration: 5000, duration: 5000,
@@ -83,6 +75,14 @@ export function createInteractiveBashBlockerHook(ctx: PluginInput) {
TMUX_SUGGESTION 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,
})
}, },
} }
} }

View File

@@ -1,6 +1,4 @@
export interface InteractiveBashBlockerConfig { export interface InteractiveBashBlockerConfig {
additionalPatterns?: string[]
allowPatterns?: string[]
disabled?: boolean disabled?: boolean
} }