From b383cafc440b969ffdbddc1e50357dd269e6fbdc Mon Sep 17 00:00:00 2001 From: teknium1 Date: Sun, 8 Mar 2026 03:00:05 -0700 Subject: [PATCH] refactor: rename and enhance shell detection in local environment Renamed _find_shell to _find_bash to clarify its purpose of specifically locating bash. Improved the shell detection logic to prioritize bash over the user's $SHELL, ensuring compatibility with the fence wrapper's syntax requirements. Added a backward compatibility alias for _find_shell to maintain existing imports in process_registry.py. --- tools/environments/local.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tools/environments/local.py b/tools/environments/local.py index 945e3349..e1df97b4 100644 --- a/tools/environments/local.py +++ b/tools/environments/local.py @@ -17,19 +17,19 @@ from tools.environments.base import BaseEnvironment _OUTPUT_FENCE = "__HERMES_FENCE_a9f7b3__" -def _find_shell() -> str: - """Find the best shell for command execution. +def _find_bash() -> str: + """Find bash for command execution. - On Unix: uses $SHELL, falls back to bash. + The fence wrapper uses bash syntax (semicolons, $?, printf), so we + must use bash — not the user's $SHELL which could be fish/zsh/etc. On Windows: uses Git Bash (bundled with Git for Windows). - Raises RuntimeError if no suitable shell is found on Windows. """ if not _IS_WINDOWS: return ( - os.environ.get("SHELL") - or shutil.which("bash") + shutil.which("bash") or ("/usr/bin/bash" if os.path.isfile("/usr/bin/bash") else None) or ("/bin/bash" if os.path.isfile("/bin/bash") else None) + or os.environ.get("SHELL") # last resort: whatever they have or "/bin/sh" ) @@ -59,6 +59,11 @@ def _find_shell() -> str: "Or set HERMES_GIT_BASH_PATH to your bash.exe location." ) + +# Backward compat — process_registry.py imports this name +_find_shell = _find_bash + + # Noise lines emitted by interactive shells when stdin is not a terminal. # Used as a fallback when output fence markers are missing. _SHELL_NOISE_SUBSTRINGS = ( @@ -159,13 +164,11 @@ class LocalEnvironment(BaseEnvironment): exec_command = self._prepare_command(command) try: - # Use the user's shell as an interactive login shell (-lic) so - # that ALL rc files are sourced — including content after the - # interactive guard in .bashrc (case $- in *i*)..esac) where - # tools like nvm, pyenv, and cargo install their init scripts. - # -l alone isn't enough: .profile sources .bashrc, but the guard - # returns early because the shell isn't interactive. - user_shell = _find_shell() + # The fence wrapper uses bash syntax (semicolons, $?, printf). + # Always use bash for the wrapper — NOT $SHELL which could be + # fish, zsh, or another shell with incompatible syntax. + # The -lic flags source rc files so tools like nvm/pyenv work. + user_shell = _find_bash() # Wrap with output fences so we can later extract the real # command output and discard shell init/exit noise. fenced_cmd = (