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.
This commit is contained in:
varuntej07
2026-07-18 19:06:51 +01:00
committed by safishamsi
parent e5a4b51d87
commit faa0ac2984
2 changed files with 30 additions and 10 deletions
+18 -10
View File
@@ -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"
+12
View File
@@ -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"]