Files
oh-my-opencode/src/hooks/interactive-bash-blocker/index.ts
T
YeonGyu-Kim b1abb7999b 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.
2025-12-15 08:26:16 +09:00

89 lines
2.4 KiB
TypeScript

import type { PluginInput } from "@opencode-ai/plugin"
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 checkTUICommand(command: string): BlockResult {
const normalizedCmd = command.trim()
for (const pattern of ALWAYS_BLOCK_PATTERNS) {
if (pattern.test(normalizedCmd)) {
return {
blocked: true,
reason: `Command requires full TUI`,
command: normalizedCmd,
matchedPattern: pattern.source,
}
}
}
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 (
input: { tool: string; sessionID: string; callID: string },
output: { args: Record<string, unknown> }
): Promise<void> => {
const toolLower = input.tool.toLowerCase()
if (toolLower !== "bash") {
return
}
const command = output.args.command as string | undefined
if (!command) {
return
}
const result = checkTUICommand(command)
if (result.blocked) {
log(`[${HOOK_NAME}] Blocking TUI command`, {
sessionID: input.sessionID,
command: result.command,
pattern: result.matchedPattern,
})
ctx.client.tui
.showToast({
body: {
title: "TUI Command Blocked",
message: `${result.reason}\nUse tmux or interactive-terminal skill instead.`,
variant: "error",
duration: 5000,
},
})
.catch(() => {})
throw new Error(
`[${HOOK_NAME}] ${result.reason}\n` +
`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,
})
},
}
}