Skip to content

captain_hook

captain_hook

InlineTests

InlineTests = dict[str | Input, Block | Warn | Allow]

Inline test specification mapping inputs to expected outcomes.

A mapping whose keys are Input descriptors (or legacy str session keys) and whose values are the expected hook result — Block, Warn, or Allow.

Keys

Input: Structured test-input descriptor specifying tool, command, file, content, and/or transcript context. str: Legacy session-key format — replayed from a recorded session during run_inline_tests when a fixture exists, else skipped.

Values

Block: The hook must block, optionally matching a regex pattern. Warn: The hook must warn, optionally matching a regex pattern. Allow: The hook must allow (return None or action "allow").

Example::

tests: InlineTests = {
    Input(command="rm -rf /"): Block(pattern="dangerous"),
    Input(command="ls"): Allow(),
}

Command dataclass

Command(
    raw: str,
    executable: str,
    args: tuple[str, ...],
    env: tuple[tuple[str, str], ...] = (),
    redirects: tuple[Redirect, ...] = (),
)

A single parsed shell command with executable, arguments, env vars, and redirects.

Use Command.parse(raw) to parse a command string, or access via CommandLine.

CommandLine dataclass

CommandLine(
    raw: str, parts: tuple[tuple[Command, str | None], ...]
)

A full parsed bash command line, potentially containing multiple commands joined by operators.

Use CommandLine.parse(raw) to parse. Access individual commands via .commands or the final command via .primary.

Redirect dataclass

Redirect(op: str, target: str, fd: int | None = None)

A shell redirect parsed from a bash command (e.g. > file.txt, 2>&1).

HookContext dataclass

HookContext(
    session: SessionStore,
    transcript: Transcript,
    settings: BaseSettings | None,
    project_root: Path | None = None,
)

Runtime context injected into every hook event, providing session state, transcript, settings, and LLM/CLI helpers.

t property
t: Transcript

Alias for transcript.

s property
s: SessionStore

Alias for session.

state property
state: SessionStore

Alias for session.

conf property
conf: BaseSettings | None

Alias for settings.

c property
c: BaseSettings | None

Alias for settings (shortest form).

turn cached property
turn: Turn

The current transcript turn (cached).

prior cached property
prior: TranscriptSlice

Transcript slice before the current turn (cached).

BaseHookEvent dataclass

BaseHookEvent(_raw: dict[str, Any], ctx: HookContext)

Base class for all hook events, providing access to raw payload, context, and convenience methods.

tasks cached property
tasks: Tasks

The live task list for this session, read from Claude Code's native task store.

Unlike transcript-derived task_ops(), this reflects updates made by subagents, teammates, or resumed sessions, and is empty when the session has no task store — it never falls back to another session's tasks.

warn
warn(
    *parts: str | tuple[str, object] | object,
) -> HookResult

Emit a warning whose parts are auto-rendered and joined with newlines.

Each part is rendered by form: a plain str passes through verbatim; a (label, value) tuple becomes "{label}: {json}" with value JSON-encoded; any other object is JSON-encoded directly. Rendered parts are joined with "\n".

Parameters:

Name Type Description Default
*parts str | tuple[str, object] | object

Warning fragments, each a str, a (label, value) tuple, or any JSON-serializable object.

()

Returns:

Type Description
HookResult

A warn :class:HookResult carrying the joined message.

NotificationEvent dataclass

NotificationEvent(_raw: dict[str, Any], ctx: HookContext)

Bases: BaseHookEvent

Fires on system notifications, providing message, title, and notification type.

PostToolUseEvent dataclass

PostToolUseEvent(_raw: dict[str, Any], ctx: HookContext)

Bases: ToolHookEvent

Fires after a tool completes successfully, with access to the tool response.

PostToolUseFailureEvent dataclass

PostToolUseFailureEvent(
    _raw: dict[str, Any], ctx: HookContext
)

