[security-guidance] Every hook fails with ENOENT on Windows when python3 is a Python Install Manager alias; Stop hook's asyncRewake then loops forever
Plugin: security-guidance 2.0.8 (hooks/sg-python.sh)
Platform: Windows 11, Git Bash, Python 3.14 via the Python Install Manager (pymanager, Store/MSIX)
Harness: Claude Code desktop app (Windows), plugins materialized under %APPDATA%\Claude\local-agent-mode-sessions\<session>\rpm\plugin_*\
Symptom
Every hook invocation — PostToolUse (Edit/Write), the git commit/push hooks, and Stop/SubagentStop — fails with:
C:\Users\<me>\AppData\Local\Python\pythoncore-3.14-64\python.exe: can't open file
'C:\\Users\\<me>\\AppData\\Roaming\\Claude\\local-agent-mode-sessions\\...\\rpm\\plugin_01YBNfaNwQztYsnUydt8m47G\\hooks\\security_reminder_hook.py': [Errno 2] No such file or directoryThe file exists (116 KB, executable). ls from Git Bash sees it.
Because the Stop hook is declared with "asyncRewake": true, the harness treats that stderr as review findings and re-wakes the session; the re-woken turn's Stop fires the hook again, which fails again, which re-wakes again — an unbounded loop with no backoff. ~/.claude.json recorded "security-guidance@inline": { "usageCount": 5845 } in one afternoon.
Root cause
On this machine python3, python and py on PATH are all app execution aliases in %LOCALAPPDATA%\Microsoft\WindowsApps\, reparse-pointed into the Python Install Manager MSIX package:
WindowsApps/python3 -> C:/Program Files/WindowsApps/PythonSoftwareFoundation.PythonManager_26.3.240.0_x64__3847v3x7pw1km/python3.exeA process launched through that alias runs under the package's identity and gets AppData filesystem virtualization, so %APPDATA%\Roaming\Claude simply does not exist from its point of view. Same interpreter binary, two launch paths:
| Launched via | os.path.exists(r"C:\Users\<me>\AppData\Roaming\Claude") |
sees hook file |
|---|---|---|
WindowsApps\python3 alias |
False | False |
C:\Users\<me>\AppData\Local\Python\pythoncore-3.14-64\python.exe (full path) |
True | True |
sg-python.sh's probe ("$cmd" -c 'print(version)') passes for the alias — unlike the old Microsoft Store stub it was written to dodge, the pymanager alias is a real, working Python. It just can't see ${CLAUDE_PLUGIN_ROOT}. Pass 2 picks python3 first, so the alias always wins. Prepending the real install dir to PATH does not help: pymanager's install dir ships python.exe/pythonw.exe but no python3.exe, so python3 still resolves to the alias and is still tried first.
ensure_agent_sdk.py (SessionStart) fails the same way, so the SDK never installs either.
Suggested fix (two parts)
1. sg-python.sh: require that the interpreter can actually see the hook script, and prefer registered full-path installs over PATH names.
hook_script="$1"
can_see_hook() { # $@ = interpreter command
"$@" -c 'import os, sys; sys.exit(0 if os.path.exists(sys.argv[1]) else 1)' "$hook_script" 2>/dev/null
}
try_exec() { # $@ = interpreter command
v=$(probe "$@") || return 1
case "$v" in [0-9]*.[0-9]*) ;; *) return 1 ;; esac
if [ "${ALLOW_OLD:-0}" != "1" ] && ! is_sdk_compatible "$v"; then return 1; fi
can_see_hook "$@" || return 1
exec "$@" "${ARGS[@]}"
}
ARGS=("$@")
# Pass 0 — Windows: registered full-path installs from `py -0p` (never aliases)
if command -v py >/dev/null 2>&1; then
while IFS= read -r line; do
p=$(printf '%s' "$line" | sed -n 's/.*[[:space:]]\([A-Za-z]:\\.*python[w]*\.exe\)[[:space:]]*$/\1/p')
[ -n "$p" ] && try_exec "$p"
done < <(py -0p 2>/dev/null || true)
fi
# Pass 1..3 as today, but each candidate goes through try_exec (which now
# also checks can_see_hook).Verified locally: the existing security_reminder_hook.py, unchanged, exits 0 and produces its normal output when launched with the full-path interpreter.
2. Harness (Claude Code): asyncRewake should not re-wake on hook execution failure. A hook that exits non-zero with no JSON output and only an ENOENT/traceback on stderr is broken, not a set of findings. Re-injecting its stderr as a message — on a Stop hook, whose re-woken turn re-fires the same hook — guarantees an infinite loop. At minimum: don't rewake on exit ≥ 2 / non-JSON stderr, and add backoff/dedup for identical consecutive rewake payloads.
Workarounds for affected users (until fixed)
- Windows Settings → Apps → Advanced app settings → App execution aliases → turn off the
python3.exe,python.exe,py.exeentries for Python Install Manager, then add%LOCALAPPDATA%\Python\pythoncore-3.14-64to the user PATH. (python3then fails the probe → shim falls through topython→ real exe.) - Or install a python.org build and disable the
python3alias so it can't shadow it. - Or disable the Security Guidance plugin in the desktop app until this ships.
Generated with Claude Code
Source: anthropics/claude-plugins-official