From de889addabe7eb145cd59c47603e00cc414d90ef Mon Sep 17 00:00:00 2001 From: "Aryan Singh K." <70511529+aryansk@users.noreply.github.com> Date: Mon, 24 Aug 2026 22:51:10 +0530 Subject: [PATCH 1/3] fix: clear Windows error for bash/git tools Fixes #54 On native Windows, shell=True invokes cmd.exe, not bash, so bash syntax fails with confusing subprocess errors. Detect missing bash (shutil.which) and fail with actionable message pointing to docs/windows.md and WSL2/Git Bash, instead of running cmd.exe. Same for git when not on PATH. Validation: py_compile passes, git diff --check clean; Windows check returns clear message when os.name == nt and no bash/git, otherwise proceeds as before. --- gcode/tools.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/gcode/tools.py b/gcode/tools.py index e919f37..01f1241 100644 --- a/gcode/tools.py +++ b/gcode/tools.py @@ -50,6 +50,15 @@ def execute_bash(command: str) -> str: Returns combined stdout and stderr, and reports a non-zero exit code if the command fails. """ + # Windows: shell=True invokes cmd.exe, not bash. Detect missing bash and + # fail with a clear, actionable message rather than a confusing subprocess + # error. See docs/windows.md and issue #54. + if os.name == "nt" and not shutil.which("bash"): + return ( + "execute_bash: bash not found on native Windows. GCode's bash tool " + "requires bash (use WSL2 or Git Bash). See docs/windows.md for Windows " + f"setup. Command was: {command}" + ) if not AUTO_APPROVE: try: confirm = input(f"GCode wants to run: {command}\nApprove? (y/n): ") @@ -338,6 +347,11 @@ def git_commit(message: str) -> str: def _git(args: list) -> str: + if os.name == "nt" and not shutil.which("git"): + return ( + "git not found on native Windows. Install Git for Windows and ensure " + "git is on PATH, or use WSL2/Git Bash. See docs/windows.md." + ) cmd = ["git"] + args try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, check=False) From a4e8c50a0bb165c0a6cc3ab9888de94f7e806a3d Mon Sep 17 00:00:00 2001 From: shauryagangrade <288927048+shauryagangrade@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:17:03 +0530 Subject: [PATCH 2/3] fix: clear Windows error for bash/git tools: run bash explicitly on Windows (shell=True uses cmd.exe) --- gcode/tools.py | 32 ++++++++++++++++++++++++++------ tests/test_tools.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/gcode/tools.py b/gcode/tools.py index 01f1241..ce7ff08 100644 --- a/gcode/tools.py +++ b/gcode/tools.py @@ -50,10 +50,13 @@ def execute_bash(command: str) -> str: Returns combined stdout and stderr, and reports a non-zero exit code if the command fails. """ - # Windows: shell=True invokes cmd.exe, not bash. Detect missing bash and + # Windows: shell=True invokes cmd.exe, not bash. If bash is unavailable, # fail with a clear, actionable message rather than a confusing subprocess - # error. See docs/windows.md and issue #54. - if os.name == "nt" and not shutil.which("bash"): + # error. When bash exists, run it explicitly so bash syntax works (shell=True + # on Windows would still route through cmd.exe). See docs/windows.md and + # issue #54. + bash = shutil.which("bash") + if os.name == "nt" and bash is None: return ( "execute_bash: bash not found on native Windows. GCode's bash tool " "requires bash (use WSL2 or Git Bash). See docs/windows.md for Windows " @@ -72,9 +75,26 @@ def execute_bash(command: str) -> str: if confirm.strip().lower() != "y": return "Command execution cancelled by user." try: - result = subprocess.run( # nosec B602 — execute_bash is the tool's purpose; gated by y/n approval - command, shell=True, capture_output=True, text=True, timeout=BASH_TIMEOUT, check=False - ) + # nosec B602 — execute_bash is the tool's purpose; gated by y/n approval. + # On Windows, shell=True would invoke cmd.exe; run bash explicitly. + if os.name == "nt": + assert bash is not None # presence checked above + result = subprocess.run( + [bash, "-c", command], + capture_output=True, + text=True, + timeout=BASH_TIMEOUT, + check=False, + ) + else: + result = subprocess.run( # nosec B602 — gated by y/n approval + command, + shell=True, + capture_output=True, + text=True, + timeout=BASH_TIMEOUT, + check=False, + ) except subprocess.TimeoutExpired: return f"Command timed out after {BASH_TIMEOUT}s: {command}" except KeyboardInterrupt: diff --git a/tests/test_tools.py b/tests/test_tools.py index 739ad43..2154aab 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -174,6 +174,45 @@ def test_execute_bash_auto_approve_skips_prompt(tmp_path): set_auto_approve(AUTO_APPROVE) +def test_execute_bash_windows_missing_bash_returns_clear_error(): + from gcode.tools import AUTO_APPROVE, execute_bash, set_auto_approve + + set_auto_approve(True) + try: + with ( + patch("gcode.tools.os.name", "nt"), + patch("gcode.tools.shutil.which", return_value=None), + ): + out = execute_bash.invoke({"command": "echo hi"}) + finally: + set_auto_approve(AUTO_APPROVE) + assert "bash not found on native Windows" in out + + +def test_execute_bash_windows_runs_bash_explicitly(tmp_path, monkeypatch): + """On Windows, shell=True would invoke cmd.exe; bash must run explicitly.""" + from gcode.tools import AUTO_APPROVE, execute_bash, set_auto_approve + + monkeypatch.chdir(tmp_path) + set_auto_approve(True) + try: + with ( + patch("gcode.tools.os.name", "nt"), + patch("gcode.tools.shutil.which", return_value="C:/Program Files/Git/bin/bash.exe"), + patch("gcode.tools.subprocess.run") as run, + ): + run.return_value.returncode = 0 + run.return_value.stdout = "hi from bash\n" + run.return_value.stderr = "" + out = execute_bash.invoke({"command": "echo hi"}) + finally: + set_auto_approve(AUTO_APPROVE) + assert "hi from bash" in out + cmd, kwargs = run.call_args + assert cmd[0] == ["C:/Program Files/Git/bin/bash.exe", "-c", "echo hi"] + assert kwargs.get("shell") is not True + + def test_grep_passes_include_as_one_argument(): """The glob must stay attached to --include, as --include=. From 3f8ddd96dc430c69affff7d1df34643998aa4925 Mon Sep 17 00:00:00 2001 From: shauryagangrade <288927048+shauryagangrade@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:23:25 +0530 Subject: [PATCH 3/3] fix: clear Windows error for bash/git tools: avoid bandit B101 assert in Windows bash path --- gcode/tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gcode/tools.py b/gcode/tools.py index ce7ff08..fa3dd43 100644 --- a/gcode/tools.py +++ b/gcode/tools.py @@ -78,7 +78,8 @@ def execute_bash(command: str) -> str: # nosec B602 — execute_bash is the tool's purpose; gated by y/n approval. # On Windows, shell=True would invoke cmd.exe; run bash explicitly. if os.name == "nt": - assert bash is not None # presence checked above + if bash is None: + return "execute_bash: bash not found on native Windows." result = subprocess.run( [bash, "-c", command], capture_output=True,