diff --git a/CHANGELOG.md b/CHANGELOG.md index 83bfc7a71..fc4d46174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -139,6 +139,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu - Fix: Node subpath imports (`#services/foo` via a `package.json` `imports` map, including `*` wildcards and condition objects) now resolve to the mapped file — previously every `#`-specifier resolved to nothing (#3382, thanks @julien-e). - Fix: TS import-type normalization no longer scans every type-argument range per match (an O(matches × ranges) blowup that pinned extraction at 100% CPU on large mixed files); the filter is now a sorted-index lookup with byte-identical output (#3359, thanks @Sagexd08). - Fix: Dart extraction now stamps `source_location` (1-based `L{line}`) on nodes and edges, matching every other extractor, instead of leaving it null (#3365, thanks @ayushcodes10). +- Fix: the git post-commit hook and skill interpreter probes now strip a shebang argument before using it as a path, correctly parse `/usr/bin/env -S ...` launchers, and reject PATH-controlled env interpreter names before verification. A pipx launcher (`#!/.../python -E`) now resolves instead of silently falling back to a `python3` without graphify and printing "could not locate a Python with graphify installed" after every commit (#2629, thanks @hpstr5000). ## 0.9.55 (2026-09-05) diff --git a/graphify/hooks.py b/graphify/hooks.py index e78e291f6..ebe8c3cf8 100644 --- a/graphify/hooks.py +++ b/graphify/hooks.py @@ -80,8 +80,20 @@ *) _SHEBANG="" ;; esac case "$_SHEBANG" in - */env\\ *) GRAPHIFY_PYTHON="${_SHEBANG#*/env }" ;; - *) GRAPHIFY_PYTHON="$_SHEBANG" ;; + */env\\ -S\\ *) GRAPHIFY_PYTHON="${_SHEBANG#*/env -S }" ;; + */env\\ *) GRAPHIFY_PYTHON="${_SHEBANG#*/env }" ;; + *) GRAPHIFY_PYTHON="$_SHEBANG" ;; + esac + # Strip any interpreter argument: pipx writes `#!/.../python -E`, so the + # shebang carries a trailing argument that is not part of the path. Keep + # the leading word only, else the space trips the allowlist below and the + # valid interpreter is discarded (silent python3 fallback, #2629). + GRAPHIFY_PYTHON="${GRAPHIFY_PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$GRAPHIFY_PYTHON" in + */*|*\\\\*) ;; + *) GRAPHIFY_PYTHON= ;; esac # Allowlist: only keep characters valid in a filesystem path to prevent # injection if the shebang contains shell metacharacters. diff --git a/graphify/skill-agents.md b/graphify/skill-agents.md index 190827d9a..c6517940f 100644 --- a/graphify/skill-agents.md +++ b/graphify/skill-agents.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -661,8 +669,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-aider.md b/graphify/skill-aider.md index 4996beb78..38214597e 100644 --- a/graphify/skill-aider.md +++ b/graphify/skill-aider.md @@ -69,9 +69,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi diff --git a/graphify/skill-amp.md b/graphify/skill-amp.md index 190827d9a..c6517940f 100644 --- a/graphify/skill-amp.md +++ b/graphify/skill-amp.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -661,8 +669,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-claw.md b/graphify/skill-claw.md index abd2811d2..5f4efb9ac 100644 --- a/graphify/skill-claw.md +++ b/graphify/skill-claw.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-codex.md b/graphify/skill-codex.md index af3f723c7..a08a39813 100644 --- a/graphify/skill-codex.md +++ b/graphify/skill-codex.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -661,8 +669,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-copilot.md b/graphify/skill-copilot.md index abd2811d2..5f4efb9ac 100644 --- a/graphify/skill-copilot.md +++ b/graphify/skill-copilot.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-devin.md b/graphify/skill-devin.md index f9be846cb..4ff037efe 100644 --- a/graphify/skill-devin.md +++ b/graphify/skill-devin.md @@ -80,9 +80,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -881,8 +889,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-droid.md b/graphify/skill-droid.md index fd148d485..58396fdf3 100644 --- a/graphify/skill-droid.md +++ b/graphify/skill-droid.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -661,8 +669,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-kilo.md b/graphify/skill-kilo.md index 3e70b050a..5fa4dfbf8 100644 --- a/graphify/skill-kilo.md +++ b/graphify/skill-kilo.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-kiro.md b/graphify/skill-kiro.md index abd2811d2..5f4efb9ac 100644 --- a/graphify/skill-kiro.md +++ b/graphify/skill-kiro.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-opencode.md b/graphify/skill-opencode.md index 91ced6067..a009d686e 100644 --- a/graphify/skill-opencode.md +++ b/graphify/skill-opencode.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -656,8 +664,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-pi.md b/graphify/skill-pi.md index abd2811d2..5f4efb9ac 100644 --- a/graphify/skill-pi.md +++ b/graphify/skill-pi.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-trae.md b/graphify/skill-trae.md index 050667bc2..f4f9e4b16 100644 --- a/graphify/skill-trae.md +++ b/graphify/skill-trae.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -662,8 +670,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill-vscode.md b/graphify/skill-vscode.md index 20c7c0835..978458fe3 100644 --- a/graphify/skill-vscode.md +++ b/graphify/skill-vscode.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -660,8 +668,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/graphify/skill.md b/graphify/skill.md index abd2811d2..5f4efb9ac 100644 --- a/graphify/skill.md +++ b/graphify/skill.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tests/test_hooks.py b/tests/test_hooks.py index bd6d7e521..c803508b9 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -9,6 +9,14 @@ import pytest from graphify.hooks import install, uninstall, status, _hooks_dir, _HOOK_MARKER, _CHECKOUT_MARKER +REPO_ROOT = Path(__file__).resolve().parent.parent + + +def _with_repo_pythonpath(env: dict[str, str]) -> dict[str, str]: + current = env.get("PYTHONPATH") + env["PYTHONPATH"] = str(REPO_ROOT) if not current else str(REPO_ROOT) + os.pathsep + current + return env + def _make_git_repo(tmp_path: Path) -> Path: subprocess.run(["git", "init", str(tmp_path)], check=True, capture_output=True) @@ -235,6 +243,15 @@ def test_hook_check_no_additionalContext(tmp_path): ) _HOOK_SCRIPTS = [("post-commit", _HOOK_SCRIPT), ("post-checkout", _CHECKOUT_SCRIPT)] +SHEBANG_ARGUMENT_STRIP = '_SHEBANG="${_SHEBANG%% *}"' +INTERPRETER_GUARD_ARGUMENT_STRIP = 'PYTHON="${PYTHON%% *}"' +SHEBANG_PREFIX_STRIP = "sed 's/^#![[:space:]]*//'" +DESTRUCTIVE_SHEBANG_PREFIX_STRIP = "tr -d '#!'" +GRAPHIFY_PYTHON_MARKER = Path("graphify-out/.graphify_python") +ENV_SPLIT_SHEBANG_PREFIX = "#!/usr/bin/env -S " +CORRUPTED_INTERPRETER_MARKER = "corrupted" +FALLBACK_INTERPRETER_MARKER = "fallback" +PATH_CONTROLLED_INTERPRETER_MARKER = "path-controlled" @pytest.mark.parametrize("name,script", _HOOK_SCRIPTS) @@ -1533,3 +1550,310 @@ def test_commit_hook_still_rebuilds_for_a_source_change(tmp_path): result = _emitted_hook_run(repo, _HOOK_SCRIPT, [], {"GRAPHIFY_OUT": "custom-out"}) assert result.returncode == 0, result.stderr assert _LAUNCH_LINE in result.stdout, result.stdout + + +@pytest.mark.skipif(os.name == "nt", reason="POSIX shebang probe") +def test_hook_probe_resolves_pipx_shebang_with_argument(tmp_path): + """#2629: pipx writes `#!/.../python -E` — a shebang *with an argument*. + + The interpreter probe must strip that argument before using the shebang as a + path; otherwise the space trips the character allowlist, the valid pipx + interpreter is discarded, and the hook silently falls back to a `python3` + that has no graphify (printing 'could not locate a Python with graphify'). + """ + import sys + from graphify.hooks import _PYTHON_DETECT + + bindir = tmp_path / "bin" + bindir.mkdir() + launcher = bindir / "graphify" + # pipx-style launcher: shebang carries the `-E` argument. + launcher.write_text(f"#!{sys.executable} -E\nimport graphify\n", encoding="utf-8") + launcher.chmod(0o755) + + script = _PYTHON_DETECT + '\nprintf "%s" "$GRAPHIFY_PYTHON"\n' + env = _with_repo_pythonpath(dict(os.environ, PATH=f"{bindir}{os.pathsep}{os.environ['PATH']}")) + resolved = subprocess.run( + ["bash", "-c", script], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + ).stdout.strip() + + assert resolved == sys.executable, ( + f"probe did not resolve the pipx interpreter from a shebang with an " + f"argument; got {resolved!r}, expected {sys.executable!r}" + ) + + +@pytest.mark.skipif(os.name == "nt", reason="POSIX shebang probe") +def test_hook_probe_resolves_env_split_shebang_with_argument(tmp_path): + """The hook probe must treat `/usr/bin/env -S ...` as an env option wrapper, + not as the interpreter path.""" + import sys + from graphify.hooks import _PYTHON_DETECT + + bindir = tmp_path / "bin" + bindir.mkdir() + launcher = bindir / "graphify" + launcher.write_text( + f"{ENV_SPLIT_SHEBANG_PREFIX}{sys.executable} -E\n", + encoding="utf-8", + newline="\n", + ) + launcher.chmod(0o755) + + script = _PYTHON_DETECT + '\nprintf "%s" "$GRAPHIFY_PYTHON"\n' + env = _with_repo_pythonpath(dict(os.environ, PATH=f"{bindir}{os.pathsep}{os.environ['PATH']}")) + resolved = subprocess.run( + ["bash", "-c", script], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + ).stdout.strip() + + assert resolved == sys.executable + + +@pytest.mark.skipif(os.name == "nt", reason="POSIX shebang probe") +def test_hook_probe_rejects_path_controlled_env_split_interpreter(tmp_path): + """The hook probe must not retain an env-derived interpreter name from PATH.""" + from graphify.hooks import _PYTHON_DETECT + + bindir = tmp_path / "bin" + bindir.mkdir() + launcher = bindir / "graphify" + launcher.write_text( + f"{ENV_SPLIT_SHEBANG_PREFIX}python -E\n", + encoding="utf-8", + newline="\n", + ) + launcher.chmod(0o755) + path_controlled_python = bindir / "python" + path_controlled_python.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8", newline="\n") + path_controlled_python.chmod(0o755) + fallback_python = bindir / "python3" + fallback_python.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8", newline="\n") + fallback_python.chmod(0o755) + + script = _PYTHON_DETECT + '\nprintf "%s" "$GRAPHIFY_PYTHON"\n' + env = _with_repo_pythonpath(dict(os.environ, PATH=f"{bindir}{os.pathsep}/usr/bin{os.pathsep}/bin")) + resolved = subprocess.run( + ["bash", "-c", script], + cwd=tmp_path, + env=env, + capture_output=True, + text=True, + ).stdout.strip() + + assert resolved == "python3" + + +def test_generated_skill_probes_strip_shebang_argument(): + """#2629: both POSIX probe sites in the rendered skill (step-1 install and the + subcommand interpreter guard) must strip a shebang argument, so pipx + `#!/.../python -E` launchers resolve instead of silently reverting to python3.""" + skill_paths = sorted((Path(__file__).resolve().parent.parent / "graphify").glob("skill*.md")) + assert skill_paths + for skill_path in skill_paths: + if skill_path.name == "skill-windows.md": + continue + skill = skill_path.read_text() + assert DESTRUCTIVE_SHEBANG_PREFIX_STRIP not in skill, ( + f"{skill_path.name} removes every #/! character instead of only the shebang prefix" + ) + assert SHEBANG_PREFIX_STRIP in skill, ( + f"{skill_path.name} does not remove the shebang prefix structurally" + ) + assert SHEBANG_ARGUMENT_STRIP in skill, ( + f"{skill_path.name} step-1 probe does not strip shebang argument" + ) + if skill_path.name != "skill-aider.md": + assert INTERPRETER_GUARD_ARGUMENT_STRIP in skill, ( + f"{skill_path.name} interpreter guard does not strip shebang argument" + ) + + +def test_monolith_sources_strip_shebang_argument(): + """#2629: the aider/devin monolith sources also carry the POSIX probe and must + not preserve the pipx shebang-argument bug.""" + fragment_root = Path(__file__).resolve().parent.parent / "tools" / "skillgen" / "fragments" + monolith_paths = [ + fragment_root / "core" / "aider.md", + fragment_root / "core" / "devin.md", + ] + for monolith_path in monolith_paths: + monolith = monolith_path.read_text() + assert DESTRUCTIVE_SHEBANG_PREFIX_STRIP not in monolith, ( + f"{monolith_path.name} removes every #/! character instead of only the shebang prefix" + ) + assert SHEBANG_PREFIX_STRIP in monolith, ( + f"{monolith_path.name} does not remove the shebang prefix structurally" + ) + assert SHEBANG_ARGUMENT_STRIP in monolith, ( + f"{monolith_path.name} probe does not strip shebang argument" + ) + assert INTERPRETER_GUARD_ARGUMENT_STRIP in (fragment_root / "core" / "devin.md").read_text() + + +@pytest.mark.skipif(shutil.which("sh") is None, reason="sh required to run emitted guard") +def test_interpreter_guard_rejects_truncated_shebang_path(tmp_path): + """#2629: stripping a shebang argument must not adopt a truncated path when the + interpreter path itself contains a space. The guard must verify the candidate + and fall back to python3 instead of writing a broken .graphify_python marker.""" + fragment = ( + Path(__file__).resolve().parent.parent + / "tools" + / "skillgen" + / "fragments" + / "shell" + / "interpreter-guard-posix.md" + ).read_text() + guard = fragment.split("```bash", 1)[1].split("```", 1)[0] + bindir = tmp_path / "bin" + bindir.mkdir() + launcher = bindir / "graphify" + launcher.write_text(f"#!{tmp_path / 'John Doe' / 'python'} -E\n", encoding="utf-8") + launcher.chmod(0o755) + fallback = bindir / "python3" + fallback.write_text( + "#!/bin/sh\nmkdir -p graphify-out\nprintf '%s' \"$0\" > graphify-out/.graphify_python\n", + encoding="utf-8", + newline="\n", + ) + fallback.chmod(0o755) + env = _with_repo_pythonpath(dict(os.environ)) + env["PATH"] = str(bindir) + os.pathsep + env["PATH"] + result = subprocess.run(["sh", "-c", guard], cwd=tmp_path, env=env, capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + assert (tmp_path / GRAPHIFY_PYTHON_MARKER).read_text() == str(fallback) + + +@pytest.mark.skipif(shutil.which("sh") is None, reason="sh required to run emitted guard") +def test_interpreter_guard_resolves_env_split_shebang_argument(tmp_path): + """The subcommand guard must parse `/usr/bin/env -S python -E` by selecting + the interpreter after `-S`, not the `-S` option itself.""" + import sys + + fragment = ( + Path(__file__).resolve().parent.parent + / "tools" + / "skillgen" + / "fragments" + / "shell" + / "interpreter-guard-posix.md" + ).read_text() + guard = fragment.split("```bash", 1)[1].split("```", 1)[0] + bindir = tmp_path / "bin" + bindir.mkdir() + launcher = bindir / "graphify" + launcher.write_text( + f"{ENV_SPLIT_SHEBANG_PREFIX}{sys.executable} -E\n", + encoding="utf-8", + newline="\n", + ) + launcher.chmod(0o755) + fallback = bindir / "python3" + fallback.write_text( + f"#!/bin/sh\nmkdir -p graphify-out\nprintf '{FALLBACK_INTERPRETER_MARKER}' > graphify-out/.graphify_python\n", + encoding="utf-8", + newline="\n", + ) + fallback.chmod(0o755) + env = _with_repo_pythonpath(dict(os.environ)) + env["PATH"] = str(bindir) + os.pathsep + env["PATH"] + result = subprocess.run(["sh", "-c", guard], cwd=tmp_path, env=env, capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + assert (tmp_path / GRAPHIFY_PYTHON_MARKER).read_text() == sys.executable + + +@pytest.mark.skipif(shutil.which("sh") is None, reason="sh required to run emitted guard") +def test_interpreter_guard_rejects_path_controlled_env_split_interpreter(tmp_path): + """The subcommand guard must not verify an env-derived interpreter name via PATH.""" + fragment = ( + Path(__file__).resolve().parent.parent + / "tools" + / "skillgen" + / "fragments" + / "shell" + / "interpreter-guard-posix.md" + ).read_text() + guard = fragment.split("```bash", 1)[1].split("```", 1)[0] + bindir = tmp_path / "bin" + bindir.mkdir() + launcher = bindir / "graphify" + launcher.write_text( + f"{ENV_SPLIT_SHEBANG_PREFIX}python -E\n", + encoding="utf-8", + newline="\n", + ) + launcher.chmod(0o755) + path_controlled_python = bindir / "python" + path_controlled_python.write_text( + f"#!/bin/sh\nmkdir -p graphify-out\nprintf '{PATH_CONTROLLED_INTERPRETER_MARKER}' > graphify-out/.graphify_python\n", + encoding="utf-8", + newline="\n", + ) + path_controlled_python.chmod(0o755) + fallback = bindir / "python3" + fallback.write_text( + f"#!/bin/sh\nmkdir -p graphify-out\nprintf '{FALLBACK_INTERPRETER_MARKER}' > graphify-out/.graphify_python\n", + encoding="utf-8", + newline="\n", + ) + fallback.chmod(0o755) + env = _with_repo_pythonpath(dict(os.environ)) + env["PATH"] = str(bindir) + os.pathsep + env["PATH"] + result = subprocess.run(["sh", "-c", guard], cwd=tmp_path, env=env, capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + assert (tmp_path / GRAPHIFY_PYTHON_MARKER).read_text() == FALLBACK_INTERPRETER_MARKER + + +@pytest.mark.skipif(shutil.which("sh") is None, reason="sh required to run emitted guard") +def test_interpreter_guard_does_not_corrupt_shebang_path_before_allowlist(tmp_path): + """The subcommand guard must remove only the leading `#!` marker. + + `tr -d '#!'` deletes every `#` and `!` in the line, so a rejected interpreter + path can be rewritten into a different allowed path and executed. + """ + fragment = ( + Path(__file__).resolve().parent.parent + / "tools" + / "skillgen" + / "fragments" + / "shell" + / "interpreter-guard-posix.md" + ).read_text() + guard = fragment.split("```bash", 1)[1].split("```", 1)[0] + bindir = tmp_path / "bin" + bindir.mkdir() + launcher = bindir / "graphify" + rejected_interpreter = tmp_path / "py!bad" / "python" + corrupted_interpreter = tmp_path / "pybad" / "python" + corrupted_interpreter.parent.mkdir() + corrupted_interpreter.write_text( + f"#!/bin/sh\nmkdir -p graphify-out\nprintf '{CORRUPTED_INTERPRETER_MARKER}' > graphify-out/.graphify_python\n", + encoding="utf-8", + newline="\n", + ) + corrupted_interpreter.chmod(0o755) + launcher.write_text(f"#!{rejected_interpreter} -E\n", encoding="utf-8", newline="\n") + launcher.chmod(0o755) + fallback = bindir / "python3" + fallback.write_text( + f"#!/bin/sh\nmkdir -p graphify-out\nprintf '{FALLBACK_INTERPRETER_MARKER}' > graphify-out/.graphify_python\n", + encoding="utf-8", + newline="\n", + ) + fallback.chmod(0o755) + env = dict(os.environ) + env["PATH"] = str(bindir) + os.pathsep + env["PATH"] + result = subprocess.run(["sh", "-c", guard], cwd=tmp_path, env=env, capture_output=True, text=True) + + assert result.returncode == 0, result.stderr + assert (tmp_path / GRAPHIFY_PYTHON_MARKER).read_text() == FALLBACK_INTERPRETER_MARKER diff --git a/tests/test_skillgen.py b/tests/test_skillgen.py index ab6f94474..dbd2a9dfd 100644 --- a/tests/test_skillgen.py +++ b/tests/test_skillgen.py @@ -597,6 +597,19 @@ def test_monolith_roundtrip_passes_for_aider_and_devin(): assert problems == [], f"[{key}]\n" + "\n".join(problems) +@pytest.mark.parametrize("line", [ + "# Only trust shebang-derived interpreter paths. Env command names like", + "# `python` are PATH-controlled; explicit fallbacks handle them below.", + ' case "$_SHEBANG" in */*|*\\\\*) ;; *) _SHEBANG="" ;; esac', + ' ""|*[!a-zA-Z0-9/_.@-]*) ;;', + '# `python` are PATH-controlled; use the explicit python3 fallback.', + ' case "$PYTHON" in */*|*\\\\*) ;; *) PYTHON="python3" ;; esac', +]) +def test_monolith_shebang_argument_fix_sanctions_path_control_lines(line): + """The monolith drift guard permits the pathless-env hardening lines.""" + assert gen._is_shebang_argument_fix_line(line) + + def test_monoliths_change_only_sanctioned_lines(): """Every line that differs from pristine v8 is a sanctioned change-class. diff --git a/tools/skillgen/expected/graphify__skill-agents.md b/tools/skillgen/expected/graphify__skill-agents.md index 190827d9a..c6517940f 100644 --- a/tools/skillgen/expected/graphify__skill-agents.md +++ b/tools/skillgen/expected/graphify__skill-agents.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -661,8 +669,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-aider.md b/tools/skillgen/expected/graphify__skill-aider.md index 4996beb78..38214597e 100644 --- a/tools/skillgen/expected/graphify__skill-aider.md +++ b/tools/skillgen/expected/graphify__skill-aider.md @@ -69,9 +69,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi diff --git a/tools/skillgen/expected/graphify__skill-amp.md b/tools/skillgen/expected/graphify__skill-amp.md index 190827d9a..c6517940f 100644 --- a/tools/skillgen/expected/graphify__skill-amp.md +++ b/tools/skillgen/expected/graphify__skill-amp.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -661,8 +669,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-claw.md b/tools/skillgen/expected/graphify__skill-claw.md index abd2811d2..5f4efb9ac 100644 --- a/tools/skillgen/expected/graphify__skill-claw.md +++ b/tools/skillgen/expected/graphify__skill-claw.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-codex.md b/tools/skillgen/expected/graphify__skill-codex.md index af3f723c7..a08a39813 100644 --- a/tools/skillgen/expected/graphify__skill-codex.md +++ b/tools/skillgen/expected/graphify__skill-codex.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -661,8 +669,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-copilot.md b/tools/skillgen/expected/graphify__skill-copilot.md index abd2811d2..5f4efb9ac 100644 --- a/tools/skillgen/expected/graphify__skill-copilot.md +++ b/tools/skillgen/expected/graphify__skill-copilot.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-devin.md b/tools/skillgen/expected/graphify__skill-devin.md index f9be846cb..4ff037efe 100644 --- a/tools/skillgen/expected/graphify__skill-devin.md +++ b/tools/skillgen/expected/graphify__skill-devin.md @@ -80,9 +80,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -881,8 +889,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-droid.md b/tools/skillgen/expected/graphify__skill-droid.md index fd148d485..58396fdf3 100644 --- a/tools/skillgen/expected/graphify__skill-droid.md +++ b/tools/skillgen/expected/graphify__skill-droid.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -661,8 +669,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-kilo.md b/tools/skillgen/expected/graphify__skill-kilo.md index 3e70b050a..5fa4dfbf8 100644 --- a/tools/skillgen/expected/graphify__skill-kilo.md +++ b/tools/skillgen/expected/graphify__skill-kilo.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-kiro.md b/tools/skillgen/expected/graphify__skill-kiro.md index abd2811d2..5f4efb9ac 100644 --- a/tools/skillgen/expected/graphify__skill-kiro.md +++ b/tools/skillgen/expected/graphify__skill-kiro.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-opencode.md b/tools/skillgen/expected/graphify__skill-opencode.md index 91ced6067..a009d686e 100644 --- a/tools/skillgen/expected/graphify__skill-opencode.md +++ b/tools/skillgen/expected/graphify__skill-opencode.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -656,8 +664,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-pi.md b/tools/skillgen/expected/graphify__skill-pi.md index abd2811d2..5f4efb9ac 100644 --- a/tools/skillgen/expected/graphify__skill-pi.md +++ b/tools/skillgen/expected/graphify__skill-pi.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-trae.md b/tools/skillgen/expected/graphify__skill-trae.md index 050667bc2..f4f9e4b16 100644 --- a/tools/skillgen/expected/graphify__skill-trae.md +++ b/tools/skillgen/expected/graphify__skill-trae.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -662,8 +670,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill-vscode.md b/tools/skillgen/expected/graphify__skill-vscode.md index 20c7c0835..978458fe3 100644 --- a/tools/skillgen/expected/graphify__skill-vscode.md +++ b/tools/skillgen/expected/graphify__skill-vscode.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -660,8 +668,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/expected/graphify__skill.md b/tools/skillgen/expected/graphify__skill.md index abd2811d2..5f4efb9ac 100644 --- a/tools/skillgen/expected/graphify__skill.md +++ b/tools/skillgen/expected/graphify__skill.md @@ -75,9 +75,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -664,8 +672,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/fragments/core/aider.md b/tools/skillgen/fragments/core/aider.md index 4996beb78..38214597e 100644 --- a/tools/skillgen/fragments/core/aider.md +++ b/tools/skillgen/fragments/core/aider.md @@ -69,9 +69,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi diff --git a/tools/skillgen/fragments/core/devin.md b/tools/skillgen/fragments/core/devin.md index f9be846cb..4ff037efe 100644 --- a/tools/skillgen/fragments/core/devin.md +++ b/tools/skillgen/fragments/core/devin.md @@ -80,9 +80,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi @@ -881,8 +889,17 @@ Before running any subcommand below (`--update`, `--cluster-only`, `query`, `pat if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/fragments/shell/interpreter-guard-posix.md b/tools/skillgen/fragments/shell/interpreter-guard-posix.md index f7e344881..97db4b801 100644 --- a/tools/skillgen/fragments/shell/interpreter-guard-posix.md +++ b/tools/skillgen/fragments/shell/interpreter-guard-posix.md @@ -2,8 +2,17 @@ if [ ! -f graphify-out/.graphify_python ]; then GRAPHIFY_BIN=$(which graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then - PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + PYTHON=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` and strip any shebang argument (pipx + # writes `.../python -E`) before the allowlist check, else the space + # forces the unverified python3 fallback into .graphify_python (#2629). + case "$PYTHON" in */env\ -S\ *) PYTHON="${PYTHON#*/env -S }" ;; */env\ *) PYTHON="${PYTHON#*/env }" ;; esac + PYTHON="${PYTHON%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; use the explicit python3 fallback. + case "$PYTHON" in */*|*\\*) ;; *) PYTHON="python3" ;; esac case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac + if [ "$PYTHON" != "python3" ] && ! "$PYTHON" -c "import graphify" 2>/dev/null; then PYTHON="python3"; fi else PYTHON="python3" fi diff --git a/tools/skillgen/fragments/shell/posix.md b/tools/skillgen/fragments/shell/posix.md index 3534417d2..717d409d4 100644 --- a/tools/skillgen/fragments/shell/posix.md +++ b/tools/skillgen/fragments/shell/posix.md @@ -9,9 +9,17 @@ if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then fi # 2. Read shebang from graphify binary (pipx and direct pip installs) if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then - _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + _SHEBANG=$(head -n 1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') + # Resolve `/usr/bin/env -S python` / `/usr/bin/env python` to the interpreter, then strip any argument + # (pipx writes `.../python -E`) so a shebang argument is not mistaken for the + # path and rejected by the allowlist below (silent python3 fallback, #2629). + case "$_SHEBANG" in */env\ -S\ *) _SHEBANG="${_SHEBANG#*/env -S }" ;; */env\ *) _SHEBANG="${_SHEBANG#*/env }" ;; esac + _SHEBANG="${_SHEBANG%% *}" + # Only trust shebang-derived interpreter paths. Env command names like + # `python` are PATH-controlled; explicit fallbacks handle them below. + case "$_SHEBANG" in */*|*\\*) ;; *) _SHEBANG="" ;; esac case "$_SHEBANG" in - *[!a-zA-Z0-9/_.@-]*) ;; + ""|*[!a-zA-Z0-9/_.@-]*) ;; *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; esac fi diff --git a/tools/skillgen/gen.py b/tools/skillgen/gen.py index 09e19ede0..130fcfff1 100644 --- a/tools/skillgen/gen.py +++ b/tools/skillgen/gen.py @@ -1076,6 +1076,44 @@ def _is_shebang_allowlist_fix_line(line: str) -> bool: return "[!a-zA-Z0-9/_." in line +def _is_shebang_argument_fix_line(line: str) -> bool: + """Whether a line is part of the pipx shebang-argument fix (#2629). + + The POSIX interpreter probes read a launcher shebang and treat it as the + interpreter path. pipx can append an argument like ``-E`` to that shebang; + the probes now remove only the leading ``#!`` marker, resolve + ``/usr/bin/env -S`` / ``/usr/bin/env``, and keep only the leading word + before validating/running the interpreter. The generated guard also rejects + pathless env-derived command names before writing ``.graphify_python``. Only + those explanatory comments and shell guard lines are sanctioned for the + aider/devin monoliths. + """ + stripped = line.strip() + return ( + stripped.startswith("# Resolve `/usr/bin/env -S python`") + or stripped.startswith("# (pipx writes `.../python -E`)") + or stripped.startswith("# writes `.../python -E`)") + or stripped.startswith("# path and rejected by the allowlist below") + or stripped.startswith("# forces the unverified python3 fallback") + or stripped.startswith("# Only trust shebang-derived interpreter paths") + or stripped.startswith("# `python` are PATH-controlled") + or stripped == '_SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d \'#!\')' + or stripped == "_SHEBANG=$(head -n 1 \"$GRAPHIFY_BIN\" | sed 's/^#![[:space:]]*//')" + or stripped.startswith('case "$_SHEBANG" in */env\\ *)') + or stripped.startswith('case "$_SHEBANG" in */env\\ -S\\ *)') + or stripped == '_SHEBANG="${_SHEBANG%% *}"' + or stripped == 'case "$_SHEBANG" in */*|*\\\\*) ;; *) _SHEBANG="" ;; esac' + or stripped == '""|*[!a-zA-Z0-9/_.@-]*) ;;' + or stripped == 'PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d \'#!\')' + or stripped == "PYTHON=$(head -n 1 \"$GRAPHIFY_BIN\" | sed 's/^#![[:space:]]*//')" + or stripped.startswith('case "$PYTHON" in */env\\ *)') + or stripped.startswith('case "$PYTHON" in */env\\ -S\\ *)') + or stripped == 'PYTHON="${PYTHON%% *}"' + or stripped == 'case "$PYTHON" in */*|*\\\\*) ;; *) PYTHON="python3" ;; esac' + or stripped.startswith('if [ "$PYTHON" != "python3" ]') + ) + + def _is_obsidian_usage_comment_line(line: str) -> bool: """Whether a line is part of the ``/graphify`` usage-comment fix (#1681). @@ -1159,6 +1197,7 @@ def _is_community_label_export_fix_line(line: str) -> bool: _is_sensitive_reporting_fix_line, _is_no_api_key_fix_line, _is_shebang_allowlist_fix_line, + _is_shebang_argument_fix_line, _is_obsidian_usage_comment_line, _is_uv_from_interpreter_fix_line, _is_semantic_cache_scope_fix_line,