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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export interface InteractiveBashBlockerConfig {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface BlockResult {
|
||||
blocked: boolean
|
||||
reason?: string
|
||||
command?: string
|
||||
matchedPattern?: string
|
||||
}
|
||||
@@ -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.
|
||||
40
src/hooks/non-interactive-env/index.ts
Normal file
40
src/hooks/non-interactive-env/index.ts
Normal 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,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
10
src/hooks/non-interactive-env/types.ts
Normal file
10
src/hooks/non-interactive-env/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface NonInteractiveEnvConfig {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface TUICheckResult {
|
||||
isTUI: boolean
|
||||
reason?: string
|
||||
command?: string
|
||||
matchedPattern?: string
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user