Skip to content

Commit b1abb79

Browse files
committed
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.
1 parent 8618d57 commit b1abb79

File tree

3 files changed

+37
-66
lines changed

3 files changed

+37
-66
lines changed

src/hooks/interactive-bash-blocker/constants.ts

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
11
export const HOOK_NAME = "interactive-bash-blocker"
22

3-
export const INTERACTIVE_FLAG_PATTERNS = [
4-
/\bgit\s+(?:rebase|add|stash|reset|checkout|commit|merge|revert|cherry-pick)\s+.*-i\b/,
5-
/\bgit\s+(?:rebase|add|stash|reset|checkout|commit|merge|revert|cherry-pick)\s+.*--interactive\b/,
6-
/\bgit\s+.*-p\b/,
7-
/\bgit\s+add\s+.*--patch\b/,
8-
/\bgit\s+stash\s+.*--patch\b/,
9-
3+
export const NON_INTERACTIVE_ENV = {
4+
CI: "true",
5+
DEBIAN_FRONTEND: "noninteractive",
6+
GIT_TERMINAL_PROMPT: "0",
7+
GCM_INTERACTIVE: "never",
8+
HOMEBREW_NO_AUTO_UPDATE: "1",
9+
}
10+
11+
export const ALWAYS_BLOCK_PATTERNS = [
1012
/\b(?:vim?|nvim|nano|emacs|pico|joe|micro|helix|hx)\b/,
11-
1213
/^\s*(?:python|python3|ipython|node|bun|deno|irb|pry|ghci|erl|iex|lua|R)\s*$/,
13-
1414
/\btop\b(?!\s+\|)/,
1515
/\bhtop\b/,
1616
/\bbtop\b/,
1717
/\bless\b(?!\s+\|)/,
1818
/\bmore\b(?!\s+\|)/,
1919
/\bman\b/,
2020
/\bwatch\b/,
21-
/\bssh\b(?!.*-[oTNf])/,
22-
/\btelnet\b/,
23-
/\bftp\b/,
24-
/\bsftp\b/,
25-
/\bmysql\b(?!.*-e)/,
26-
/\bpsql\b(?!.*-c)/,
27-
/\bmongo\b(?!.*--eval)/,
28-
/\bredis-cli\b(?!.*[^\s])/,
29-
3021
/\bncurses\b/,
3122
/\bdialog\b/,
3223
/\bwhiptail\b/,
@@ -39,32 +30,14 @@ export const INTERACTIVE_FLAG_PATTERNS = [
3930
/\blazygit\b/,
4031
/\blazydocker\b/,
4132
/\bk9s\b/,
42-
43-
/\bapt\s+(?:install|remove|upgrade|dist-upgrade)\b(?!.*-y)/,
44-
/\bapt-get\s+(?:install|remove|upgrade|dist-upgrade)\b(?!.*-y)/,
45-
/\byum\s+(?:install|remove|update)\b(?!.*-y)/,
46-
/\bdnf\s+(?:install|remove|update)\b(?!.*-y)/,
47-
/\bpacman\s+-S\b(?!.*--noconfirm)/,
48-
/\bbrew\s+(?:install|uninstall|upgrade)\b(?!.*--force)/,
49-
50-
/\bread\b(?!\s+.*<)/,
51-
5233
/\bselect\b.*\bin\b/,
5334
]
5435

55-
export const STDIN_REQUIRING_COMMANDS = [
56-
"passwd",
57-
"su",
58-
"sudo -S",
59-
"gpg --gen-key",
60-
"ssh-keygen",
61-
]
62-
6336
export const TMUX_SUGGESTION = `
6437
[interactive-bash-blocker]
65-
This command requires interactive input which is not supported in this environment.
38+
This command requires a full interactive terminal (TUI) which cannot be emulated.
6639
67-
**Recommendation**: Use tmux for interactive commands.
40+
**Recommendation**: Use tmux for TUI commands.
6841
6942
Example with interactive-terminal skill:
7043
\`\`\`
Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,36 @@
11
import type { PluginInput } from "@opencode-ai/plugin"
2-
import {
3-
HOOK_NAME,
4-
INTERACTIVE_FLAG_PATTERNS,
5-
STDIN_REQUIRING_COMMANDS,
6-
TMUX_SUGGESTION,
7-
} from "./constants"
2+
import { HOOK_NAME, NON_INTERACTIVE_ENV, ALWAYS_BLOCK_PATTERNS, TMUX_SUGGESTION } from "./constants"
83
import type { BlockResult } from "./types"
94
import { log } from "../../shared"
105

116
export * from "./constants"
127
export * from "./types"
138

14-
function checkInteractiveCommand(command: string): BlockResult {
9+
function checkTUICommand(command: string): BlockResult {
1510
const normalizedCmd = command.trim()
1611

17-
for (const pattern of INTERACTIVE_FLAG_PATTERNS) {
12+
for (const pattern of ALWAYS_BLOCK_PATTERNS) {
1813
if (pattern.test(normalizedCmd)) {
1914
return {
2015
blocked: true,
21-
reason: `Command contains interactive pattern`,
16+
reason: `Command requires full TUI`,
2217
command: normalizedCmd,
2318
matchedPattern: pattern.source,
2419
}
2520
}
2621
}
2722

28-
for (const cmd of STDIN_REQUIRING_COMMANDS) {
29-
if (normalizedCmd.includes(cmd)) {
30-
return {
31-
blocked: true,
32-
reason: `Command requires stdin interaction: ${cmd}`,
33-
command: normalizedCmd,
34-
matchedPattern: cmd,
35-
}
36-
}
37-
}
38-
3923
return { blocked: false }
4024
}
4125

26+
function wrapWithNonInteractiveEnv(command: string): string {
27+
const envPrefix = Object.entries(NON_INTERACTIVE_ENV)
28+
.map(([key, value]) => `${key}=${value}`)
29+
.join(" ")
30+
31+
return `${envPrefix} ${command} < /dev/null 2>&1 || ${envPrefix} ${command} 2>&1`
32+
}
33+
4234
export function createInteractiveBashBlockerHook(ctx: PluginInput) {
4335
return {
4436
"tool.execute.before": async (
@@ -56,10 +48,10 @@ export function createInteractiveBashBlockerHook(ctx: PluginInput) {
5648
return
5749
}
5850

59-
const result = checkInteractiveCommand(command)
51+
const result = checkTUICommand(command)
6052

6153
if (result.blocked) {
62-
log(`[${HOOK_NAME}] Blocking interactive command`, {
54+
log(`[${HOOK_NAME}] Blocking TUI command`, {
6355
sessionID: input.sessionID,
6456
command: result.command,
6557
pattern: result.matchedPattern,
@@ -68,7 +60,7 @@ export function createInteractiveBashBlockerHook(ctx: PluginInput) {
6860
ctx.client.tui
6961
.showToast({
7062
body: {
71-
title: "Interactive Command Blocked",
63+
title: "TUI Command Blocked",
7264
message: `${result.reason}\nUse tmux or interactive-terminal skill instead.`,
7365
variant: "error",
7466
duration: 5000,
@@ -78,11 +70,19 @@ export function createInteractiveBashBlockerHook(ctx: PluginInput) {
7870

7971
throw new Error(
8072
`[${HOOK_NAME}] ${result.reason}\n` +
81-
`Command: ${result.command}\n` +
82-
`Pattern: ${result.matchedPattern}\n` +
83-
TMUX_SUGGESTION
73+
`Command: ${result.command}\n` +
74+
`Pattern: ${result.matchedPattern}\n` +
75+
TMUX_SUGGESTION
8476
)
8577
}
78+
79+
output.args.command = wrapWithNonInteractiveEnv(command)
80+
81+
log(`[${HOOK_NAME}] Wrapped command with non-interactive environment`, {
82+
sessionID: input.sessionID,
83+
original: command,
84+
wrapped: output.args.command,
85+
})
8686
},
8787
}
8888
}

src/hooks/interactive-bash-blocker/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
export interface InteractiveBashBlockerConfig {
2-
additionalPatterns?: string[]
3-
allowPatterns?: string[]
42
disabled?: boolean
53
}
64

0 commit comments

Comments
 (0)