refactor(hooks): rename interactive-bash-blocker to non-interactive-env

- Replace regex-based command blocking with environment configuration
- Add cross-platform null device support (NUL for Windows, /dev/null for Unix)
- Wrap all bash commands with non-interactive environment variables
- Only block TUI programs that require full PTY
- Update schema, README docs, and all imports/exports

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-14 22:34:55 +09:00
parent 5dd4d97c94
commit 192e8adf18
11 changed files with 1169 additions and 277 deletions

View File

@@ -608,7 +608,7 @@ OmO를 비활성화하고 원래 build/plan 에이전트를 복원하려면:
}
```
사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`
사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`
### MCPs

View File

@@ -609,7 +609,7 @@ Disable specific built-in hooks via `disabled_hooks` in `~/.config/opencode/oh-m
}
```
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`
### MCPs

File diff suppressed because it is too large Load Diff

View File

@@ -60,7 +60,7 @@ export const HookNameSchema = z.enum([
"startup-toast",
"keyword-detector",
"agent-usage-reminder",
"interactive-bash-blocker",
"non-interactive-env",
])
export const AgentOverrideConfigSchema = z.object({

View File

@@ -17,4 +17,4 @@ export { createAutoUpdateCheckerHook } from "./auto-update-checker";
export { createAgentUsageReminderHook } from "./agent-usage-reminder";
export { createKeywordDetectorHook } from "./keyword-detector";
export { createInteractiveBashBlockerHook } from "./interactive-bash-blocker";
export { createNonInteractiveEnvHook } from "./non-interactive-env";

View File

@@ -1,88 +0,0 @@
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,
})
},
}
}

View File

@@ -1,10 +0,0 @@
export interface InteractiveBashBlockerConfig {
disabled?: boolean
}
export interface BlockResult {
blocked: boolean
reason?: string
command?: string
matchedPattern?: string
}

View File

@@ -1,6 +1,8 @@
export const HOOK_NAME = "interactive-bash-blocker"
export const HOOK_NAME = "non-interactive-env"
export const NON_INTERACTIVE_ENV = {
export const NULL_DEVICE = process.platform === "win32" ? "NUL" : "/dev/null"
export const NON_INTERACTIVE_ENV: Record<string, string> = {
CI: "true",
DEBIAN_FRONTEND: "noninteractive",
GIT_TERMINAL_PROMPT: "0",
@@ -8,7 +10,7 @@ export const NON_INTERACTIVE_ENV = {
HOMEBREW_NO_AUTO_UPDATE: "1",
}
export const ALWAYS_BLOCK_PATTERNS = [
export const TUI_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+\|)/,
@@ -33,8 +35,8 @@ export const ALWAYS_BLOCK_PATTERNS = [
/\bselect\b.*\bin\b/,
]
export const TMUX_SUGGESTION = `
[interactive-bash-blocker]
export const TUI_SUGGESTION = `
[non-interactive-env]
This command requires a full interactive terminal (TUI) which cannot be emulated.
**Recommendation**: Use tmux for TUI commands.

View File

@@ -0,0 +1,40 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { HOOK_NAME, NULL_DEVICE, NON_INTERACTIVE_ENV } from "./constants"
import { log } from "../../shared"
export * from "./constants"
export * from "./types"
function wrapWithNonInteractiveEnv(command: string): string {
const envPrefix = Object.entries(NON_INTERACTIVE_ENV)
.map(([key, value]) => `${key}=${value}`)
.join(" ")
return `${envPrefix} ${command} < ${NULL_DEVICE} 2>&1 || ${envPrefix} ${command} 2>&1`
}
export function createNonInteractiveEnvHook(_ctx: PluginInput) {
return {
"tool.execute.before": async (
input: { tool: string; sessionID: string; callID: string },
output: { args: Record<string, unknown> }
): Promise<void> => {
if (input.tool.toLowerCase() !== "bash") {
return
}
const command = output.args.command as string | undefined
if (!command) {
return
}
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

@@ -0,0 +1,10 @@
export interface NonInteractiveEnvConfig {
disabled?: boolean
}
export interface TUICheckResult {
isTUI: boolean
reason?: string
command?: string
matchedPattern?: string
}

View File

@@ -18,7 +18,7 @@ import {
createAutoUpdateCheckerHook,
createKeywordDetectorHook,
createAgentUsageReminderHook,
createInteractiveBashBlockerHook,
createNonInteractiveEnvHook,
} from "./hooks";
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
import {
@@ -239,8 +239,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
? createAgentUsageReminderHook(ctx)
: null;
const interactiveBashBlocker = isHookEnabled("interactive-bash-blocker")
? createInteractiveBashBlockerHook(ctx)
const nonInteractiveEnv = isHookEnabled("non-interactive-env")
? createNonInteractiveEnvHook(ctx)
: null;
updateTerminalTitle({ sessionId: "main" });
@@ -483,7 +483,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
"tool.execute.before": async (input, output) => {
await claudeCodeHooks["tool.execute.before"](input, output);
await interactiveBashBlocker?.["tool.execute.before"](input, output);
await nonInteractiveEnv?.["tool.execute.before"](input, output);
await commentChecker?.["tool.execute.before"](input, output);
if (input.sessionID === getMainSessionID()) {