Skip to content

Commit d331b48

Browse files
authored
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 <[email protected]>
1 parent 4a38e70 commit d331b48

File tree

1 file changed

+23
-6
lines changed

1 file changed

+23
-6
lines changed

src/shared/command-executor.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,32 @@ import { existsSync } from "fs"
55
import { homedir } from "os"
66

77
const DEFAULT_ZSH_PATHS = ["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"]
8+
const DEFAULT_BASH_PATHS = ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash"]
89

910
function getHomeDir(): string {
1011
return process.env.HOME || process.env.USERPROFILE || homedir()
1112
}
1213

13-
function findZshPath(customZshPath?: string): string | null {
14-
if (customZshPath && existsSync(customZshPath)) {
15-
return customZshPath
14+
function findShellPath(defaultPaths: string[], customPath?: string): string | null {
15+
if (customPath && existsSync(customPath)) {
16+
return customPath
1617
}
17-
for (const path of DEFAULT_ZSH_PATHS) {
18+
for (const path of defaultPaths) {
1819
if (existsSync(path)) {
1920
return path
2021
}
2122
}
2223
return null
2324
}
2425

26+
function findZshPath(customZshPath?: string): string | null {
27+
return findShellPath(DEFAULT_ZSH_PATHS, customZshPath)
28+
}
29+
30+
function findBashPath(): string | null {
31+
return findShellPath(DEFAULT_BASH_PATHS)
32+
}
33+
2534
const execAsync = promisify(exec)
2635

2736
export interface CommandResult {
@@ -55,10 +64,18 @@ export async function executeHookCommand(
5564
let finalCommand = expandedCommand
5665

5766
if (options?.forceZsh) {
58-
const zshPath = options.zshPath || findZshPath()
67+
// Always verify shell exists before using it
68+
const zshPath = findZshPath(options.zshPath)
69+
const escapedCommand = expandedCommand.replace(/'/g, "'\\''")
5970
if (zshPath) {
60-
const escapedCommand = expandedCommand.replace(/'/g, "'\\''")
6171
finalCommand = `${zshPath} -lc '${escapedCommand}'`
72+
} else {
73+
// Fall back to bash login shell to preserve PATH from user profile
74+
const bashPath = findBashPath()
75+
if (bashPath) {
76+
finalCommand = `${bashPath} -lc '${escapedCommand}'`
77+
}
78+
// If neither zsh nor bash found, fall through to spawn with shell: true
6279
}
6380
}
6481

0 commit comments

Comments
 (0)