From 423807bd8b9aa38083d7517c4cae9b95be88cf99 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Sat, 24 Oct 2026 23:51:55 +0200 Subject: [PATCH 1/2] fix(setup): session-start hook runs a trusted, isolated interpreter The SessionStart hook runs outside the sandbox but took python3 from PATH, which project settings can set and which is often a uv-managed interpreter. It now uses only fixed system and Homebrew locations, then uv's managed CPython (read-only to the sandbox since #1785), first one 3.11 or newer, and runs it with -I and a one-line bootstrap instead of PYTHONPATH and -P, so no PYTHON* variable, user site-packages or the opened project reaches sys.path. Generated-by: Claude (Opus 5.5) --- plugins/magpie-setup/hooks/check-upgrade.sh | 29 ++++++++--- .../tests/test_session_hook.py | 51 +++++++++++++++++++ 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/plugins/magpie-setup/hooks/check-upgrade.sh b/plugins/magpie-setup/hooks/check-upgrade.sh index adef0acab..d1100c505 100755 --- a/plugins/magpie-setup/hooks/check-upgrade.sh +++ b/plugins/magpie-setup/hooks/check-upgrade.sh @@ -74,14 +74,29 @@ fi # Once per session, the isolated-setup warning: `setup_preflight` decides # whether it applies and prints the notice with it, as hook JSON whose -# `systemMessage` the user sees. Without python3 the notice goes out alone. -# `-P` keeps the working directory, which is the project being opened, off -# `sys.path`: `python3 -m` would otherwise put it ahead of `PYTHONPATH`, and -# a `setup_preflight/` package committed to that project would run in place -# of the plugin's own. +# `systemMessage` the user sees. Without a usable Python the notice goes out +# alone. +# +# The hook runs outside the sandbox, so the interpreter is never `python3` +# from PATH, which project settings can set: only fixed system and Homebrew +# locations, then uv's managed CPython (which the reference setup keeps +# read-only to the sandbox, apache/magpie#1785). It runs with `-I`, so no +# PYTHON* variable, user site-packages or working directory (the project +# being opened, which could carry its own `setup_preflight/`) reaches +# `sys.path`; a one-line bootstrap puts the plugin's own source there. +python="" +for candidate in /opt/homebrew/bin/python3 /usr/local/bin/python3 /usr/bin/python3 \ + "$HOME"/.local/share/uv/python/cpython-3.*/bin/python3; do + [ -x "$candidate" ] || continue + case "$candidate" in *freethreaded*) continue ;; esac + "$candidate" -I -c 'import sys; sys.exit(sys.version_info < (3, 11))' >/dev/null 2>&1 || continue + python="$candidate" + break +done output="$notice" -if command -v python3 >/dev/null 2>&1; then - output="$(PYTHONPATH="$root/skills/setup" python3 -P -m setup_preflight.isolated session-start \ +if [ -n "$python" ]; then + bootstrap='import runpy, sys; sys.path.insert(0, sys.argv.pop(1)); runpy.run_module("setup_preflight.isolated", run_name="__main__", alter_sys=True)' + output="$("$python" -I -c "$bootstrap" "$root/skills/setup" session-start \ --project-root "${CLAUDE_PROJECT_DIR:-$PWD}" --notice "$notice" 2>/dev/null)" || output="$notice" fi diff --git a/tools/setup-preflight/tests/test_session_hook.py b/tools/setup-preflight/tests/test_session_hook.py index edd0e2dee..bf07a058f 100644 --- a/tools/setup-preflight/tests/test_session_hook.py +++ b/tools/setup-preflight/tests/test_session_hook.py @@ -54,3 +54,54 @@ def test_a_package_in_the_project_does_not_shadow_the_plugin(project: Path, tmp_ assert not marker.exists() assert "PLANTED" not in done.stdout assert "Apache Magpie plugin" in done.stdout + + +def test_a_python3_on_path_is_never_run(project: Path, tmp_path: Path) -> None: + """The hook runs outside the sandbox; PATH comes from settings a project can set.""" + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + marker = tmp_path / "path-python-ran" + fake = bin_dir / "python3" + fake.write_text(f"#!/bin/sh\ntouch {marker}\nexit 0\n") + fake.chmod(0o755) + env_path = f"{bin_dir}:{os.environ['PATH']}" + done = subprocess.run( + ["bash", str(HOOK)], + input="{}", + capture_output=True, + text=True, + env={ + **os.environ, + "PATH": env_path, + "CLAUDE_PLUGIN_ROOT": str(PLUGIN_ROOT), + "CLAUDE_PLUGIN_DATA": str(tmp_path / "data"), + "CLAUDE_PROJECT_DIR": str(project), + }, + cwd=project, + check=False, + ) + assert done.returncode == 0, done.stderr + assert not marker.exists() + assert "Apache Magpie plugin" in done.stdout + + +def test_pythonpath_does_not_reach_the_preflight(project: Path, tmp_path: Path) -> None: + """-I drops PYTHONPATH: a planted setup_preflight there never runs.""" + planted = tmp_path / "pp" / "setup_preflight" + planted.mkdir(parents=True) + marker = tmp_path / "pp-ran" + code = f"open({str(marker)!r}, 'w').close()\n" + (planted / "__init__.py").write_text(code, encoding="utf-8") + (planted / "isolated.py").write_text(code, encoding="utf-8") + env = { + **os.environ, + "PYTHONPATH": str(tmp_path / "pp"), + "CLAUDE_PLUGIN_ROOT": str(PLUGIN_ROOT), + "CLAUDE_PLUGIN_DATA": str(tmp_path / "data"), + "CLAUDE_PROJECT_DIR": str(project), + } + done = subprocess.run( + ["bash", str(HOOK)], input="{}", capture_output=True, text=True, env=env, cwd=project, check=False + ) + assert done.returncode == 0, done.stderr + assert not marker.exists() From f98db0712435f908f733b028d2a7337ea4a1dad5 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Sat, 24 Oct 2026 23:56:23 +0200 Subject: [PATCH 2/2] fix(setup): session-start hook trusts system Python only, with -I -S Review follow-up. Trusting uv-managed interpreters needs the gateway hook's full writable-root checks, and this hook only prints a notice, so it now takes only fixed system and Homebrew locations (which no sandbox grant may cover) and otherwise prints its own notice alone. It runs with -I -S: setup_preflight needs only the standard library. Generated-by: Claude (Opus 5.5) --- plugins/magpie-setup/hooks/check-upgrade.sh | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/plugins/magpie-setup/hooks/check-upgrade.sh b/plugins/magpie-setup/hooks/check-upgrade.sh index d1100c505..b2f4cbb85 100755 --- a/plugins/magpie-setup/hooks/check-upgrade.sh +++ b/plugins/magpie-setup/hooks/check-upgrade.sh @@ -79,24 +79,25 @@ fi # # The hook runs outside the sandbox, so the interpreter is never `python3` # from PATH, which project settings can set: only fixed system and Homebrew -# locations, then uv's managed CPython (which the reference setup keeps -# read-only to the sandbox, apache/magpie#1785). It runs with `-I`, so no -# PYTHON* variable, user site-packages or working directory (the project -# being opened, which could carry its own `setup_preflight/`) reaches -# `sys.path`; a one-line bootstrap puts the plugin's own source there. +# locations, which no sandbox grant may cover (the container-gateway hook +# refuses to run at all when one does). uv-managed interpreters are left out: +# trusting them needs the gateway hook's full writable-root checks, and this +# hook only prints a notice, so without a system Python it prints its own. +# It runs with `-I -S`, so no PYTHON* variable, site-packages or working +# directory (the project being opened, which could carry its own +# `setup_preflight/`) reaches `sys.path`; a one-line bootstrap puts the +# plugin's own source there. `setup_preflight` needs only the stdlib. python="" -for candidate in /opt/homebrew/bin/python3 /usr/local/bin/python3 /usr/bin/python3 \ - "$HOME"/.local/share/uv/python/cpython-3.*/bin/python3; do +for candidate in /opt/homebrew/bin/python3 /usr/local/bin/python3 /usr/bin/python3; do [ -x "$candidate" ] || continue - case "$candidate" in *freethreaded*) continue ;; esac - "$candidate" -I -c 'import sys; sys.exit(sys.version_info < (3, 11))' >/dev/null 2>&1 || continue + "$candidate" -I -S -c 'import sys; sys.exit(sys.version_info < (3, 11))' >/dev/null 2>&1 || continue python="$candidate" break done output="$notice" if [ -n "$python" ]; then bootstrap='import runpy, sys; sys.path.insert(0, sys.argv.pop(1)); runpy.run_module("setup_preflight.isolated", run_name="__main__", alter_sys=True)' - output="$("$python" -I -c "$bootstrap" "$root/skills/setup" session-start \ + output="$("$python" -I -S -c "$bootstrap" "$root/skills/setup" session-start \ --project-root "${CLAUDE_PROJECT_DIR:-$PWD}" --notice "$notice" 2>/dev/null)" || output="$notice" fi