Skip to content

post-commit hook's interpreter allowlist rejects backslashes → silent failure on Windows paths (despite the "covers Windows paths" comment) #2126

Description

@mhintz1980

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

  1. Install graphify on Windows (uv tool): uv tool install graphifyy
  2. 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.
  3. 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
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions