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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjvF5DZG9JduSBYKxqyysi
This commit is contained in:
Krzysztof Rudnicki 2026-10-23 15:12:31 +02:00
parent b730cffaf4
commit 96a0d8af07
2 changed files with 16 additions and 7 deletions

View File

@ -6,13 +6,12 @@
# from a client that waits for hooks, and the interactive TUI streams first.
# leetcode <url-or-slug> [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 "$@"

View File

@ -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