Problem
The global Claude Code hooks in this repo (agentic-ai/Claude/hooks/, symlinked into ~/.claude/hooks/) shell out to Python tools by their bare console-script names. In uv-managed venvs and --no-sync workflows the package is importable but its console-script entry point isn't installed, so the bare command fails to spawn.
Hit while working in ulises-c/csen-346 (a uv project). post-test-runner.sh auto-detects:
elif [[ -f "$ROOT/pyproject.toml" || -f "$ROOT/pytest.ini" || -f "$ROOT/setup.py" ]]; then
TEST_CMD="pytest"
which produced, on every source edit:
post-test-runner: uv run pytest failed
error: Failed to spawn: `pytest`
Caused by: No such file or directory (os error 2)
Probe in that env (same package, two invocation styles):
| invocation |
result |
uv run --no-sync pytest |
❌ Failed to spawn |
uv run --no-sync python -m pytest |
✅ works |
uv run --no-sync pyright |
❌ Failed to spawn |
uv run --no-sync python -m pyright |
✅ works |
uv run --no-sync codespell |
❌ Failed to spawn |
uv run --no-sync python -m codespell_lib |
✅ works |
The same failure mode hit a project git pre-commit hook calling bare pyright / codespell / pytest (fixed downstream in csen-346).
Why python -m
python -m <module> resolves the package without needing the console-script bin (which is sometimes absent in uv venvs), and it composes with uv run --no-sync so the hook never mutates / re-resolves the environment — important for ML projects pinning ROCm/CUDA torch builds that a stray sync could clobber.
Proposed change
In post-test-runner.sh (and any other hook that invokes Python tools by bare name), prefer module invocation and detect uv:
elif [[ -f "$ROOT/pyproject.toml" || -f "$ROOT/pytest.ini" || -f "$ROOT/setup.py" ]]; then
if [[ -f "$ROOT/uv.lock" ]]; then
TEST_CMD="uv run --no-sync python -m pytest"
else
TEST_CMD="python -m pytest"
fi
Generalize the same pattern to any other Python tool the hooks call (e.g. pyright → python -m pyright, codespell → python -m codespell_lib).
Workaround already applied downstream
post-test-runner.sh honors a per-project .claude/test-cmd first; csen-346 now sets it to uv run --no-sync python -m pytest -q. But the default detection should be robust so each uv project doesn't need the override.
Acceptance criteria
post-test-runner.sh runs cleanly on a uv project with no .claude/test-cmd override present.
- Hooks invoke Python tooling via
python -m (no reliance on console-script bins).
- For uv projects, hooks use
--no-sync and never trigger an environment sync.
Context: ulises-c/csen-346#101 (downstream pre-commit + .claude/test-cmd fixes).
Problem
The global Claude Code hooks in this repo (
agentic-ai/Claude/hooks/, symlinked into~/.claude/hooks/) shell out to Python tools by their bare console-script names. Inuv-managed venvs and--no-syncworkflows the package is importable but its console-script entry point isn't installed, so the bare command fails to spawn.Hit while working in
ulises-c/csen-346(a uv project).post-test-runner.shauto-detects:which produced, on every source edit:
Probe in that env (same package, two invocation styles):
uv run --no-sync pytestuv run --no-sync python -m pytestuv run --no-sync pyrightuv run --no-sync python -m pyrightuv run --no-sync codespelluv run --no-sync python -m codespell_libThe same failure mode hit a project
gitpre-commit hook calling barepyright/codespell/pytest(fixed downstream in csen-346).Why
python -mpython -m <module>resolves the package without needing the console-script bin (which is sometimes absent in uv venvs), and it composes withuv run --no-syncso the hook never mutates / re-resolves the environment — important for ML projects pinning ROCm/CUDA torch builds that a stray sync could clobber.Proposed change
In
post-test-runner.sh(and any other hook that invokes Python tools by bare name), prefer module invocation and detect uv:Generalize the same pattern to any other Python tool the hooks call (e.g.
pyright→python -m pyright,codespell→python -m codespell_lib).Workaround already applied downstream
post-test-runner.shhonors a per-project.claude/test-cmdfirst;csen-346now sets it touv run --no-sync python -m pytest -q. But the default detection should be robust so each uv project doesn't need the override.Acceptance criteria
post-test-runner.shruns cleanly on a uv project with no.claude/test-cmdoverride present.python -m(no reliance on console-script bins).--no-syncand never trigger an environment sync.Context: ulises-c/csen-346#101 (downstream pre-commit +
.claude/test-cmdfixes).