From faa0ac29840b5a9e15a499a30c812477b24171f6 Mon Sep 17 00:00:00 2001 From: varuntej07 Date: Fri, 17 Jul 2026 23:25:32 -0700 Subject: [PATCH] fix(install): emit hook exe paths with forward slashes so Git Bash does not strip them Claude Code runs command-type PreToolUse hooks through Git Bash by default on Windows. The resolved exe was emitted as a raw backslash path and quoted only when it contained a space, so a space-free path like C:\Users\me\graphify.EXE reached settings.json unquoted. Git Bash treats the unquoted backslashes as escapes and strips them, producing 'C:Usersmegraphify.EXE: command not found', so every graph guard silently fails. Normalize the path to forward slashes in _resolve_graphify_exe (a no-op on POSIX), fixing the Claude, Gemini, and Codex hook emitters at once. --- graphify/install.py | 28 ++++++++++++++++++---------- tests/test_search_hook.py | 12 ++++++++++++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/graphify/install.py b/graphify/install.py index 0ad0750a5..c0afe97fe 100644 --- a/graphify/install.py +++ b/graphify/install.py @@ -1293,23 +1293,31 @@ def _uninstall_opencode_plugin(project_dir: Path) -> None: config_file.write_text(json.dumps(config, indent=2), encoding="utf-8") print(f" {_OPENCODE_CONFIG_PATH} -> plugin deregistered") def _resolve_graphify_exe() -> str: - """Return the absolute path to the graphify executable. + """Return the absolute path to the graphify executable, with forward slashes. Falls back to bare 'graphify' if resolution fails. Using an absolute path ensures the hook works in environments where the venv Scripts/ directory is not on PATH (e.g. VS Code Codex extension on Windows). + + The path is normalized to forward slashes so it survives every shell that + runs the hook command. On Windows, Claude Code runs command-type hooks + through Git Bash by default, where an unquoted backslash is an escape + character: a raw ``C:\\Users\\me\\graphify.EXE`` collapses to + ``C:Usersmegraphify.EXE: command not found`` and the guard silently fails. + Forward slashes are accepted by Git Bash, cmd.exe, and PowerShell alike, and + ``.replace`` is a no-op on POSIX where paths already use forward slashes. """ import shutil found = shutil.which("graphify") - if found: - return found - # Derive from sys.executable: same Scripts/ (Windows) or bin/ (Unix) dir - scripts_dir = Path(sys.executable).parent - for name in ("graphify.exe", "graphify"): - candidate = scripts_dir / name - if candidate.exists(): - return str(candidate) - return "graphify" + if not found: + # Derive from sys.executable: same Scripts/ (Windows) or bin/ (Unix) dir + scripts_dir = Path(sys.executable).parent + for name in ("graphify.exe", "graphify"): + candidate = scripts_dir / name + if candidate.exists(): + found = str(candidate) + break + return (found or "graphify").replace("\\", "/") def _install_codex_hook(project_dir: Path) -> None: """Add graphify PreToolUse hook to .codex/hooks.json.""" hooks_path = project_dir / ".codex" / "hooks.json" diff --git a/tests/test_search_hook.py b/tests/test_search_hook.py index 4c34fcea1..bcca886c4 100644 --- a/tests/test_search_hook.py +++ b/tests/test_search_hook.py @@ -39,6 +39,18 @@ def test_matcher_targets_bash(): assert _search_matcher()["matcher"] == "Bash" +def test_hook_command_has_no_backslashes(monkeypatch): + # On Windows the resolved exe is a backslash path; Claude Code runs command + # hooks through Git Bash by default, which treats an unquoted backslash as an + # escape character and strips it (C:\Users\me\graphify.EXE -> C:Usersme...), + # breaking every guard. The emitted command must use forward slashes. + from graphify.__main__ import _resolve_graphify_exe + monkeypatch.setattr("shutil.which", lambda _name: r"C:\Users\me\graphify.EXE") + assert _resolve_graphify_exe() == "C:/Users/me/graphify.EXE" + for h in _claude_pretooluse_hooks(): + assert "\\" not in h["hooks"][0]["command"] + + def test_command_has_no_shell_syntax(): # #522: no POSIX bash that Windows cmd.exe/PowerShell can't parse. cmd = _search_matcher()["hooks"][0]["command"]