Development
Working on captain-hook itself. For using captain-hook in your project, see Installation.
Setup
Clone the repo and sync the dev extra:
git clone https://github.com/yasyf/captain-hook
cd captain-hook
uv sync --extra devNLP assets
Tests that exercise NLP signals need the spaCy English model and WordNet data:
uv run python -c "from captain_hook.util.model_cache import ensure_nlp_resources; ensure_nlp_resources()"This mirrors what CI provisions — see .github/workflows/ci.yml.
Tests
Run the suite:
uv run pytestThe example hooks in docs/examples/ carry inline tests too:
uv run capt-hook --hooks docs/examples testDocs
The docs are Great Docs (Quarto), published to GitHub Pages. The API reference is generated from docstrings at build time; the CLI reference comes from the Click app.
Sync the docs group alongside the dev extra, then preview locally:
uv sync --extra dev --group docs
uv run great-docs previewBuild and smoke-test the wheel
Build the wheel, then install and exercise it in a throwaway venv:
uv build
uv venv --seed .wheel-smoke
uv pip install --python .wheel-smoke/bin/python dist/*.whl
.wheel-smoke/bin/capt-hook --helpUsing a local checkout
From a consumer project, point uv run --project at your clone:
uv run --project path/to/captain-hook capt-hook testLive hooks in this checkout
This repo dispatches its own hooks through "$CLAUDE_PROJECT_DIR"/.venv/bin/hook, wired by the committed .claude/settings.json — not through the plugin’s bin/hook wrapper that consumer repos get. The shim execs the fixed signed app, and the app fences dispatch on the exact wheel build, so a checkout whose .venv doesn’t match the installed app is rejected. That fence is deliberate: after a dependency change, run uv sync --extra dev and rebuild and install the same-version app, or live dispatch in this repo stays fenced off.
The plugin’s dispatch surface is the committed captain_hook/bin/hook symlink plus its hook.binrun descriptor; CI parse-validates the descriptor.
Releasing
Add the CHANGELOG entry and commit it, together with uv.lock when a dependency change moved it. Then tag the new version and push. .github/workflows/release-pypi.yml builds the sdist and wheel, publishes to PyPI via trusted publishing, cuts a GitHub release, builds and signs the universal Captain Hook.app with the exact same versioned capt-hookd, and publishes the stable release as the bundled application resource in the yasyf/tap/captain-hook formula. It also commits captain_hook/.claude-plugin/plugin.json back to main with its version synced to the tag — that bump is what makes consumer plugin caches pick up the release, so no manual plugin.json edit is needed (a hand-bump before tagging is harmless; the sync no-ops):
git tag v0.9.1
git push origin v0.9.1The release workflow re-derives the wheel version from the tag via uv version --frozen and stamps that version into the signed host — the committed pyproject.toml version is an inert 0.0.0 sentinel, so there is nothing to bump there either. The wheel, plugin, and app are one exact release unit; do not publish or document them as independently compatible artifacts. After the run goes green, git pull: the plugin.json sync commit landed on main, so your local branch is one commit behind.