# 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](../../reference/configuration-prompts.md#captain_hook.HooksSettings) to declare your knobs, then read the resolved values off `evt.ctx.c` inside the handler.

``` python
"""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 None
```

> **Note: Verified by replay**
>
> The 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

``` bash
pytest tests/    # project configured a non-pytest test_command -- use the configured runner instead
```


# What it allows

``` bash
ls -la           # not a test invocation
ruff check .     # lint, not a test invocation
```

*The block / allow split mirrors the hook inline `tests`, so it stays true as the hook evolves.*


# Run it yourself

``` bash
uvx capt-hook --hooks docs/examples test
```


# See also

- [Configuration](../../docs/guide/packs.md#configure-hook-settings)
- [Conditions reference](../../docs/reference/conditions.md)
- [Primitives guide](../../docs/guide/primitives.md)
