Settings & Configuration¶
Different projects have different test runners, different lint allowlists, different VCS conventions. You want a single typed configuration object available in every hook handler, with sensible defaults and HOOKS_* environment-variable overrides.
from __future__ import annotations
from typing import cast
from captain_hook import (
BaseHookEvent,
Event,
HookResult,
HooksSettings,
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")])
def enforce_test_command(evt: BaseHookEvent) -> HookResult | None:
settings = cast(ProjectSettings, evt.ctx.c)
if (cl := evt.command_line) and cl.q.runs("pytest") and settings.test_command != "pytest":
return evt.block(
f"BLOCKED: use the project's configured test runner instead. "
f"Run: {settings.test_command}"
)
return None
What to learn: Subclass HooksSettings (a pydantic_settings.BaseSettings) to declare your project's knobs. Defaults live on the class; overrides come from the environment via the pydantic-settings machinery. At dispatch time, every handler can read the resolved settings as evt.ctx.c (also exposed as evt.ctx.conf and evt.ctx.settings) — no global imports, no module-level config singleton.