Bases: ToolHookEvent

Fires after a tool fails, providing the error message and interrupt status.

PreCompactEvent dataclass

PreCompactEvent(_raw: dict[str, Any], ctx: HookContext)

Bases: BaseHookEvent

Fires before context compaction, providing the trigger and custom instructions.

PreToolUseEvent dataclass

PreToolUseEvent(_raw: dict[str, Any], ctx: HookContext)

Bases: ToolHookEvent

Fires before a tool is executed. Return a block result to prevent execution.

StopEvent dataclass

StopEvent(_raw: dict[str, Any], ctx: HookContext)

Bases: BaseHookEvent

Fires when the agent is about to stop. Return a block result to prevent stopping.

SubagentStartEvent dataclass

SubagentStartEvent(_raw: dict[str, Any], ctx: HookContext)

Bases: BaseHookEvent

Fires when a subagent is launched. Provides agent_type for filtering.

SubagentStopEvent dataclass

SubagentStopEvent(_raw: dict[str, Any], ctx: HookContext)

Bases: BaseHookEvent

Fires when a subagent finishes. Provides agent_type for filtering.

ToolHookEvent dataclass

ToolHookEvent(_raw: dict[str, Any], ctx: HookContext)

Bases: BaseHookEvent

Event for tool-related hooks, adding tool name, input, command, and file access.

UserPromptSubmitEvent dataclass

UserPromptSubmitEvent(
    _raw: dict[str, Any], ctx: HookContext
)

Bases: BaseHookEvent

Fires when the user submits a prompt, before the agent processes it.

File dataclass

File(*, path: Path)

A file path wrapper with glob matching, prefix checks, and test-file detection.

Delegates Path methods via __getattr__ so .suffix, .name, .parent, .exists() etc. work directly.

PathMatcher dataclass

PathMatcher(*, patterns: list[str])

A reusable set of glob patterns for matching file paths. Supports in operator.

GateVerdict

Bases: BaseModel

LLM response model for llm_gate. The LLM sets block=True to deny.

NudgeVerdict

Bases: BaseModel

LLM response model for llm_nudge. The LLM sets fire=True to trigger the nudge.

PromptCheckVerdict

Bases: BaseModel

LLM response model for prompt_check. Action is "ok", "warning", or "block".

PromptMessage dataclass

PromptMessage(
    *,
    system_text: str = "",
    contexts: tuple[tuple[str, str], ...] = (),
    ask_text: str = "",
)

Fluent builder for structured LLM prompts with system text, XML context sections, and a question.

Chain .system(), .context(tag, content), and .ask() to build prompts. str() renders the full prompt with XML-wrapped context blocks.

load classmethod
load(
    name: str,
    *,
    base: str | Path | None = None,
    **vars: object,
) -> PromptMessage

Load a prompt from a .md file and render it via :meth:from_template.

Resolution searches directories in order, returning the first existing file: the base directory if given (otherwise a prompts/ directory beside the calling module), then the framework's bundled captain_hook/prompts/. The file path is <dir>/<name>.md; name may contain / to nest.

Parameters:

Name Type Description Default
name str

Prompt name without the .md suffix; may include / for nesting.

required
base str | Path | None

Optional directory to search instead of the caller-relative prompts/.

None
**vars object

Template variables substituted into the file via str.format_map.

{}

Returns:

Name Type Description
A PromptMessage

class:PromptMessage whose system text is the rendered file contents.

Raises:

Type Description
FileNotFoundError

If no matching file exists in any searched directory.

KeyError

If the file references a placeholder not supplied in **vars.

SessionSlot

SessionSlot(session_dir: Path | None, model: type[M])

Bases: Generic[M]

A typed slot for reading/writing a single Pydantic model in a session directory.

SessionStore

SessionStore(session_dir: Path | None)

Class-keyed store providing typed SessionSlot access via store[ModelClass].

load
load(model: type[M]) -> M

