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
|
### 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
|
### MCPs
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -60,7 +60,7 @@ export const HookNameSchema = z.enum([
|
|||||||
"startup-toast",
|
"startup-toast",
|
||||||
"keyword-detector",
|
"keyword-detector",
|
||||||
"agent-usage-reminder",
|
"agent-usage-reminder",
|
||||||
"interactive-bash-blocker",
|
"non-interactive-env",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const AgentOverrideConfigSchema = z.object({
|
export const AgentOverrideConfigSchema = z.object({
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ export { createAutoUpdateCheckerHook } from "./auto-update-checker";
|
|||||||
|
|
||||||
export { createAgentUsageReminderHook } from "./agent-usage-reminder";
|
export { createAgentUsageReminderHook } from "./agent-usage-reminder";
|
||||||
export { createKeywordDetectorHook } from "./keyword-detector";
|
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",
|
CI: "true",
|
||||||
DEBIAN_FRONTEND: "noninteractive",
|
DEBIAN_FRONTEND: "noninteractive",
|
||||||
GIT_TERMINAL_PROMPT: "0",
|
GIT_TERMINAL_PROMPT: "0",
|
||||||
@@ -8,7 +10,7 @@ export const NON_INTERACTIVE_ENV = {
|
|||||||
HOMEBREW_NO_AUTO_UPDATE: "1",
|
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/,
|
/\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+\|)/,
|
||||||
@@ -33,8 +35,8 @@ export const ALWAYS_BLOCK_PATTERNS = [
|
|||||||
/\bselect\b.*\bin\b/,
|
/\bselect\b.*\bin\b/,
|
||||||
]
|
]
|
||||||
|
|
||||||
export const TMUX_SUGGESTION = `
|
export const TUI_SUGGESTION = `
|
||||||
[interactive-bash-blocker]
|
[non-interactive-env]
|
||||||
This command requires a full interactive terminal (TUI) which cannot be emulated.
|
This command requires a full interactive terminal (TUI) which cannot be emulated.
|
||||||
|
|
||||||
**Recommendation**: Use tmux for TUI commands.
|
**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,
|
createAutoUpdateCheckerHook,
|
||||||
createKeywordDetectorHook,
|
createKeywordDetectorHook,
|
||||||
createAgentUsageReminderHook,
|
createAgentUsageReminderHook,
|
||||||
createInteractiveBashBlockerHook,
|
createNonInteractiveEnvHook,
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||||
import {
|
import {
|
||||||
@@ -239,8 +239,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
|
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
|
||||||
? createAgentUsageReminderHook(ctx)
|
? createAgentUsageReminderHook(ctx)
|
||||||
: null;
|
: null;
|
||||||
const interactiveBashBlocker = isHookEnabled("interactive-bash-blocker")
|
const nonInteractiveEnv = isHookEnabled("non-interactive-env")
|
||||||
? createInteractiveBashBlockerHook(ctx)
|
? createNonInteractiveEnvHook(ctx)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
updateTerminalTitle({ sessionId: "main" });
|
updateTerminalTitle({ sessionId: "main" });
|
||||||
@@ -483,7 +483,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
|||||||
|
|
||||||
"tool.execute.before": async (input, output) => {
|
"tool.execute.before": async (input, output) => {
|
||||||
await claudeCodeHooks["tool.execute.before"](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);
|
await commentChecker?.["tool.execute.before"](input, output);
|
||||||
|
|
||||||
if (input.sessionID === getMainSessionID()) {
|
if (input.sessionID === getMainSessionID()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user