Affected version
graphifyy 0.9.5 (uv tool install)
- File:
graphify/hooks.py
- runtime shell pattern: line 45 of the generated
_PYTHON_DETECT block (probe 2, .graphify_python)
- the analogous probe-3 pattern at line 83 is the same class without the
:
- install-time Python regex + comment: lines 502 & 505
- Shell:
/bin/sh (the hook's shebang), also reproduced under bash
Summary
The post-commit git hook's interpreter-detection allowlist is supposed to admit Windows paths (C:\Users\...), and the inline comment at install time explicitly claims it does:
# The allowlist includes ':' and '\' so Windows paths (C:\...) are accepted. (hooks.py line 502)
At install time this is true — line 505 uses Python re, where \ in a character class is a literal backslash, so the pinned path is accepted.
At runtime (when git fires the hook) this is false — line 45 uses a POSIX shell case glob, where \- inside [...] is parsed as "backslash-escaping the hyphen" (making - a literal hyphen, not a range operator). The backslash character itself is never added to the allowed set. Net effect: any Windows path containing \ is rejected by probe 2 (.graphify_python) and probe 3 (shebang), and the hook falls through to the "could not locate a Python" warning.
The same character sequence ([!a-zA-Z0-9/_.@:\-]) has opposite meaning in the two regex dialects — that is the bug.
Reproduce
- Install graphify on Windows (uv tool):
uv tool install graphifyy
- From a WSL/Linux shell on a shared repo (or any cross-host scenario), run
graphify hook install. The _PINNED placeholder gets a Linux path (/home/.../python3) — this is how the mismatch hides: the Linux pinned path is valid, so probe 1 succeeds and the bug in probe 2 is never exercised on the Linux side.
- On Windows, from Git-Bash-on-Windows, the
_PINNED Linux path does not exist. The hook falls to probe 2, which reads graphify-out/.graphify_python. On a Windows install that file contains the backslash form: C:\Users\<user>\AppData\Roaming\uv\tools\graphifyy\Scripts\python.exe
- Probe 2's allowlist rejects every
\ → path set to "" → falls to probe 3 (shebang of the launcher) → that also fails on Git-Bash → falls to the final warning on every commit.
Minimal character-level proof
The exact line-45 pattern [!a-zA-Z0-9/_.@:\-] tested char-by-char under /bin/sh:
| char |
result |
a / _ . @ : - |
ACCEPTED |
\ |
REJECTED |
+ ; |
REJECTED (correctly) |
Hyphen accepted, backslash rejected → confirms \ is being consumed as an escape for -, not admitted as a set member. The install-time comment's claim ("includes ''") does not hold at runtime.
Expected vs actual
- Expected:
.graphify_python containing C:\Users\...\python.exe is accepted; the hook launches a background rebuild silently.
- Actual: every commit from Git-Bash-on-Windows prints
[graphify hook] could not locate a Python with graphify installed. Add the graphify bin dir to PATH or re-run 'graphify hook install' from the env where graphify lives. The graph silently goes stale.
Non-blocking (the warning is on stderr; commits succeed), but under some conditions the failed hook has orphaned .git/index.lock, which then blocked cross-host commits via the shared /mnt/c filesystem in my setup.
Root cause
POSIX bracket-expression parsing of \-. Two reasonable fixes, either suffices:
Fix A — escape correctly for POSIX (minimal)
In hooks.py, line 45 (and the analogous probe-3 line 83), place the backslash where POSIX will treat it as a literal set member. The portable idiom is to list \ first in the bracket expression (or last), and list - last (so it can't be read as a range operator and needs no escape):
# before (line 45)
*[!a-zA-Z0-9/_.@:\-]*)
# after — backslash as a literal set member, hyphen last (no escape needed)
*[!\a-zA-Z0-9/_.@:-]*)
Fix B — normalize separators before the allowlist (more robust, recommended)
Before the case, convert any \ to / so the rest of the probe logic deals only with POSIX-style paths (which Git-Bash, sh, and the [ -x ] test all handle natively on Windows):
_FROM_FILE=$(cat "$_GFY_PYTHON_FILE" 2>/dev/null | tr -d '[:space:]' | tr '\' '/')
This matches what actually works in practice: a forward-slash form (C:/Users/.../python.exe) passes the current allowlist and execs successfully under Git-Bash. Fix B also fixes the shebang/probe-3 path uniformly and removes the dialect mismatch entirely.
I'd lean toward Fix B. Happy to open a PR for either.
Environment
- OS: Windows 11 (10.0.26300), Git-Bash (
/bin/sh = Git for Windows' bundled shell)
- graphifyy 0.9.5 via uv tool
- Repo shared between WSL and Windows (the cross-host case is where this bites hardest — a Windows-only repo re-installed from Windows would pin a
C:\... path into _PINNED, which then hits line 35's [ -x ] test; that test does accept backslash Windows paths on Git-Bash, so probe 1 would succeed — but only if reinstalled from Windows. The cross-host case where install happened from WSL is the reliable reproduce.)
Workaround I'm using locally
Converted graphify-out/.graphify_python to forward slashes manually. Works, but the file is gitignored and gets rewritten with backslashes on the next graphify hook install, so it's not durable. Hence this issue.
Affected version
graphifyy0.9.5 (uv tool install)graphify/hooks.py_PYTHON_DETECTblock (probe 2,.graphify_python):/bin/sh(the hook's shebang), also reproduced underbashSummary
The post-commit git hook's interpreter-detection allowlist is supposed to admit Windows paths (
C:\Users\...), and the inline comment at install time explicitly claims it does:At install time this is true — line 505 uses Python
re, where\in a character class is a literal backslash, so the pinned path is accepted.At runtime (when git fires the hook) this is false — line 45 uses a POSIX shell
caseglob, where\-inside[...]is parsed as "backslash-escaping the hyphen" (making-a literal hyphen, not a range operator). The backslash character itself is never added to the allowed set. Net effect: any Windows path containing\is rejected by probe 2 (.graphify_python) and probe 3 (shebang), and the hook falls through to the "could not locate a Python" warning.The same character sequence (
[!a-zA-Z0-9/_.@:\-]) has opposite meaning in the two regex dialects — that is the bug.Reproduce
uv tool install graphifyygraphify hook install. The_PINNEDplaceholder gets a Linux path (/home/.../python3) — this is how the mismatch hides: the Linux pinned path is valid, so probe 1 succeeds and the bug in probe 2 is never exercised on the Linux side._PINNEDLinux path does not exist. The hook falls to probe 2, which readsgraphify-out/.graphify_python. On a Windows install that file contains the backslash form:C:\Users\<user>\AppData\Roaming\uv\tools\graphifyy\Scripts\python.exe\→ path set to""→ falls to probe 3 (shebang of the launcher) → that also fails on Git-Bash → falls to the final warning on every commit.Minimal character-level proof
The exact line-45 pattern
[!a-zA-Z0-9/_.@:\-]tested char-by-char under/bin/sh:a/_.@:-\+;Hyphen accepted, backslash rejected → confirms
\is being consumed as an escape for-, not admitted as a set member. The install-time comment's claim ("includes ''") does not hold at runtime.Expected vs actual
.graphify_pythoncontainingC:\Users\...\python.exeis accepted; the hook launches a background rebuild silently.[graphify hook] could not locate a Python with graphify installed. Add the graphify bin dir to PATH or re-run 'graphify hook install' from the env where graphify lives.The graph silently goes stale.Non-blocking (the warning is on stderr; commits succeed), but under some conditions the failed hook has orphaned
.git/index.lock, which then blocked cross-host commits via the shared/mnt/cfilesystem in my setup.Root cause
POSIX bracket-expression parsing of
\-. Two reasonable fixes, either suffices:Fix A — escape correctly for POSIX (minimal)
In
hooks.py, line 45 (and the analogous probe-3 line 83), place the backslash where POSIX will treat it as a literal set member. The portable idiom is to list\first in the bracket expression (or last), and list-last (so it can't be read as a range operator and needs no escape):Fix B — normalize separators before the allowlist (more robust, recommended)
Before the
case, convert any\to/so the rest of the probe logic deals only with POSIX-style paths (which Git-Bash,sh, and the[ -x ]test all handle natively on Windows):_FROM_FILE=$(cat "$_GFY_PYTHON_FILE" 2>/dev/null | tr -d '[:space:]' | tr '\' '/')This matches what actually works in practice: a forward-slash form (
C:/Users/.../python.exe) passes the current allowlist and execs successfully under Git-Bash. Fix B also fixes the shebang/probe-3 path uniformly and removes the dialect mismatch entirely.I'd lean toward Fix B. Happy to open a PR for either.
Environment
/bin/sh= Git for Windows' bundled shell)C:\...path into_PINNED, which then hits line 35's[ -x ]test; that test does accept backslash Windows paths on Git-Bash, so probe 1 would succeed — but only if reinstalled from Windows. The cross-host case where install happened from WSL is the reliable reproduce.)Workaround I'm using locally
Converted
graphify-out/.graphify_pythonto forward slashes manually. Works, but the file is gitignored and gets rewritten with backslashes on the nextgraphify hook install, so it's not durable. Hence this issue.