Custom Condition

The built-in conditions (Tool, FilePath, Content, SourceEdits, …) cover the common cases, but you’ll eventually want a predicate that depends on something specific to your project — a line-count threshold, a project-specific file shape, or a computed property of the event.

from __future__ import annotations

from dataclasses import dataclass

from captain_hook import (
    Allow,
    BaseHookEvent,
    CustomCondition,
    Event,
    HookResult,
    Input,
    Warn,
    on,
)


@dataclass(frozen=True)
class LargeEdit(CustomCondition):
    max_lines: int = 50

    def check(self, evt: BaseHookEvent) -> bool:
        return evt.content is not None and evt.content.count("\n") > self.max_lines


@on(
    Event.PreToolUse,
    only_if=[LargeEdit(max_lines=10)],
    tests={
        Input(tool="Edit", content="\n".join(str(i) for i in range(20))): Warn(),
        Input(tool="Edit", content="one\ntwo\n"): Allow(),
    },
)
def warn_large_edit(evt: BaseHookEvent) -> HookResult | None:
    lines = evt.content.count("\n") if evt.content else 0
    return evt.warn(f"Large edit detected ({lines} lines) — consider splitting into smaller changes.")

What to learn: Any frozen dataclass that implements check(self, evt: BaseHookEvent) -> bool satisfies the CustomCondition protocol and slots straight into only_if= / skip_if= alongside built-in conditions. Keep them stateless; put any parameters on the dataclass fields so they participate in __hash__ and read cleanly at the call site.