Settings & configuration
Give every hook handler a typed, per-project configuration object with defaults and environment overrides, so config-driven decisions stop relying on global imports.
Different projects ship different test runners, lint allowlists, and version-control conventions. You want one typed configuration object available in every handler, with sensible defaults and HOOKS_* environment-variable overrides — not a module-level singleton or a scatter of os.environ reads. Subclass HooksSettings to declare your knobs, then read the resolved values off evt.ctx.c inside the handler.
"""Enforce a project's configured test runner using typed per-project settings."""
from __future__ import annotations
from typing import cast
from captain_hook import (
Allow,
BaseHookEvent,
Event,
HookResult,
HooksSettings,
Input,
Tool,
on,
)
class ProjectSettings(HooksSettings):
test_command: str = "pytest"
require_tests_after_edit: bool = True
excluded_dirs: tuple[str, ...] = ("vendor", "node_modules")
@on(
Event.PreToolUse,
only_if=[Tool("Bash")],
tests={
Input(tool="Bash", command="ls -la"): Allow(),
Input(tool="Bash", command="ruff check ."): Allow(),
},
)
def enforce_test_command(evt: BaseHookEvent) -> HookResult | None:
settings = cast(ProjectSettings, evt.ctx.c)
if (cmd := evt.command).raw and cmd.q.runs("pytest") and settings.test_command != "pytest":
return evt.block(f"BLOCKED: use the project's configured test runner instead. Run: {settings.test_command}")
return NoneThe block fires only when the resolved test_command setting differs from pytest, so the live config-driven decision is exercised by real settings resolution rather than the inline tests, which assert the deterministic narrowing and allow paths.
What it catches
pytest tests/ # project configured a non-pytest test_command — use the configured runner insteadWhat it allows
ls -la # not a test invocation
ruff check . # lint, not a test invocationThe block / allow split mirrors the hook inline tests, so it stays true as the hook evolves.
Run it yourself
uvx capt-hook --hooks docs/examples test