fix: verify zsh exists before using it for hook execution (#544)
The `forceZsh` option on Linux/macOS would use a hardcoded zshPath without checking if zsh actually exists on the system. This caused hook commands to fail silently with exit code 127 on systems without zsh installed. Changes: - Always verify zsh exists via findZshPath() before using it - Fall back to bash -lc if zsh not found (preserves login shell PATH) - Fall through to spawn with shell:true if neither found The bash fallback ensures user PATH from .profile/.bashrc is available, which is important for hooks that depend on custom tool locations. Tested with opencode v1.1.3 - PreToolUse hooks now execute correctly on systems without zsh. Co-authored-by: Anas Viber <ananas-viber@users.noreply.github.com>
This commit is contained in:
@@ -5,16 +5,17 @@ import { existsSync } from "fs"
|
|||||||
import { homedir } from "os"
|
import { homedir } from "os"
|
||||||
|
|
||||||
const DEFAULT_ZSH_PATHS = ["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"]
|
const DEFAULT_ZSH_PATHS = ["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"]
|
||||||
|
const DEFAULT_BASH_PATHS = ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash"]
|
||||||
|
|
||||||
function getHomeDir(): string {
|
function getHomeDir(): string {
|
||||||
return process.env.HOME || process.env.USERPROFILE || homedir()
|
return process.env.HOME || process.env.USERPROFILE || homedir()
|
||||||
}
|
}
|
||||||
|
|
||||||
function findZshPath(customZshPath?: string): string | null {
|
function findShellPath(defaultPaths: string[], customPath?: string): string | null {
|
||||||
if (customZshPath && existsSync(customZshPath)) {
|
if (customPath && existsSync(customPath)) {
|
||||||
return customZshPath
|
return customPath
|
||||||
}
|
}
|
||||||
for (const path of DEFAULT_ZSH_PATHS) {
|
for (const path of defaultPaths) {
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
@@ -22,6 +23,14 @@ function findZshPath(customZshPath?: string): string | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findZshPath(customZshPath?: string): string | null {
|
||||||
|
return findShellPath(DEFAULT_ZSH_PATHS, customZshPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
function findBashPath(): string | null {
|
||||||
|
return findShellPath(DEFAULT_BASH_PATHS)
|
||||||
|
}
|
||||||
|
|
||||||
const execAsync = promisify(exec)
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
export interface CommandResult {
|
export interface CommandResult {
|
||||||
@@ -55,10 +64,18 @@ export async function executeHookCommand(
|
|||||||
let finalCommand = expandedCommand
|
let finalCommand = expandedCommand
|
||||||
|
|
||||||
if (options?.forceZsh) {
|
if (options?.forceZsh) {
|
||||||
const zshPath = options.zshPath || findZshPath()
|
// Always verify shell exists before using it
|
||||||
|
const zshPath = findZshPath(options.zshPath)
|
||||||
|
const escapedCommand = expandedCommand.replace(/'/g, "'\\''")
|
||||||
if (zshPath) {
|
if (zshPath) {
|
||||||
const escapedCommand = expandedCommand.replace(/'/g, "'\\''")
|
|
||||||
finalCommand = `${zshPath} -lc '${escapedCommand}'`
|
finalCommand = `${zshPath} -lc '${escapedCommand}'`
|
||||||
|
} else {
|
||||||
|
// Fall back to bash login shell to preserve PATH from user profile
|
||||||
|
const bashPath = findBashPath()
|
||||||
|
if (bashPath) {
|
||||||
|
finalCommand = `${bashPath} -lc '${escapedCommand}'`
|
||||||
|
}
|
||||||
|
// If neither zsh nor bash found, fall through to spawn with shell: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user