diff --git a/plugins/security-guidance/hooks/ensure_agent_sdk.py b/plugins/security-guidance/hooks/ensure_agent_sdk.py index 4e34f176..697b5cce 100644 --- a/plugins/security-guidance/hooks/ensure_agent_sdk.py +++ b/plugins/security-guidance/hooks/ensure_agent_sdk.py @@ -28,7 +28,9 @@ NOOP_SYSTEM = 0 # claude_agent_sdk already importable in system python NOOP_VENV = 1 # venv already built and SDK imports from it BUILT = 2 # venv created + SDK pip-installed this run BUILD_FAILED = 3 # venv create or pip install raised/timed out -SKIP_WIN32 = 4 # Windows; consumer glob doesn't handle Lib/ layout +# Outcome 4 was previously SKIP_WIN32; retired now that the consumer glob in +# llm.py also matches Windows venv layout (Lib/site-packages). Don't reuse the +# value — telemetry rows from older plugin builds still emit 4. SKIP_SENTINEL = 5 # another SessionStart is currently building @@ -60,13 +62,6 @@ def main() -> tuple[int, str, str]: err_phase / err_kind are non-empty only on BUILD_FAILED — they let telemetry split bootstrap failures by root cause. """ - # Windows venv layout (Lib/site-packages, no python* subdir) isn't - # handled by the consumer's glob in security_reminder_hook.py; skip the - # bootstrap entirely rather than build a venv that's never read. - if sys.platform == "win32": - return SKIP_WIN32, "", "" - - if _sdk_on_syspath(): return NOOP_SYSTEM, "", "" @@ -75,7 +70,11 @@ def main() -> tuple[int, str, str]: or os.path.expanduser("~/.claude/security") ) venv = state_dir / "agent-sdk-venv" - venv_py = venv / "bin" / "python" + # Windows venvs put the interpreter at Scripts\python.exe; POSIX uses bin/python. + if sys.platform == "win32": + venv_py = venv / "Scripts" / "python.exe" + else: + venv_py = venv / "bin" / "python" # Another SessionStart (concurrent CC instance, same plugin) may already # be building. The sentinel lives NEXT TO the venv, not inside it — diff --git a/plugins/security-guidance/hooks/llm.py b/plugins/security-guidance/hooks/llm.py index eff56de7..11ef6ce9 100644 --- a/plugins/security-guidance/hooks/llm.py +++ b/plugins/security-guidance/hooks/llm.py @@ -298,9 +298,12 @@ def _call_claude_via_sdk(prompt, output_schema, *, max_tokens=16000, model=None) "SECURITY_WARNINGS_STATE_DIR", os.path.expanduser("~/.claude/security"), ) - for _sp in glob.glob( - os.path.join(_state_dir, "agent-sdk-venv", "lib", - "python*", "site-packages") + # POSIX venv: lib/pythonX.Y/site-packages + # Windows venv: Lib/site-packages (capital L, no pythonX.Y subdir) + _venv_root = os.path.join(_state_dir, "agent-sdk-venv") + for _sp in ( + glob.glob(os.path.join(_venv_root, "lib", "python*", "site-packages")) + + glob.glob(os.path.join(_venv_root, "Lib", "site-packages")) ): if os.path.isdir(_sp) and _sp not in sys.path: sys.path.insert(0, _sp) @@ -1094,9 +1097,12 @@ def agentic_review( "SECURITY_WARNINGS_STATE_DIR", os.path.expanduser("~/.claude/security"), ) - for _sp in glob.glob( - os.path.join(_state_dir, "agent-sdk-venv", "lib", - "python*", "site-packages") + # POSIX venv: lib/pythonX.Y/site-packages + # Windows venv: Lib/site-packages (capital L, no pythonX.Y subdir) + _venv_root = os.path.join(_state_dir, "agent-sdk-venv") + for _sp in ( + glob.glob(os.path.join(_venv_root, "lib", "python*", "site-packages")) + + glob.glob(os.path.join(_venv_root, "Lib", "site-packages")) ): if os.path.isdir(_sp) and _sp not in sys.path: sys.path.insert(0, _sp)