From 96a0d8af07006848292c654d4b22f9b1fc0cf981 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Fri, 23 Oct 2026 15:12:31 +0200 Subject: [PATCH] leetcode launcher: stop exporting PYTHONPATH into the coached session The REPL passes os.environ to `claude -p`, so a PYTHONPATH set by the launcher reached every hook and every Bash call of the coached session. The old launcher did not do that. The launcher now runs `/usr/bin/python3 -P -m leetcode_coach.leetcode_repl` and relies on install.sh's editable install, the same way the Stop-hook shim does. A test now pins that the launcher sets no PYTHONPATH. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PjvF5DZG9JduSBYKxqyysi --- leetcode_coach/leetcode_coach/leetcode | 11 +++++------ .../leetcode_coach/tests/test_leetcode_repl.py | 12 +++++++++++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/leetcode_coach/leetcode_coach/leetcode b/leetcode_coach/leetcode_coach/leetcode index 7b0b85b..ce07d85 100755 --- a/leetcode_coach/leetcode_coach/leetcode +++ b/leetcode_coach/leetcode_coach/leetcode @@ -6,13 +6,12 @@ # from a client that waits for hooks, and the interactive TUI streams first. # leetcode [what you are stuck on...] # -# Symlinked from ~/.local/bin/leetcode. The package lives in leetcode-guard; -# the repo root goes on PYTHONPATH so the launcher works from any checkout, -# installed or not, and -P keeps the caller's cwd off sys.path. +# Symlinked from ~/.local/bin/leetcode. The package is found through +# install.sh's editable install, like the Stop-hook shim; no PYTHONPATH here, +# because the REPL passes its environment on to `claude -p` and every hook and +# Bash call of the coached session would inherit it. -P keeps cwd off sys.path. # ============================================================================ set -euo pipefail -repo="$(dirname "$(dirname "$(readlink -f "$0")")")" -PYTHONPATH="${repo}${PYTHONPATH:+:${PYTHONPATH}}" \ - exec /usr/bin/python3 -P -m leetcode_coach.leetcode_repl "$@" +exec /usr/bin/python3 -P -m leetcode_coach.leetcode_repl "$@" diff --git a/leetcode_coach/leetcode_coach/tests/test_leetcode_repl.py b/leetcode_coach/leetcode_coach/tests/test_leetcode_repl.py index 3734974..3e7bd14 100644 --- a/leetcode_coach/leetcode_coach/tests/test_leetcode_repl.py +++ b/leetcode_coach/leetcode_coach/tests/test_leetcode_repl.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import os from pathlib import Path import subprocess from typing import Any @@ -167,8 +168,17 @@ def test_help_prints_usage(capsys: pytest.CaptureFixture[str]) -> None: def test_launcher_runs_the_package_repl() -> None: + # The real launcher finds the package through install.sh's editable + # install; PYTHONPATH stands in for it here (-P still honours it). + env = {**os.environ, "PYTHONPATH": str(LAUNCHER.resolve().parents[1])} proc = subprocess.run( - [str(LAUNCHER), "--help"], capture_output=True, text=True, check=False + [str(LAUNCHER), "--help"], capture_output=True, text=True, check=False, env=env ) assert proc.returncode == 0, proc.stderr assert proc.stdout.startswith("Usage: leetcode [--local]") + + +def test_launcher_does_not_leak_pythonpath_into_the_session() -> None: + """The REPL hands its environment to `claude -p`; the launcher adds nothing.""" + body = LAUNCHER.read_text(encoding="utf-8").split("set -euo pipefail", 1)[1] + assert "PYTHONPATH" not in body