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:
@@ -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:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|||||||
@@ -1,44 +1,36 @@
|
|||||||
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) {
|
|
||||||
if (normalizedCmd.includes(cmd)) {
|
|
||||||
return {
|
|
||||||
blocked: true,
|
|
||||||
reason: `Command requires stdin interaction: ${cmd}`,
|
|
||||||
command: normalizedCmd,
|
|
||||||
matchedPattern: cmd,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { blocked: false }
|
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) {
|
||||||
return {
|
return {
|
||||||
"tool.execute.before": async (
|
"tool.execute.before": async (
|
||||||
@@ -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,
|
||||||
@@ -78,11 +70,19 @@ export function createInteractiveBashBlockerHook(ctx: PluginInput) {
|
|||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${HOOK_NAME}] ${result.reason}\n` +
|
`[${HOOK_NAME}] ${result.reason}\n` +
|
||||||
`Command: ${result.command}\n` +
|
`Command: ${result.command}\n` +
|
||||||
`Pattern: ${result.matchedPattern}\n` +
|
`Pattern: ${result.matchedPattern}\n` +
|
||||||
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,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
export interface InteractiveBashBlockerConfig {
|
export interface InteractiveBashBlockerConfig {
|
||||||
additionalPatterns?: string[]
|
|
||||||
allowPatterns?: string[]
|
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user