Skip to content

Primitives

Primitives are convenience wrappers that register hooks for common patterns. They handle event targeting, fire counting, and echo suppression automatically.

nudge

Warn the agent when conditions or signals match:

from captain_hook import nudge, TouchedFile

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

With signal scoring (fires when transcript text matches patterns):

from captain_hook import nudge, Signal, Signals

nudge(
    "Stop retrying — narrow the failing test first",
    signals=Signals(
        patterns=[
            Signal(r"let me try again", weight=2),
            Signal(r"retry", weight=1),
        ],
        threshold=3,
        window=10,
    ),
)

Default events: PostToolUse (with signals) or PreToolUse (without). Default max_fires: 3 (with signals) or 1 (without).

Parameter Description
message Warning text shown to the agent
when Predicate (evt) -> bool for additional filtering
signals Signal patterns for transcript text scoring
only_if / skip_if Condition lists
events Override default event targeting
max_fires Limit fires per session
tests Inline test dict

gate

A blocking nudge — prevents the agent from proceeding:

from captain_hook import gate, RanCommand

gate("Run tests before stopping",
     skip_if=[RanCommand(r"uv\s+run\s+mtest")])

gate() is equivalent to nudge(..., block=True).

Default events: Stop | SubagentStop.

lint

Check file content with a string- or AST-mode check function. The mode is inferred from the check's first parameter type; both return the violation strings spliced into {violations}:

from captain_hook import lint

def find_prints(content: str) -> list[str]:
    return [line for line in content.splitlines() if "print(" in line]

lint(find_prints, message="Use logger.info() instead of print(): {violations}")
import ast
from captain_hook import lint

def find_bare_except(tree: ast.AST) -> list[str]:
    return [
        f"line {node.lineno}"
        for node in ast.walk(tree)
        if isinstance(node, ast.ExceptHandler) and node.type is None
    ]

lint(find_bare_except, message="Avoid bare except clauses: {violations}")

Lints fire on PostToolUse for Edit/Write of .py files. Test files are skipped by default.

styleguide

Apply AST-based style rules to Python edits, reporting only what the edit changed. A rule is a StyleRule subclass whose docstring is the message and whose match is built from the matchers module:

from captain_hook.style import StyleRule, matchers as M, styleguide

class NoPrint(StyleRule):
    """
    print() calls don't belong in committed code:
      - {violations}
    """
    match = M.calls("print")
    label = "print() call"

styleguide(NoPrint)

captain-hook ships no rules of its own — styleguide() is the substrate (parsing, change-scoping, formatting, test wiring). Each call registers one hook; scope it with only_if / skip_if / events / block. See Style Rules for the full guide, including the matcher algebra, StyleDiffRule, and change scoping.

block_command

Block specific bash commands:

from captain_hook import block_command

block_command(
    ["git", "stash"],
    reason="git stash is not allowed; use jj",
    hint="Run `jj shelve` instead",
)

The token list is converted to a regex: ["git", "stash"] becomes r"git\s+stash". Use "*" for wildcards: ["git", "stash", "*"] matches git stash pop, git stash drop, etc.

warn_command

Warn (but don't block) on specific commands:

from captain_hook import warn_command

warn_command(
    ["python", "-c"],
    message="Consider using mtest instead of raw python",
)

audit

Append a JSONL record per matching event for offline analysis:

from captain_hook import audit, Event

audit(Event.PreToolUse | Event.PostToolUse | Event.Stop)

By default, each line contains ts, event, tool, file, and a session id derived from the transcript path. Records are written to $CLAUDE_PROJECT_DIR/.context/hook-logs/<YYYY-MM-DD>.jsonl.

Customize the destination or record shape:

from datetime import datetime
from captain_hook import audit, Event

audit(
    Event.PostToolUse,
    log_dir="logs/hooks",
    filename=lambda d: f"{d:%Y-%m-%dT%H}.jsonl",
    fields=lambda evt: {"event": evt.event_name.name, "tool": evt.tool_name},
)
Parameter Description
events Event mask (default: PreToolUse \| PostToolUse \| Stop)
log_dir Output directory (default: $CLAUDE_PROJECT_DIR/.context/hook-logs)
filename (datetime) -> str mapping a timestamp to a filename
fields (evt) -> dict for the per-record payload
only_if / skip_if Condition lists

More primitives

For LLM-powered hooks (llm_gate, llm_nudge, prompt_check), see LLM Hooks. For multi-step enforcement, see Workflows.