Read model from its session slot, defaulting to a fresh model().

Parameters:

Name Type Description Default
model type[M]

The Pydantic model class to read.

required

Returns:

Type Description
M

The persisted instance, or a newly constructed model() when no

M

stored state exists for this session.

track classmethod
track(model: type[BaseModel]) -> None

Register model so it appears in tracked_models() and tracked_paths().

untrack classmethod
untrack(model: type[BaseModel]) -> None

Reverse track — primarily for test isolation.

tracked_models classmethod
tracked_models() -> Sequence[type[BaseModel]]

Return the registered tracked-state models as an immutable tuple.

tracked_paths
tracked_paths() -> dict[str, Path]

Return {ModelClass.__name__: Path} for every tracked model whose slot has a path.

AutoConf

Automatic settings builder that infers a HooksSettings subclass from a conf module's attributes.

HooksSettings

Bases: BaseSettings

Base settings class for hook configuration, backed by environment variables with HOOKS_ prefix.

HookState

Bases: BaseModel

Per-hook persistent state tracked across events in a session (currently just fire_count for max_fires).

PrimitiveState

Bases: BaseModel

Per-primitive state for nudges/gates: last fire index, consumed-signal hashes, and echo-window lemmas.

Task dataclass

Task(
    *,
    id: str,
    subject: str,
    status: str,
    description: str = "",
    owner: str | None = None,
    blocked_by: tuple[str, ...] = (),
    blocks: tuple[str, ...] = (),
)

A task read from Claude Code's native task store (~/.claude/tasks/<list-id>/<id>.json).

Tasks dataclass

Tasks(tasks: tuple[Task, ...] = ())

Bases: Sequence[Task]

The live task list for one session, read from the native store rather than the transcript.

Always keyed by the exact list id (session id) — a session with no store has no tasks, never another session's. This is the source of truth for completion gates; transcript-derived task_ops() misses updates made by subagents, teammates, or resumed sessions.

resolve_root classmethod
resolve_root() -> Path

Resolve the root of Claude Code's native task store (<config-dir>/tasks).

for_session classmethod
for_session(
    session_id: str, *, root: Path | None = None
) -> Tasks

Load the task list stored under session_id, empty when absent.

Allow dataclass

Allow()

Inline test expectation: the hook should allow (return None or action "allow").

Block dataclass

Block(*, pattern: str | None = None)

Inline test expectation: the hook should block. Optional regex pattern matches the block message.

Input dataclass

Input(
    *,
    command: str | None = None,
    file: str | None = None,
    content: str | None = None,
    old: str | None = None,
    tool: str | None = None,
    prompt: str | None = None,
    agent_type: str | None = None,
    permission_mode: str | None = None,
    transcript: Path
    | TranscriptFixture
    | list[dict[str, Any]]
    | None = None,
)

Inline test input descriptor modeling an event payload. Set fields based on the target event type.

TranscriptFixture

TranscriptFixture(messages: list[dict[str, Any]])

A lightweight transcript stub for use in inline tests.

Wraps a list of raw message dicts that get parsed into a Transcript when the test runs.

Warn dataclass

Warn(*, pattern: str | None = None)

Inline test expectation: the hook should warn. Optional regex pattern matches the warning message.

EditOp dataclass

EditOp(file_path: File, old_string: str, new_string: str)

A parsed Edit tool operation extracted from a transcript tool use.

TaskOp dataclass

TaskOp(
    action: Literal["create", "update", "get", "list"],
    task_id: str | None = None,
    status: str | None = None,
    title: str | None = None,
)

A parsed task-tracker operation (create/update/get/list) extracted from a transcript tool use.

WriteOp dataclass

WriteOp(file_path: File, content: str)

A parsed Write/Create tool operation extracted from a transcript tool use.

ToolUse dataclass

ToolUse(
    *,
    name: str,
    raw_input: RawDict = (lambda: {})(),
    id: str,
    result: ToolResult | None = None,
    message_index: int = -1,
)

A tool invocation extracted from the transcript, with typed input parsing, file/command access, and result linkage.

ToolUseQuery dataclass

ToolUseQuery(items: list[ToolUse])

Chainable query builder for filtering and inspecting transcript tool uses.

Use .where() to filter by name, file, error status, etc., and terminal methods like .count(), .any(), .first(), .last(), .files() to extract results.

ToolUseSequence

ToolUseSequence(
    items: list[ToolUse], *, _include_errors: bool = False
)

Sequence of tool uses that filters out errors by default.

Access .with_errors for an unfiltered view. Use .where(...) to build a ToolUseQuery for chained filtering.

Transcript dataclass

Transcript(
    messages: list[TranscriptMessage],
    path: Path | None = None,
    classifier: Callable[[TranscriptMessage], bool]
    | None = None,
)

The full session transcript: a sequence of messages with tool-use querying, slicing, and history checks.

TranscriptMessage dataclass

TranscriptMessage(
    *,
    type: str,
    content: list[ContentBlock],
    raw: RawDict = (lambda: {})(),
)

A single message in a transcript with parsed content blocks, tool-use extraction, and text access.

TranscriptSlice dataclass

TranscriptSlice(
    messages: list[TranscriptMessage],
    path: Path | None = None,
    classifier: Callable[[TranscriptMessage], bool]
    | None = None,
)

Bases: Transcript

A contiguous slice of a Transcript, returned by slicing operations like recent, after, before.

Turn dataclass

Turn(
    messages: list[TranscriptMessage],
    path: Path | None = None,
    classifier: Callable[[TranscriptMessage], bool]
    | None = None,
    start_idx: int = 0,
    user_message: TranscriptMessage | None = None,
)

Bases: TranscriptSlice

The current conversation turn starting from the last user message, with edited-file tracking.

AgentInput dataclass

AgentInput(
    *,
    raw: RawDict = (lambda: {})(),
    prompt: str,
    agent_type: str | None = None,
    model: str | None = None,
    name: str | None = None,
    run_in_background: bool | None = None,
)

Bases: InputBase

Parsed Agent/Task tool input.

BashInput dataclass

BashInput(
    *,
    raw: RawDict = (lambda: {})(),
    command: str,
    timeout: int | None = None,
    description: str | None = None,
)

Bases: InputBase

Parsed Bash/Execute tool input.

EditInput dataclass

EditInput(
    *,
    raw: RawDict = (lambda: {})(),
    file_path: str,
    old: str,
    new: str,
    replace_all: bool = False,
)

Bases: FileInputBase

Parsed Edit tool input with old/new content for replacements.

FileInputBase dataclass

FileInputBase(
    *, raw: RawDict = (lambda: {})(), file_path: str
)

Bases: InputBase

Base for tool inputs that reference a file, providing a cached file property returning a File.

GenericInput dataclass

GenericInput(*, raw: RawDict = (lambda: {})())

Bases: InputBase

Fallback typed input for unrecognized tools, providing dict-like get() access to raw data.

GlobInput dataclass

GlobInput(
    *,
    raw: RawDict = (lambda: {})(),
    pattern: str,
    path: str | None = None,
)

Bases: InputBase

Parsed Glob tool input.

GrepInput dataclass

GrepInput(
    *,
    raw: RawDict = (lambda: {})(),
    pattern: str,
    path: str | None = None,
    file_type: str | None = None,
    glob: str | None = None,
    output_mode: str | None = None,
)

Bases: InputBase

Parsed Grep tool input.

InputBase dataclass

InputBase(*, raw: RawDict = (lambda: {})())

Base class for typed tool inputs. Provides from_raw() parsing and as_() type narrowing.

ReadInput dataclass

ReadInput(
    *,
    raw: RawDict = (lambda: {})(),
    file_path: str,
    limit: int | None = None,
    offset: int | None = None,
)

Bases: FileInputBase

Parsed Read tool input.

SkillInput dataclass

SkillInput(
    *,
    raw: RawDict = (lambda: {})(),
    skill: str,
    args: str | None = None,
)

Bases: InputBase

Parsed Skill tool input.

TaskCreateInput dataclass

TaskCreateInput(
    *,
    raw: RawDict = (lambda: {})(),
    subject: str,
    description: str | None = None,
)

Bases: InputBase

Parsed TaskCreate tool input.

TaskUpdateInput dataclass

TaskUpdateInput(
    *,
    raw: RawDict = (lambda: {})(),
    task_id: str,
    status: str | None = None,
    subject: str | None = None,
    description: str | None = None,
)

Bases: InputBase

Parsed TaskUpdate tool input.

WriteInput dataclass

WriteInput(
    *,
    raw: RawDict = (lambda: {})(),
    file_path: str,
    content: str,
)

Bases: FileInputBase

Parsed Write/Create tool input.

TextBlock dataclass

TextBlock(*, text: str = '')

A text content block from a transcript message.

ToolResult dataclass

ToolResult(
    *,
    tool_use_id: str,
    content: list[Any] | str = (lambda: [])(),
    is_error: bool = False,
    is_async: bool = False,
)

A tool-result content block linking back to its tool use via tool_use_id.

ToolUseBlock dataclass

ToolUseBlock(
    *, name: str, input: RawDict = (lambda: {})(), id: str
)

A tool-use content block from a transcript message, containing the tool name and raw input.

Action

Bases: StrEnum

Hook result action determining how the hook output is handled.

  • block: Prevents the tool use or stops the agent.
  • warn: Adds advisory context without blocking.
  • allow: Explicitly permits the action.

Agent dataclass

Agent(name: str)

Condition matching the current event's subagent type against a name pattern.

Content dataclass

Content(pattern: str, project_only: bool = True)

Condition matching the current event's file content against a regex.

Applies to Edit (new content) and Write (file content) tool events.

CustomCondition

Bases: Protocol

Protocol for user-defined hook conditions.

Implement check to create arbitrary matching logic beyond the built-in condition types.

Example

class LargeFile(CustomCondition): ... def check(self, evt: BaseHookEvent) -> bool: ... return bool(evt.file and evt.file.path.stat().st_size > 1_000_000) ... app.hook(Event.PreToolUse, only_if=[LargeFile()], message="Large file", block=True)

Event

Bases: Flag

Hook lifecycle events that can trigger registered hooks.

Combinable with | to match multiple events in a single hook registration.

Example

hook(Event.Stop | Event.SubagentStop, message="review first", block=True)

FilePath

FilePath(*patterns: str, **kwargs: bool)

Bases: FilePathFields

Condition matching the current event's file path against glob patterns.

Accepts one or more glob patterns as positional arguments.

Example

hook(Event.PostToolUse, only_if=[FilePath(".py", ".pyi")], message="Python file edited")

HookResult dataclass

HookResult(*, action: Action, message: str | None = None)

The return value from a hook handler, specifying the action and optional message.

of classmethod
of(
    action: Action, message: str | None = None
) -> HookResult

Build a HookResult, dedenting and stripping message for readable triple-quoted handler returns.

InPlanMode dataclass

InPlanMode()

Matches when the agent is in plan mode.

Reads permission_mode from the current event payload; falls back to counting EnterPlanMode vs ExitPlanMode tool uses in the transcript when the payload omits the field.

RanCommand dataclass

RanCommand(pattern: str, subagents: bool = True)

Transcript-history condition: true when a Bash tool use with a matching command exists.

ReadFile

ReadFile(*patterns: str, **kwargs: bool)

Bases: ReadFileFields

Transcript-history condition: true when a Read tool use targeted a matching file.

Accepts one or more glob patterns as positional arguments.

Signal dataclass

Signal(*, pattern: str, weight: int = 1, flags: int = 0)

A regex-based signal pattern used in the scoring pipeline.

Signals are matched against transcript text via re.search. Each match contributes weight to the cumulative score. Use negative weights to suppress false positives.

Example

Signal(pattern=r"retry", weight=2, flags=re.IGNORECASE)

Signals dataclass

Signals(
    patterns: Sequence[Signal | NlpSignal],
    threshold: int,
    window: int = 15,
)

Bundle of signal patterns with a scoring threshold.

When a bare list[Signal] is passed to a primitive, resolve_signals wraps it with threshold=1 — meaning any single signal match triggers. Pass a higher threshold to require multiple signals to fire together.

TestFile dataclass

TestFile(project_only: bool = True)

Condition that matches when the current event targets a test file (test_*.py, conftest.py).

Tool dataclass

Tool(pattern: str)

Condition matching the current event's tool name against a regex pattern.

Use in only_if or skip_if to filter hooks by which tool is being used.

Example

hook(Event.PreToolUse, only_if=[Tool("Bash|Execute")], message="...", block=True)

TouchedFile

TouchedFile(*patterns: str, **kwargs: bool)

Bases: TouchedFileFields

Transcript-history condition: true when an Edit/Write targeted a file matching the glob.

Accepts one or more glob patterns as positional arguments.

UsedSkill dataclass

UsedSkill(name: str, subagents: bool = True)

Transcript-history condition: true when a Skill tool use with a matching name exists.

Workflow dataclass

Workflow(
    *,
    label: str,
    marker: str,
    steps: list[Step],
    artifacts: list[Artifact[BaseModel]] = (lambda: [])(),
    post_complete: Callable[
        [BaseHookEvent], HookResult | None
    ]
    | None = None,
    on_start: Callable[[BaseHookEvent], HookResult | None]
    | None = None,
)
setup
setup(evt: BaseHookEvent) -> HookResult | None

Run the on_start callback when the workflow's subagent launches.

categorize_files

categorize_files(
    paths: Iterable[str | Path], *, lang: str = "py"
) -> tuple[list[str], list[str], list[str]]

Split paths into source, test, and skipped buckets for a language.

A path that does not match the lang globs is skipped; otherwise it is classified as a test file (via :attr:File.is_test, which treats conftest.py and anything under a tests/ directory as tests) or as source.

Parameters:

Name Type Description Default
paths Iterable[str | Path]

File paths to categorize; blank entries are ignored.

required
lang str

Language key into LANG_GLOBS (defaults to "py"); unknown keys fall back to *.<lang>.

'py'

Returns:

Type Description
list[str]

A (source, test, skipped) tuple, each a sorted, de-duplicated list of

list[str]

path strings.

block_command

block_command(
    pattern: str | list[str],
    *,
    reason: str,
    hint: str | None = None,
    tests: InlineTests | None = None,
) -> None

Register a declarative hook that blocks a Bash command matching a pattern.

Example

block_command(["git", "stash"], reason="git stash is not allowed", hint="Use jj")

gate

gate(message: str, **kwargs: Any) -> None

Register a blocking gate — shorthand for nudge(message, block=True, ...).

Example

gate("Run tests before committing", when=lambda evt: not has_tests(evt))

llm_gate

llm_gate(
    prompt: str,
    *,
    message: str | Callable[[GateVerdict], str],
    response_model: type[GateVerdict] = GateVerdict,
    verdict: Callable[[GateVerdict], bool] = lambda r: (
        r.block
    ),
    signals: Sequence[Signal | NlpSignal]
    | Signals
    | None = None,
    when: Callable[[BaseHookEvent], bool] | None = None,
    only_if: Sequence[TCondition] = (),
    skip_if: Sequence[TCondition] = (),
    events: Event | None = None,
    max_fires: int | None = None,
    tests: InlineTests | None = None,
    max_context: int = 2000,
    specialty: TSpecialty = "review",
    model: TModel = "small",
    agent: bool = True,
    transcript: bool = True,
) -> None

Register an LLM-powered blocking gate.

Defaults are tuned for the common case: agent=True and transcript=True so the gate has tool access and full transcript context. Pass agent=False, transcript=False for cheap, stateless yes/no checks.

Example

llm_gate("Is the agent making excuses?", ... message=lambda r: f"Excuse detected: {r.reasoning}", ... signals=Signals([Signal(r"external.*service", weight=2)], threshold=2))

llm_nudge

llm_nudge(
    prompt: str,
    *,
    message: str | Callable[[NudgeVerdict], str],
    response_model: type[NudgeVerdict] = NudgeVerdict,
    verdict: Callable[[NudgeVerdict], bool] = lambda r: (
        r.fire
    ),
    signals: Sequence[Signal | NlpSignal]
    | Signals
    | None = None,
    when: Callable[[BaseHookEvent], bool] | None = None,
    only_if: Sequence[TCondition] = (),
    skip_if: Sequence[TCondition] = (),
    events: Event | None = None,
    max_fires: int | None = None,
    tests: InlineTests | None = None,
    async_: bool = False,
    max_context: int = 2000,
    specialty: TSpecialty = "review",
    model: TModel = "small",
    agent: bool = True,
    transcript: bool = True,
) -> None

Register an LLM-powered advisory nudge.

Defaults are tuned for the common case: agent=True and transcript=True so the nudge has tool access and full transcript context. Pass agent=False, transcript=False for cheap, stateless yes/no checks.

Example

llm_nudge("Is the agent speculating instead of observing?", ... message="Observe, don't infer -- check traces first", ... signals=Signals([Signal(r"should contain", weight=2)], threshold=3))

prompt_check

prompt_check(
    evt: BaseHookEvent,
    template: str | PromptMessage,
    fmt: dict[str, Any] | None = None,
    *,
    prefix: str,
    suffix: str = "",
    timeout: int = 45,
    include_reasoning: bool = True,
    response_model: type[
        PromptCheckVerdict
    ] = PromptCheckVerdict,
) -> HookResult | None

Run an LLM check with a formatted prompt and return block/warn/None.

session_id_for

session_id_for(evt: BaseHookEvent) -> str | None

Return a 12-char sha256 prefix of the transcript path, or None if unavailable.

warn_command

warn_command(
    pattern: str | list[str],
    *,
    message: str,
    tests: InlineTests | None = None,
    events: Event = Event.PostToolUse,
) -> None

Register a declarative hook that warns on a Bash command matching a pattern.

Example

warn_command(["python", "-c", "*"], message="Prefer uv run mtest")

audit

audit(
    events: Event = Event.PreToolUse
    | Event.PostToolUse
    | Event.Stop,
    *,
    log_dir: Path | str | None = None,
    filename: Callable[[datetime], str] = lambda d: (
        f"{d}.jsonl"
    ),
    fields: Callable[
        [BaseHookEvent], dict[str, Any]
    ] = default_fields,
    only_if: Sequence[TCondition] = (),
    skip_if: Sequence[TCondition] = (),
) -> None

Register a hook that appends one JSONL record per matching event.

Each matching event writes a single line to <log_dir>/<filename(now)>. Default fields are ts, event, tool, file, and session_id (a 12-char sha256 prefix of the transcript path).

Example

from captain_hook import audit, Event audit(Event.PreToolUse | Event.PostToolUse | Event.Stop)

Parameters:

Name Type Description Default
events Event

Event mask to audit. Defaults to PreToolUse | PostToolUse | Stop.

PreToolUse | PostToolUse | Stop
log_dir Path | str | None

Output directory. Defaults to $CLAUDE_PROJECT_DIR/.context/hook-logs.

None
filename Callable[[datetime], str]

(datetime) -> str mapping a timestamp to a filename.

lambda d: f'{d}.jsonl'
fields Callable[[BaseHookEvent], dict[str, Any]]

(evt) -> dict for the per-record payload.

default_fields
only_if Sequence[TCondition]

Conditions that must match for the event to be recorded.

()
skip_if Sequence[TCondition]

Conditions that, if matched, suppress recording.

()

lint

lint(
    check: Callable[[str], list[str]],
    *,
    message: str,
    trigger: str | None = ...,
    sep: str = ...,
    block: bool = ...,
    events: Event | None = ...,
    tests: InlineTests | None = ...,
    max_shown: int = ...,
) -> None
lint(
    check: Callable[[AST], Iterator[str]],
    *,
    message: str,
    trigger: str | None = ...,
    sep: str = ...,
    block: bool = ...,
    events: Event | None = ...,
    tests: InlineTests | None = ...,
    max_shown: int = ...,
) -> None
lint(
    check: Callable[[str], list[str]]
    | Callable[[AST], Iterator[str]],
    *,
    message: str,
    trigger: str | None = None,
    sep: str = ", ",
    block: bool = False,
    events: Event | None = None,
    tests: InlineTests | None = None,
    max_shown: int = 5,
) -> None

Register a lint check that runs on Python file edits/writes.

Supports two modes based on the check function's type hint: - String mode: receives the file content as str, returns violation strings. - AST mode: receives each ast.AST node, yields violation strings.

Example

def find_prints(content: str) -> list[str]: ... return [line for line in content.splitlines() if "print(" in line] lint(find_prints, message="Remove print statements: {violations}")

nudge

nudge(
    message: str,
    *,
    when: Callable[[BaseHookEvent], bool] | None = None,
    signals: Sequence[Signal | NlpSignal]
    | Signals
    | None = None,
    only_if: Sequence[TCondition] = (),
    skip_if: Sequence[TCondition] = (),
    block: bool = False,
    events: Event | None = None,
    max_fires: int | None = None,
    tests: InlineTests | None = None,
    async_: bool = False,
) -> None

Register a nudge that warns (or blocks) when conditions or signals match.

Example

nudge("Remember to run tests", only_if=[TouchedFile("*/.py")])

With signal scoring: >>> nudge("Stop retrying", ... signals=Signals([Signal(r"retry", weight=2)], threshold=2, window=5))

session_state

session_state(cls: type[T]) -> type[T]

Decorator that registers a Pydantic model for collective SessionStore introspection.

Example

@session_state ... class Snapshot(BaseModel): ... op_id: str

build_settings

build_settings(
    module: ModuleType, prefix: str = "HOOKS_"
) -> BaseSettings

Build a settings instance from a conf module, using an explicit HooksSettings subclass or auto-inferring fields.

cite_message

cite_message(
    sig: Signals, triggering: list[str], message: str
) -> str

Append trigger context to a message when signal matches are found.

resolve_signals

resolve_signals(
    signals: Sequence[Signal | NlpSignal] | Signals | None,
) -> Signals | None

Normalize signals input into a Signals bundle, or None.

A bare list[Signal] is wrapped with threshold=1 (any single match triggers).

transcript_texts

transcript_texts(
    evt: BaseHookEvent, window: int
) -> list[str]

Extract text from recent transcript messages for signal scoring.

For UserPromptSubmit events, returns just the user prompt. Otherwise returns .text from the last window messages.

parse_tool_input

parse_tool_input(
    name: str, raw: RawDict | Any
) -> ToolInput

Parse raw tool input into a typed dataclass based on tool name.

read_json

read_json(path: Path) -> dict[str, Any] | None
read_json(
    path: Path, default: dict[str, Any]
) -> dict[str, Any]
read_json(
    path: Path, default: dict[str, Any] | None = None
) -> dict[str, Any] | None

Read and parse a JSON file, returning default on missing file or parse error.