Skip to content

fix(modern-python): python shim's suggested uv run command fails outside a project#195

Draft
thescribedevelopment wants to merge 1 commit into
trailofbits:mainfrom
thescribedevelopment:fix/modern-python-shim-uv-run-passthrough
Draft

fix(modern-python): python shim's suggested uv run command fails outside a project#195
thescribedevelopment wants to merge 1 commit into
trailofbits:mainfrom
thescribedevelopment:fix/modern-python-shim-uv-run-passthrough

Conversation

@thescribedevelopment

Copy link
Copy Markdown

Bug

The python/python3 PATH shim suggests uv run python ... — but outside a uv project that command can never succeed. uv run only shadows the shims with a managed venv bin/ when a project environment exists; outside a project, uv run python resolves the interpreter via ordinary PATH lookup, hits the shim again, and errors with the same suggestion:

$ cd ~   # no pyproject.toml anywhere above
$ uv run python3 script.py
ERROR: Use `uv run python3 script.py` instead of `python3 script.py`

The claim in README.md / setup-shims.sh ("uv run is unaffected because it prepends its managed virtualenv's bin/ to PATH") only holds inside a project. For a coding agent this is a hard dead end: the shim's own advice loops.

Fix

The python shim now detects the uv run context and passes through to the real interpreter:

  • Detection: uv ≥ 0.6.0 sets UV=<path-to-uv> in the environment of every subprocess it spawns. This is documented, public API (env-var reference; added in astral-sh/uv#11326, listed in the 0.6.0 changelog as uv-owned — uv overwrites user-set values). On uv < 0.6.0 the shim degrades fail-safe to today's blocking behavior.
  • Resolution: walk PATH, skip the shim itself with a same-file -ef test (robust to symlinked/aliased spellings of the shims dir — a naive pwd comparison can be tricked into an infinite exec loop, which we hit while testing), and exec the first real interpreter found. Empty PATH entries are normalized to . per execvp semantics.
  • Loop guard: exec preserves the PID, so the shim exports its PID before exec'ing; a shim that finds its own PID already recorded knows a second shim copy on PATH exec'd it (e.g. two plugin cache versions) and exits 127 with a clear error instead of ping-ponging forever.
  • Not-found: if UV is set but no real interpreter exists on PATH, the shim emits a distinct error (exit 127, mirroring the uv shim) instead of re-suggesting the command that just failed.
  • pip: python -m pip stays blocked everywhere, including under uv run (the block is hoisted above the passthrough).

Bare python/python3 invocations outside uv run behave exactly as before.

Design tradeoffs

  • UV=1 python3 evil.py bypasses the shim. Accepted: the shims are behavioral nudges for an agent, not a security boundary (absolute-path invocation always bypassed them), and the passthrough is silent, so no error text teaches the bypass.
  • Gating on a uv-managed interpreter instead would break the fix: outside a project uv resolves the system interpreter via PATH.
  • Changing the suggestion text to uv run script.py alone is insufficient: that form works today (uv provisions the interpreter itself), but python -c ..., bare REPL, and -m module invocations have no script-form equivalent.
  • The -m pip block matches the canonical spelling only (-mpip / options-before--m reach the interpreter). Same argv-level matching the shims already use; once a real interpreter can be exec'd, argv matching is inherently best-effort.
  • UV is inherited by descendants of any uv-launched process (including uvx / uv tool run), so the nudge is disabled in those subtrees — passthrough there is correct/harmless.
  • The sibling uv shim's PATH walk has the same latent aliased-spelling loop issue; happy to port the -ef fix in a follow-up if wanted.

Tests

python-shim.bats: existing 8 tests kept but made hermetic against ambient UV (previously, running the suite via uv run bats failed 5 tests and hung on the -m http.server case once the passthrough existed); 6 new tests cover passthrough with arg/exit-code forwarding (via both python and the python3 symlink), aliased-spelling skip, the two-copies loop guard (with timeout as a hang backstop), the distinct not-found error, and -m pip precedence under UV.

Verified locally: all 14 pass with bats 1.13 in a clean env and with UV leaked into the ambient env; full shims suite (37 tests) green; shellcheck -x (0.10.0) clean; shfmt -i 2 -ci (v3.12.0, the pre-commit pin) produces no diff; shim behavior exercised end-to-end on Ubuntu with uv 0.11.18 (bare → blocked; uv run python3 script.py / uv run python -c → run; uv pip → still blocked). Constructs also validated against bash 3.2.57 for macOS default-shell compatibility.

Version bumped 1.5.2 → 1.5.3 in plugin.json and the root marketplace.json per AGENTS.md.

🤖 Generated with Claude Code

… work outside a project

Outside a uv project there is no venv bin/ on PATH to shadow the shims,
so `uv run python script.py` resolves python via PATH lookup and hits
the shim again: the exact command the shim suggests can never succeed.

The python shim now detects the `uv run` context via the UV environment
variable (documented; set by uv >= 0.6.0 for all subprocesses it spawns)
and execs the real interpreter, found by walking PATH and skipping the
shim itself via a same-file (-ef) test. A PID-based guard (exec keeps
the PID) turns the two-shim-copies-on-PATH edge into a clean exit 127
instead of an exec loop, and `python -m pip` stays blocked everywhere.
Bare invocations outside `uv run` behave exactly as before.

Docs updated (README + setup-shims.sh claimed uv run was unaffected by
construction, which only holds inside a project). Existing bats tests
made hermetic against ambient UV; six new tests cover the passthrough,
symlink/aliased-spelling skip, loop guard, distinct not-found error,
and pip precedence.

Co-Authored-By: Claude Fable 5 <[email protected]>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants