Skip to content

Conversation

@ananas-viber
Copy link
Contributor

@ananas-viber ananas-viber commented Jan 6, 2026

Summary

  • Fix hook commands failing silently on systems without zsh installed
  • Add bash login shell fallback to preserve user PATH when zsh unavailable
  • Graceful degradation: zsh → bash → default shell

Changes

  • Refactored findZshPath into generic findShellPath helper function
  • Added DEFAULT_BASH_PATHS constant and findBashPath() function
  • Updated executeHookCommand to cascade: zsh -lc → bash -lc → spawn shell:true
  • Always verify shell exists before using it (fixes the root cause)

Before:

const zshPath = options.zshPath || findZshPath()  // Bug: skips existence check
if (zshPath) {
  finalCommand = `${zshPath} -lc '${escapedCommand}'`
}

After:

const zshPath = findZshPath(options.zshPath)  // Always verifies existence
if (zshPath) {
  finalCommand = `${zshPath} -lc '${escapedCommand}'`
} else {
  const bashPath = findBashPath()  // Fallback preserves login shell PATH
  if (bashPath) {
    finalCommand = `${bashPath} -lc '${escapedCommand}'`
  }
}

Testing

Tested on Ubuntu Linux without zsh installed:

# Stock opencode v1.1.3 + patched oh-my-opencode
opencode run "Add comment to TestComponent.tsx"

# Result: All PreToolUse hooks fire correctly
# - delegation-enforcer.sh: ✅ Captured Edit tool
# - test-hook.sh: ✅ Captured Glob, Read tools

PATH comparison (why bash -lc matters):

Shell PATH entries
/bin/sh (spawn default) 19
bash -lc 21
zsh -lc 21

The login shell flag (-l) sources user profile files, ensuring custom PATH additions are available for hooks.

Related Issues

No existing issue - discovered during opencode plugin testing.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 6, 2026

All contributors have signed the CLA. Thank you! ✅
Posted by the CLA Assistant Lite bot.

@ananas-viber
Copy link
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@ananas-viber
Copy link
Contributor Author

recheck

github-actions bot added a commit that referenced this pull request Jan 6, 2026
@ananas-viber ananas-viber force-pushed the fix/zsh-existence-check branch from c41a4d7 to 09c0122 Compare January 6, 2026 13:23
@ananas-viber
Copy link
Contributor Author

recheck

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.
@ananas-viber ananas-viber force-pushed the fix/zsh-existence-check branch from 09c0122 to 53deb91 Compare January 6, 2026 13:28
@ananas-viber
Copy link
Contributor Author

recheck

@ananas-viber ananas-viber marked this pull request as ready for review January 6, 2026 13:34
@greptile-apps
Copy link

greptile-apps bot commented Jan 6, 2026

Greptile Summary

Fixed silent hook execution failures on systems without zsh by verifying shell existence before use. Refactored findZshPath into generic findShellPath helper and added bash fallback (zsh -lcbash -lc → default shell) to preserve user PATH from login shell profiles.

Key changes:

  • Created findShellPath() helper that always verifies shell existence via existsSync()
  • Added DEFAULT_BASH_PATHS constant and findBashPath() function for fallback
  • Updated executeHookCommand() to cascade through zsh → bash → spawn with shell:true
  • Moved escapedCommand calculation outside conditional to avoid duplication

Impact:
Hooks now work correctly on Linux systems without zsh installed (previously failed silently). The bash fallback with -lc flag preserves PATH entries from user profile, maintaining parity with zsh behavior.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The refactoring improves robustness by always verifying shell existence before use, fixing a legitimate bug where hooks would fail silently on systems without zsh. The fallback cascade (zsh → bash → default) is well-designed and preserves existing behavior for systems with zsh while gracefully degrading for those without. Code is clean, follows project conventions, and has been tested on Ubuntu without zsh installed.
  • No files require special attention

Important Files Changed

Filename Overview
src/shared/command-executor.ts Refactored shell path discovery to verify existence before use, added bash fallback when zsh unavailable - fixes silent hook failures on systems without zsh

Sequence Diagram

sequenceDiagram
    participant Hook as Hook Caller
    participant EHC as executeHookCommand()
    participant FZP as findZshPath()
    participant FBP as findBashPath()
    participant FSP as findShellPath()
    participant Spawn as spawn()

    Hook->>EHC: executeHookCommand(cmd, stdin, cwd, {forceZsh: true, zshPath})
    EHC->>EHC: Expand ~ and $CLAUDE_PROJECT_DIR
    
    alt forceZsh enabled
        EHC->>FZP: findZshPath(options.zshPath)
        FZP->>FSP: findShellPath(DEFAULT_ZSH_PATHS, customPath)
        FSP->>FSP: Check customPath exists
        alt customPath exists
            FSP-->>FZP: return customPath
            FZP-->>EHC: return zshPath
            EHC->>EHC: finalCommand = zshPath -lc 'escapedCommand'
        else customPath not found
            FSP->>FSP: Check DEFAULT_ZSH_PATHS
            alt zsh found in defaults
                FSP-->>FZP: return path
                FZP-->>EHC: return zshPath
                EHC->>EHC: finalCommand = zshPath -lc 'escapedCommand'
            else zsh not found
                FSP-->>FZP: return null
                FZP-->>EHC: return null
                EHC->>FBP: findBashPath() [FALLBACK]
                FBP->>FSP: findShellPath(DEFAULT_BASH_PATHS)
                FSP->>FSP: Check DEFAULT_BASH_PATHS
                alt bash found
                    FSP-->>FBP: return bashPath
                    FBP-->>EHC: return bashPath
                    EHC->>EHC: finalCommand = bashPath -lc 'escapedCommand'
                else bash not found
                    FSP-->>FBP: return null
                    FBP-->>EHC: return null
                    EHC->>EHC: finalCommand = expandedCommand [spawn with shell:true]
                end
            end
        end
    else forceZsh disabled
        EHC->>EHC: finalCommand = expandedCommand
    end
    
    EHC->>Spawn: spawn(finalCommand, {shell: true, cwd, env})
    Spawn->>Spawn: Execute hook command
    Spawn-->>EHC: {exitCode, stdout, stderr}
    EHC-->>Hook: return CommandResult
Loading

@greptile-apps
Copy link

greptile-apps bot commented Jan 6, 2026

Greptile found no issues!

From now on, if a review finishes and we haven't found any issues, we will not post anything, but you can confirm that we reviewed your changes in the status check section.

This feature can be toggled off in your Code Review Settings by deselecting "Create a status check for each PR".

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

@ananas-viber
Copy link
Contributor Author

ananas-viber commented Jan 6, 2026

@junhoyeo @code-yeongyu For your attention, would appreciate if you can review.

@code-yeongyu code-yeongyu merged commit d331b48 into code-yeongyu:dev Jan 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants