[security-guidance] Every hook fails with ENOENT on Windows when python3 is a Python Install Manager alias; Stop hook's asyncRewake then loops forever

Author: MK-AIFyCreated Sep 16, 2026Updated Sep 16, 2026

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 directory

The 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.exe

A 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.

bash
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.exe entries for Python Install Manager, then add %LOCALAPPDATA%\Python\pythoncore-3.14-64 to the user PATH. (python3 then fails the probe → shim falls through to python → real exe.)
  • Or install a python.org build and disable the python3 alias 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