Skip to content

Claude hooks: invoke Python tools via python -m (uv / --no-sync venvs lack console scripts) #30

Description

@ulises-c

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. pyrightpython -m pyright, codespellpython -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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions