Files
oh-my-opencode/src/hooks/non-interactive-env/non-interactive-env-hook.ts
T
YeonGyu-Kim af66b8de0b refactor(hooks): replace direct output.args mutation with replaceToolArgs (env + prompt injectors)
Replace output.args.command and output.args.prompt direct assignments
with replaceToolArgs() in non-interactive-env, prometheus-md-only,
and sisyphus-junior-notepad hooks.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-18 12:37:45 +09:00

109 lines
3.5 KiB
TypeScript

import type { PluginInput } from "@opencode-ai/plugin"
import { HOOK_NAME, NON_INTERACTIVE_ENV, SHELL_COMMAND_PATTERNS } from "./constants"
import { log, buildEnvPrefix, replaceToolArgs } from "../../shared"
import { detectShellType, type ShellType } from "../../shared/shell-env"
export * from "./constants"
export * from "./detector"
export * from "./types"
const BANNED_COMMAND_PATTERNS = SHELL_COMMAND_PATTERNS.banned
.filter((command) => !command.includes("("))
.map((cmd) => new RegExp(`\\b${cmd}\\b`))
function detectBannedCommand(command: string): string | undefined {
for (let i = 0; i < BANNED_COMMAND_PATTERNS.length; i++) {
if (BANNED_COMMAND_PATTERNS[i].test(command)) {
return SHELL_COMMAND_PATTERNS.banned[i]
}
}
return undefined
}
function detectWindowsShellType(shellPath: string | undefined): ShellType | undefined {
if (!shellPath) {
return undefined
}
const shellName = shellPath.replace(/\\/g, "/").split("/").pop()?.toLowerCase()
if (shellName === "cmd" || shellName === "cmd.exe") {
return "cmd"
}
if (
shellName === "powershell" ||
shellName === "powershell.exe" ||
shellName === "pwsh" ||
shellName === "pwsh.exe"
) {
return "powershell"
}
return undefined
}
function detectCommandShellType(): ShellType {
if (process.platform === "win32" && process.env.SHELL) {
const shellType = detectWindowsShellType(process.env.SHELL)
if (shellType) {
return shellType
}
}
if (process.platform === "win32" && !process.env.SHELL && !process.env.MSYSTEM) {
return detectWindowsShellType(process.env.ComSpec) ?? "cmd"
}
return detectShellType()
}
export function createNonInteractiveEnvHook(_ctx: PluginInput) {
return {
"tool.execute.before": async (
input: { tool: string; sessionID: string; callID: string },
output: { args: Record<string, unknown>; message?: string }
): Promise<void> => {
if (input.tool.toLowerCase() !== "bash") {
return
}
const command = output.args.command as string | undefined
if (!command) {
return
}
const bannedCmd = detectBannedCommand(command)
if (bannedCmd) {
output.message = `Warning: '${bannedCmd}' is an interactive command that may hang in non-interactive environments.`
}
// Only prepend env vars for git commands (editor blocking, pager, etc.)
const isGitCommand = /\bgit\b/.test(command)
if (!isGitCommand) {
return
}
// NOTE: We intentionally removed the isNonInteractive() check here.
// Even when OpenCode runs in a TTY, the agent cannot interact with
// spawned bash processes. Git commands like `git rebase --continue`
// would open editors (vim/nvim) that hang forever.
// The env vars (GIT_EDITOR=:, EDITOR=:, etc.) must ALWAYS be injected
// for git commands to prevent interactive prompts.
const shellType = detectCommandShellType()
const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV, shellType)
// Check if the command already starts with the prefix to avoid stacking.
// This maintains the non-interactive behavior and makes the operation idempotent.
if (command.trim().startsWith(envPrefix.trim())) {
return
}
replaceToolArgs(output, { command: `${envPrefix} ${command}` })
log(`[${HOOK_NAME}] Prepended non-interactive env vars to git command`, {
sessionID: input.sessionID,
envPrefix,
})
},
}
}