---------------------------------------------------------------------- This is the API documentation for the captain_hook library. ---------------------------------------------------------------------- ## Registration Declaring and registering hooks. hook(events: 'Event', *, only_if: 'Sequence[TCondition]' = (), skip_if: 'Sequence[TCondition]' = (), message: 'str | None' = None, block: 'bool' = False, respect_gitignore: 'bool' = True, max_fires: 'int | None' = None, tests: 'InlineTests | None' = None, async_: 'bool' = False, skip_planning_agents: 'bool' = True) -> 'None' on(events: 'Event', *, only_if: 'Sequence[TCondition]' = (), skip_if: 'Sequence[TCondition]' = (), respect_gitignore: 'bool' = True, max_fires: 'int | None' = None, tests: 'InlineTests | None' = None, async_: 'bool' = False, skip_planning_agents: 'bool' = True) -> 'Callable[[HookHandler], HookHandler]' register(events: 'Event', *, only_if: 'Sequence[TCondition]' = (), skip_if: 'Sequence[TCondition]' = (), message: 'str | None' = None, block: 'bool' = False, respect_gitignore: 'bool' = True, max_fires: 'int | None' = None, tests: 'InlineTests | None' = None, async_: 'bool' = False, skip_planning_agents: 'bool' = True) -> 'Callable[[HookHandler], HookHandler] | None' ## Primitives One-line hooks for the common cases. audit(events: 'Event' = , *, log_dir: 'Path | str | None' = None, filename: 'Callable[[datetime], str]' = at 0x7f5d0f741440>, fields: 'Callable[[BaseHookEvent], dict[str, Any]]' = , 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 ``/``. 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) Args: events: Event mask to audit. Defaults to PreToolUse | PostToolUse | Stop. log_dir: Output directory. Defaults to ``$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: Conditions that must match for the event to be recorded. skip_if: Conditions that, if matched, suppress recording. 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)) 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)) lint(check: 'Callable[[str], list[str]] | Callable[[ast.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}") diff_lint(check: 'DiffCheck', *, message: 'str', sep: 'str' = ', ', block: 'bool' = False, events: 'Event | None' = None, tests: 'InlineTests | None' = None, max_shown: 'int' = 5, only_if: 'Sequence[TCondition]' = (Tool(pattern='Edit'), FilePath(patterns=('*.py',), project_only=False)), skip_if: 'Sequence[TCondition]' = (TestFile(project_only=True),)) -> 'None' 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") warn_command(pattern: 'str | list[str]', *, message: 'str', tests: 'InlineTests | None' = None, events: 'Event' = ) -> 'None' Register a declarative hook that warns on a Bash command matching a pattern. Example: >>> warn_command(["python", "-c", "*"], message="Prefer uv run mtest") llm_gate(prompt: 'str', *, message: 'str | Callable[[GateVerdict], str]', response_model: 'type[GateVerdict]' = , verdict: 'Callable[[GateVerdict], bool]' = at 0x7f5d0f743e20>, 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(prompt: 'str', *, message: 'str | Callable[[NudgeVerdict], str]', response_model: 'type[NudgeVerdict]' = , verdict: 'Callable[[NudgeVerdict], bool]' = at 0x7f5d0f764180>, 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)) llm_evaluate(evt: 'BaseHookEvent', prompt: 'str', response_model: 'type[M]', *, signals: 'Sequence[Signal | NlpSignal] | Signals | None' = None, when: 'Callable[[BaseHookEvent], bool] | None' = None, max_context: 'int' = 2000, specialty: 'TSpecialty' = 'review', model: 'TModel' = 'small', agent: 'bool' = False, transcript: 'bool' = False) -> 'M | None' prompt_check(evt: 'BaseHookEvent', template: 'str | Prompt', fmt: 'dict[str, Any] | None' = None, *, prefix: 'str', suffix: 'str' = '', timeout: 'int' = 45, include_reasoning: 'bool' = True, response_model: 'type[PromptCheckVerdict]' = ) -> 'HookResult | None' Run an LLM check with a formatted prompt and return block/warn/None. styleguide(*rules: 'type[StyleRule]', block: 'bool' = False, only_if: 'Sequence[TCondition]' = (), skip_if: 'Sequence[TCondition]' = (), events: 'Event | None' = None, max_shown: 'int' = 5) -> 'None' Register one change-scoped hook applying the given style rules to Python edits and writes. Each rule is a [`StyleRule`][captain_hook.style.StyleRule] (or [`StyleDiffRule`][captain_hook.style.StyleDiffRule]) subclass whose docstring is its message. The single registered hook parses the edited file once, runs every rule against the post-edit tree, scopes each violation to the changed lines, and emits one aggregated warning (or block, when ``block`` is set). Call again with different ``only_if`` / ``skip_if`` / ``events`` / ``block`` to register a separately scoped hook. Args: rules: ``StyleRule`` / ``StyleDiffRule`` subclasses to apply. block: Block the tool call instead of warning. only_if: Extra conditions ANDed onto the built-in ``Edit|Write`` + ``*.py`` guards. skip_if: Extra conditions ORed onto the built-in test-file skip. events: Override the default ``PostToolUse`` targeting. max_shown: Maximum violations shown per rule. Example: >>> styleguide(NoPrint, NoBareExcept) >>> styleguide(NoSqlInjection, block=True, only_if=[FilePath("api/**/*.py")]) captain_hook.style.matchers Composable AST matchers for style rules — import this module as ``M``. Every factory (``kind``, ``calls``, ``under``, ...) and preset (``imports``, ``control_flow``, ...) is a module-level [`Matcher`][captain_hook.style.matchers.Matcher] builder or constant; author a rule by combining them with ``&``, ``|``, and ``~``. Example: >>> from captain_hook.style import matchers as M >>> M.imports & M.child_of(M.control_flow) & ~M.under(M.type_checking) StyleRule() Base class for a single-tree AST style rule applied to Python edits and writes. Subclass it and write the rule's message as the class **docstring** (``{violations}`` is substituted at fire time). Declare the rule as data by setting ``match`` to a [`Matcher`][captain_hook.style.matchers.Matcher] (and optionally ``label``); override ``check`` only for logic a matcher can't express. The class name is the rule's identity — ``NoNestedImports`` becomes ``"no-nested-imports"``. Example: ```python from captain_hook.style import matchers as M class NoNestedImports(StyleRule): """Lazy imports belong at the top of the function body: {violations}""" match = M.imports & M.child_of(M.control_flow) & ~M.under(M.type_checking) ``` StyleDiffRule() Base class for a diff rule: flags constructs newly introduced by the change. Like [`StyleRule`][captain_hook.style.StyleRule], but it compares the pre-edit and post-edit trees. The declarative form flags nodes matching ``match`` in the new tree that were absent from the old tree (by unparsed source); override ``check`` when the "newly introduced" identity needs custom logic. Example: ```python from captain_hook.style import matchers as M class NoNewWildcardImport(StyleDiffRule): """Wildcard import added by this edit: {violations}""" match = M.imports.where(lambda n: any(a.name == "*" for a in n.names)) ``` Violation(line: 'int', label: 'str') -> None A single style violation, located by line so the runner can scope it to the edit. Attributes: line: 1-based line number of the offending construct in the post-edit file. label: Short human-readable description, rendered as ``"{label} (line {line})"``. GateVerdict(*, block: bool, reasoning: str) -> None LLM response model for ``llm_gate``. The LLM sets ``block=True`` to deny. NudgeVerdict(*, fire: bool, reasoning: str) -> None LLM response model for ``llm_nudge``. The LLM sets ``fire=True`` to trigger the nudge. PromptCheckVerdict(*, action: Literal['ok', 'warning', 'block'], reason: str) -> None LLM response model for ``prompt_check``. Action is ``"ok"``, ``"warning"``, or ``"block"``. ## Conditions Typed filters that decide when hooks fire. Tool(pattern: 'str') -> None 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) FilePath(*patterns: 'str', **kwargs: 'bool') -> 'None' 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") TouchedFile(*patterns: 'str', **kwargs: 'bool') -> 'None' Transcript-history condition: true when an Edit/Write targeted a file matching the glob. Accepts one or more glob patterns as positional arguments. TestFile(project_only: 'bool' = True) -> None Condition that matches when the current event targets a test file (``test_*.py``, ``conftest.py``). ReadFile(*patterns: 'str', **kwargs: 'bool') -> 'None' Transcript-history condition: true when a Read tool use targeted a matching file. Accepts one or more glob patterns as positional arguments. RanCommand(pattern: 'str', subagents: 'bool' = True) -> None Transcript-history condition: true when a Bash tool use with a matching command exists. UsedSkill(name: 'str', subagents: 'bool' = True) -> None Transcript-history condition: true when a Skill tool use with a matching name exists. InPlanMode() -> None 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. Waiting() -> None Waiting() Signal(*, pattern: 'str', weight: 'int' = 1, flags: 'int' = 0) -> None 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(patterns: 'Sequence[Signal | NlpSignal]', threshold: 'int', window: 'int' = 15) -> None 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. CustomCondition(*args, **kwargs) 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) ## Events & Results Typed lifecycle events and hook outcomes. Event(*values) 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) BaseHookEvent(_raw: 'dict[str, Any]', ctx: 'HookContext') -> None Base class for all hook events, providing access to raw payload, context, and convenience methods. ToolHookEvent(_raw: 'dict[str, Any]', ctx: 'HookContext') -> None Event for tool-related hooks, adding tool name, input, command, and file access. PreToolUseEvent(_raw: 'dict[str, Any]', ctx: 'HookContext') -> None Fires before a tool is executed. Return a block result to prevent execution. PostToolUseEvent(_raw: 'dict[str, Any]', ctx: 'HookContext') -> None Fires after a tool completes successfully, with access to the tool response. PostToolUseFailureEvent(_raw: 'dict[str, Any]', ctx: 'HookContext') -> None Fires after a tool fails, providing the error message and interrupt status. UserPromptSubmitEvent(_raw: 'dict[str, Any]', ctx: 'HookContext') -> None Fires when the user submits a prompt, before the agent processes it. StopEvent(_raw: 'dict[str, Any]', ctx: 'HookContext') -> None Fires when the agent is about to stop. Return a block result to prevent stopping. SubagentStartEvent(_raw: 'dict[str, Any]', ctx: 'HookContext') -> None Fires when a subagent is launched. Provides ``agent_type`` for filtering. SubagentStopEvent(_raw: 'dict[str, Any]', ctx: 'HookContext') -> None Fires when a subagent finishes. Provides ``agent_type`` for filtering. PreCompactEvent(_raw: 'dict[str, Any]', ctx: 'HookContext') -> None Fires before context compaction, providing the trigger and custom instructions. NotificationEvent(_raw: 'dict[str, Any]', ctx: 'HookContext') -> None Fires on system notifications, providing message, title, and notification type. HookContext(session: 'SessionStore', transcript: 'Transcript', settings: 'BaseSettings | None', project_root: 'Path | None' = None) -> None Runtime context injected into every hook event: session state, transcript, settings, and LLM/CLI helpers. HookResult(*, action: 'Action', message: 'str | None' = None) -> None The return value from a hook handler, specifying the action and optional message. Action(*values) 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(name: 'str') -> None Condition matching the current event's subagent type against a name pattern. Content(pattern: 'str', project_only: 'bool' = True) -> None Condition matching the current event's file content against a regex. Applies to Edit (new content) and Write (file content) tool events. ## BaseHookEvent Methods Methods for the BaseHookEvent class event is_subagent session_id BaseHookEvent.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. user_prompt stop_hook_active transcript_path permission_mode parent_agent_type tool_name BaseHookEvent.input command BaseHookEvent.command_line file content old agent_type command_matches(self, *patterns: 'str') -> 'bool' file_matches(self, *globs: 'str') -> 'bool' content_matches(self, pattern: 'str') -> 'bool' allow(self) -> 'HookResult' warn(self, *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"``. Args: *parts: Warning fragments, each a ``str``, a ``(label, value)`` tuple, or any JSON-serializable object. Returns: A warn :class:`HookResult` carrying the joined message. block(self, message: 'str') -> 'HookResult' ## ToolHookEvent Methods Methods for the ToolHookEvent class tool_name ToolHookEvent.command_line content old agent_type command_matches(self, *patterns: 'str') -> 'bool' file_matches(self, *globs: 'str') -> 'bool' content_matches(self, pattern: 'str') -> 'bool' ## HookContext Methods Methods for the HookContext class t Alias for ``transcript``. s Alias for ``session``. state Alias for ``session``. conf Alias for ``settings``. c Alias for ``settings`` (shortest form). HookContext.turn The current transcript turn (cached). HookContext.prior Transcript slice before the current turn (cached). call_cli(self, args: 'list[str]', *, input: 'str | None' = None, timeout: 'int' = 30, env: 'dict[str, str] | None' = None) -> 'str' git(self, *args: 'str') -> 'str | None' HookContext.changed_paths HookContext.repo_root HookContext.current_branch call_llm(self, template: 'str | Prompt', *args: 'Any', specialty: 'TSpecialty' = 'general', model: 'TModel' = 'small', timeout: 'int' = 180, transcript: 'bool' = False, agent: 'bool' = False, response_model: 'type[BaseModel] | None' = None, **kwargs: 'Any') -> 'str | BaseModel' resolve_schema_path(backend: 'LlmBackend', schema: 'str | None') -> 'str | None' ## Files & Commands Path matching and AST-level command inspection. File(*, path: 'Path') -> None 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(*, patterns: 'list[str]') -> None A reusable set of glob patterns for matching file paths. Supports ``in`` operator. 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. Args: paths: File paths to categorize; blank entries are ignored. lang: Language key into ``LANG_GLOBS`` (defaults to ``"py"``); unknown keys fall back to ``*.``. Returns: A ``(source, test, skipped)`` tuple, each a sorted, de-duplicated list of path strings. 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. Command(raw: 'str', executable: 'str', args: 'tuple[str, ...]', env: 'tuple[tuple[str, str], ...]' = (), redirects: 'tuple[Redirect, ...]' = ()) -> None 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(raw: 'str', parts: 'tuple[tuple[Command, str | None], ...]') -> 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(op: 'str', target: 'str', fd: 'int | None' = None) -> None A shell redirect parsed from a bash command (e.g. ``> file.txt``, ``2>&1``). EditOp(file_path: 'File', old_string: 'str', new_string: 'str') -> None A parsed Edit tool operation extracted from a transcript tool use. WriteOp(file_path: 'File', content: 'str') -> None A parsed Write/Create tool operation extracted from a transcript tool use. TaskOp(action: "Literal['create', 'update', 'get', 'list']", task_id: 'str | None' = None, status: 'str | None' = None, title: 'str | None' = None) -> None A parsed task-tracker operation (create/update/get/list) extracted from a transcript tool use. ## File Methods Methods for the File class __getattr__(self, name: 'str') -> 'Any' __str__(self) -> 'str' __fspath__(self) -> 'str' __eq__(self, other: 'object') -> 'bool' __hash__(self) -> 'int' File.is_test matches(self, *patterns: 'str') -> 'bool' under(self, *prefixes: 'str') -> 'bool' exists(self) -> 'bool' read_text(self) -> 'str' contains(self, pattern: 'str') -> 'bool' ## Command Methods Methods for the Command class parse(raw: 'str') -> 'Command' empty() -> 'Command' Command.argv Command.program Command.env_dict matches(self, pattern: 'str') -> 'bool' has_arg(self, *patterns: 'str') -> 'bool' __str__(self) -> 'str' __contains__(self, item: 'str') -> 'bool' __bool__(self) -> 'bool' ## CommandLine Methods Methods for the CommandLine class parse(raw: 'str') -> 'CommandLine' CommandLine.commands CommandLine.primary CommandLine.head __iter__(self) -> 'Iterator[Command]' __len__(self) -> 'int' __str__(self) -> 'str' __contains__(self, item: 'str') -> 'bool' __bool__(self) -> 'bool' CommandLine.q node_text(node: 'Node') -> 'str' word_text(node: 'Node') -> 'str' extract_redirect(node: 'Node') -> 'Redirect' extract_command(node: 'Node') -> 'Command' collect_parts(children: 'list[Node]', ops: 'frozenset[str]') -> 'list[tuple[Command, str | None]]' walk_redirected(node: 'Node') -> 'list[tuple[Command, str | None]]' walk_node(node: 'Node') -> 'list[tuple[Command, str | None]]' fallback(raw: 'str') -> 'Command' ## Transcript Typed access to conversation history. Transcript(messages: 'list[TranscriptMessage]', path: 'Path | None' = None, classifier: 'Callable[[TranscriptMessage], bool] | None' = None) -> None The full session transcript: a sequence of messages with tool-use querying, slicing, and history checks. TranscriptMessage(*, type: 'str', content: 'list[ContentBlock]', raw: 'RawDict' = ) -> None A single message in a transcript with parsed content blocks, tool-use extraction, and text access. TranscriptSlice(messages: 'list[TranscriptMessage]', path: 'Path | None' = None, classifier: 'Callable[[TranscriptMessage], bool] | None' = None) -> None A contiguous slice of a Transcript, returned by slicing operations like ``recent``, ``after``, ``before``. ToolUse(*, name: 'str', raw_input: 'RawDict' = , id: 'str', result: 'ToolResult | None' = None, message_index: 'int' = -1) -> None A transcript tool invocation with typed input parsing, file/command access, and result linkage. ToolUseQuery(items: 'list[ToolUse]') -> None 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(items: 'list[ToolUse]', *, _include_errors: 'bool' = False) -> 'None' 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. Turn(messages: 'list[TranscriptMessage]', path: 'Path | None' = None, classifier: 'Callable[[TranscriptMessage], bool] | None' = None, start_idx: 'int' = 0, user_message: 'TranscriptMessage | None' = None) -> None The current conversation turn starting from the last user message, with edited-file tracking. Task(*, id: 'str', subject: 'str', status: 'str', description: 'str' = '', owner: 'str | None' = None, blocked_by: 'tuple[str, ...]' = (), blocks: 'tuple[str, ...]' = ()) -> None A task read from Claude Code's native task store (``~/.claude/tasks//.json``). Tasks(tasks: 'tuple[Task, ...]' = ()) -> None 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. ## Transcript Methods Methods for the Transcript class is_user_message(self, msg: 'TranscriptMessage') -> 'bool' __len__(self) -> 'int' __bool__(self) -> 'bool' __getitem__(self, key: 'int | slice') -> 'TranscriptMessage | TranscriptSlice' __str__(self) -> 'str' from_path(path: 'Path | str | None') -> 'Transcript' from_simple_messages(messages: 'list[dict[str, str]]') -> 'Transcript' from_messages(messages: 'list[dict[str, Any]]') -> 'Transcript' from_parsed(messages: 'list[TranscriptMessage]') -> 'Transcript' Transcript.tool_uses count_tools(self, *names: 'str') -> 'int' has_tool(self, name: 'str') -> 'bool' Transcript.commands has_command(self, pattern: 'str') -> 'bool' has_edit_to(self, *globs: 'str') -> 'bool' user_said(self, *keywords: 'str') -> 'bool' all_edits_under(self, *prefixes: 'str') -> 'bool' first_user_message(self) -> 'str | None' after(self, tool: 'str', file: 'str | None' = None) -> 'TranscriptSlice' before(self, tool: 'str') -> 'TranscriptSlice' prior(self) -> 'TranscriptSlice' recent(self, n: 'int') -> 'TranscriptSlice' full_text assistant_text(self, n: 'int' = 10, max_per_msg: 'int' = 500) -> 'str' extract_files(self, tools: 'list[str] | None' = None) -> 'list[File]' has_read(self, pattern: 'str') -> 'bool' has_skill(self, *names: 'str') -> 'bool' Transcript.subagents count_failures(self) -> 'int' has_override(self, token: 'str', invalidated_by: 'list[str] | None' = None) -> 'bool' edit_ops(self) -> 'list[EditOp]' write_ops(self) -> 'list[WriteOp]' task_ops(self) -> 'list[TaskOp]' Transcript.turn_start Transcript.current_turn since_last_user(self) -> 'Turn' ## TranscriptMessage Methods Methods for the TranscriptMessage class from_raw(*, type: 'str', content: 'list[RawBlock] | str', raw: 'RawDict') -> 'TranscriptMessage' is_async_tool_use(raw: 'RawDict') -> 'bool' TranscriptMessage.notification tool_uses tool_results text ## ToolUse Methods Methods for the ToolUse class is_error ToolUse.input ToolUse.file command ToolUse.command_line agent_type ## ToolUseQuery Methods Methods for the ToolUseQuery class where(self, *, name: 'str | None' = None, file: 'str | list[str] | None' = None, file_under: 'str | list[str] | None' = None, is_error: 'bool | None' = None, input_has: 'dict[str, Any] | None' = None, raw_input: 'Mapping[str, Any] | None' = None) -> 'ToolUseQuery' count(self) -> 'int' any(self) -> 'bool' list(self) -> 'list[ToolUse]' first(self) -> 'ToolUse | None' last(self) -> 'ToolUse | None' files(self) -> 'list[File]' __iter__(self) -> 'Iterator[ToolUse]' __len__(self) -> 'int' __bool__(self) -> 'bool' ## ToolUseSequence Methods Methods for the ToolUseSequence class where(self, **kwargs: 'Any') -> 'ToolUseQuery' __getitem__(self, idx: 'int | slice') -> 'ToolUse | list[ToolUse]' __iter__(self) -> 'Iterator[ToolUse]' __len__(self) -> 'int' __bool__(self) -> 'bool' with_errors ## Tasks Methods Methods for the Tasks class resolve_root() -> 'Path' Resolve the root of Claude Code's native task store (``/tasks``). for_session(session_id: 'str', *, root: 'Path | None' = None) -> 'Tasks' Load the task list stored under ``session_id``, empty when absent. __getitem__(self, index: 'int | slice') -> 'Task | tuple[Task, ...]' __len__(self) -> 'int' get(self, task_id: 'str') -> 'Task | None' with_status(self, *statuses: 'str') -> 'tuple[Task, ...]' pending in_progress completed open all_completed ## Tool Inputs Parsed tool-input models. InputBase(*, raw: 'RawDict' = ) -> None Base class for typed tool inputs. Provides ``from_raw()`` parsing and ``as_()`` type narrowing. FileInputBase(*, raw: 'RawDict' = , file_path: 'str') -> None Base for tool inputs that reference a file, providing a cached ``file`` property returning a ``File``. AgentInput(*, raw: 'RawDict' = , prompt: 'str', agent_type: 'str | None' = None, model: 'str | None' = None, name: 'str | None' = None, run_in_background: 'bool | None' = None) -> None Parsed Agent/Task tool input. BashInput(*, raw: 'RawDict' = , command: 'str', timeout: 'int | None' = None, description: 'str | None' = None) -> None Parsed Bash/Execute tool input. EditInput(*, raw: 'RawDict' = , file_path: 'str', old: 'str', new: 'str', replace_all: 'bool' = False) -> None Parsed Edit tool input with old/new content for replacements. GenericInput(*, raw: 'RawDict' = ) -> None Fallback typed input for unrecognized tools, providing dict-like ``get()`` access to raw data. GlobInput(*, raw: 'RawDict' = , pattern: 'str', path: 'str | None' = None) -> None Parsed Glob tool input. GrepInput(*, raw: 'RawDict' = , pattern: 'str', path: 'str | None' = None, file_type: 'str | None' = None, glob: 'str | None' = None, output_mode: 'str | None' = None) -> None Parsed Grep tool input. ReadInput(*, raw: 'RawDict' = , file_path: 'str', limit: 'int | None' = None, offset: 'int | None' = None) -> None Parsed Read tool input. SkillInput(*, raw: 'RawDict' = , skill: 'str', args: 'str | None' = None) -> None Parsed Skill tool input. TaskCreateInput(*, raw: 'RawDict' = , subject: 'str', description: 'str | None' = None) -> None Parsed TaskCreate tool input. TaskUpdateInput(*, raw: 'RawDict' = , task_id: 'str', status: 'str | None' = None, subject: 'str | None' = None, description: 'str | None' = None) -> None Parsed TaskUpdate tool input. WriteInput(*, raw: 'RawDict' = , file_path: 'str', content: 'str') -> None Parsed Write/Create tool input. ToolResult(*, tool_use_id: 'str', content: 'list[Any] | str' = , is_error: 'bool' = False, is_async: 'bool' = False) -> None A tool-result content block linking back to its tool use via ``tool_use_id``. ## Signals NLP signal scoring over transcript text. Clause(noun: 'Phrase', verb: 'Phrase | None' = None, adj: 'Phrase | None' = None, negated: 'bool' = False) -> None Clause(noun: 'Phrase', verb: 'Phrase | None' = None, adj: 'Phrase | None' = None, negated: 'bool' = False) NlpSignal(*, clauses: 'Sequence[Clause]', weight: 'int' = 1) -> None NlpSignal(*, clauses: 'Sequence[Clause]', weight: 'int' = 1) Phrase(*terms: 'str') -> 'None' Phrase(*terms: 'str') -> 'None' ## State & Sessions Session state, workflow state, and multi-step workflows. HookState(*, fire_count: int = 0) -> None Per-hook persistent state tracked across events in a session (``fire_count`` for ``max_fires``). PrimitiveState(*, last_fired_at: int = 0, consumed: set[str] = , echo_lemmas: set[str] = , echo_window_end: int = 0) -> None Per-primitive state for nudges/gates: last fire index, consumed-signal hashes, and echo-window lemmas. SourceEdits(lang: 'str' = 'py', include_tests: 'bool' = False, paths: 'str | None' = None) -> None SourceEdits(lang: 'str' = 'py', include_tests: 'bool' = False, paths: 'str | None' = None) workflow_state(name: 'str') -> 'Callable[[type[T]], type[T]]' SessionSlot(session_dir: 'Path | None', model: 'type[M]') -> 'None' A typed slot for reading/writing a single Pydantic model in a session directory. SessionStore(session_dir: 'Path | None') -> 'None' Class-keyed store providing typed ``SessionSlot`` access via ``store[ModelClass]``. 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 Workflow(*, label: 'str', marker: 'str', steps: 'list[Step]', artifacts: 'list[Artifact[BaseModel]]' = , post_complete: 'Callable[[BaseHookEvent], HookResult | None] | None' = None, on_start: 'Callable[[BaseHookEvent], HookResult | None] | None' = None) -> None Workflow(*, label: 'str', marker: 'str', steps: 'list[Step]', artifacts: 'list[Artifact[BaseModel]]' = , post_complete: 'Callable[[BaseHookEvent], HookResult | None] | None' = None, on_start: 'Callable[[BaseHookEvent], HookResult | None] | None' = None) Step(*, name: 'str', check: 'Callable[[Transcript], bool]', stopped_at: 'str', next_step: 'str') -> None Step(*, name: 'str', check: 'Callable[[Transcript], bool]', stopped_at: 'str', next_step: 'str') Artifact(*, path: 'str', model: 'type[M]', validate: 'Callable[[M], str | None]' = at 0x7f5d0f7647c0>) -> None Artifact(*, path: 'str', model: 'type[M]', validate: 'Callable[[M], str | None]' = at 0x7f5d0f7647c0>) text_matches(pattern: 'str') -> 'Callable[[Transcript], bool]' workflow(*, label: 'str', marker: 'str', steps: 'list[Step]', artifacts: 'list[Artifact[BaseModel]] | None' = None, post_complete: 'Callable[[BaseHookEvent], HookResult | None] | None' = None, on_start: 'Callable[[BaseHookEvent], HookResult | None] | None' = None, only_if: 'Sequence[TCondition]' = (), skip_if: 'Sequence[TCondition]' = (), tests: 'InlineTests | None' = None) -> 'None' ## SessionStore Methods Methods for the SessionStore class __getitem__(self, model: 'type[M]') -> 'SessionSlot[M]' load(self, model: 'type[M]') -> 'M' Read ``model`` from its session slot, defaulting to a fresh ``model()``. Args: model: The Pydantic model class to read. Returns: The persisted instance, or a newly constructed ``model()`` when no stored state exists for this session. track(model: 'type[BaseModel]') -> 'None' Register ``model`` so it appears in ``tracked_models()`` and ``tracked_paths()``. untrack(model: 'type[BaseModel]') -> 'None' Reverse ``track`` — primarily for test isolation. tracked_models() -> 'Sequence[type[BaseModel]]' Return the registered tracked-state models as an immutable tuple. tracked_paths(self) -> 'dict[str, Path]' Return ``{ModelClass.__name__: Path}`` for every tracked model whose slot has a path. ## Testing Inline tests for hooks. 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) -> None Inline test input descriptor modeling an event payload. Set fields based on the target event type. Allow() -> None Inline test expectation: the hook should allow (return None or action ``"allow"``). Block(*, pattern: 'str | None' = None) -> None Inline test expectation: the hook should block. Optional regex ``pattern`` matches the block message. Warn(*, pattern: 'str | None' = None) -> None Inline test expectation: the hook should warn. Optional regex ``pattern`` matches the warning message. TranscriptFixture(messages: 'list[dict[str, Any]]') -> 'None' 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. ## Configuration & Prompts Settings, scaffolding, and LLM prompt helpers. HooksSettings(_case_sensitive: 'bool | None' = None, _nested_model_default_partial_update: 'bool | None' = None, _env_prefix: 'str | None' = None, _env_prefix_target: 'EnvPrefixTarget | None' = None, _env_file: 'DotenvType | None' = PosixPath('.'), _env_file_encoding: 'str | None' = None, _env_ignore_empty: 'bool | None' = None, _env_nested_delimiter: 'str | None' = None, _env_nested_max_split: 'int | None' = None, _env_parse_none_str: 'str | None' = None, _env_parse_enums: 'bool | None' = None, _cli_prog_name: 'str | None' = None, _cli_parse_args: 'bool | list[str] | tuple[str, ...] | None' = None, _cli_settings_source: 'CliSettingsSource[Any] | None' = None, _cli_parse_none_str: 'str | None' = None, _cli_hide_none_type: 'bool | None' = None, _cli_avoid_json: 'bool | None' = None, _cli_enforce_required: 'bool | None' = None, _cli_use_class_docs_for_groups: 'bool | None' = None, _cli_exit_on_error: 'bool | None' = None, _cli_prefix: 'str | None' = None, _cli_flag_prefix_char: 'str | None' = None, _cli_implicit_flags: "bool | Literal['dual', 'toggle'] | None" = None, _cli_ignore_unknown_args: 'bool | None' = None, _cli_kebab_case: "bool | Literal['all', 'no_enums'] | None" = None, _cli_shortcuts: 'Mapping[str, str | list[str]] | None' = None, _secrets_dir: 'PathType | None' = None, _build_sources: 'tuple[tuple[PydanticBaseSettingsSource, ...], dict[str, Any]] | None' = None, *, planning_agents: list[str] = , waiting_tools: list[str] = , state_dir: pathlib._local.Path = , log_dir: pathlib._local.Path = ) -> None Base settings class for hook configuration, backed by environment variables with ``HOOKS_`` prefix. build_settings(module: 'types.ModuleType', prefix: 'str' = 'HOOKS_') -> 'BaseSettings' Build settings from a conf module via an explicit ``HooksSettings`` subclass or auto-inferred fields. Prompt(*, system_text: 'str' = '', contexts: 'tuple[tuple[str, str], ...]' = (), ask_text: 'str' = '') -> None 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. ## Prompt Methods Methods for the Prompt class system(self, text: 'str') -> 'Prompt' context(self, tag: 'str', content: 'str | None') -> 'Prompt' ask(self, text: 'str') -> 'Prompt' from_template(text: 'str', **vars: 'object') -> 'Prompt' load(name: 'str', *, base: 'str | Path | None' = None, **vars: 'object') -> 'Prompt' 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 ``/.md``; ``name`` may contain ``/`` to nest. Args: name: Prompt name without the ``.md`` suffix; may include ``/`` for nesting. base: Optional directory to search instead of the caller-relative ``prompts/``. **vars: Template variables substituted into the file via ``str.format_map``. Returns: A :class:`Prompt` whose system text is the rendered file contents. Raises: FileNotFoundError: If no matching file exists in any searched directory. KeyError: If the file references a placeholder not supplied in ``**vars``. __str__(self) -> 'str' ---------------------------------------------------------------------- This is the CLI documentation for the package. ---------------------------------------------------------------------- ## CLI: capt-hook ``` Usage: capt-hook [OPTIONS] COMMAND [ARGS]... Captain Hook — declarative hook framework for Claude Code lifecycle events. Options: --hooks TEXT Path to hooks package directory (default: $CLAUDE_PROJECT_DIR/.claude/hooks) --root TEXT Project root for gitignore and session resolution --help Show this message and exit. Commands: generate-settings Generate Claude Code settings JSON for... init Scaffold the hooks directory, install bundled... logs View a recent captain-hook session log. run Dispatch a hook event (reads JSON from stdin, writes JSON to stdout) skills Manage the bundled Claude Code skills. test Run inline tests from all registered hooks. ``` ### capt-hook run ``` Usage: capt-hook run [OPTIONS] EVENT Dispatch a hook event (reads JSON from stdin, writes JSON to stdout). EVENT is one of: PreToolUse, PostToolUse, PostToolUseFailure, UserPromptSubmit, Stop, SubagentStop, SubagentStart, PreCompact, Notification. Options: --async Run async hooks only --help Show this message and exit. ``` ### capt-hook generate-settings ``` Usage: capt-hook generate-settings [OPTIONS] Generate Claude Code settings JSON for .claude/settings.local.json. Options: --hooks-dir TEXT Hooks directory relative to project root --no-merge Output standalone JSON instead of merging --from TEXT Package source for uvx --from (local path or PyPI spec, default: capt-hook) --help Show this message and exit. ``` ### capt-hook test ``` Usage: capt-hook test [OPTIONS] Run inline tests from all registered hooks. Options: --json Emit one JSON record per test (CI mode) --help Show this message and exit. ``` ### capt-hook init ``` Usage: capt-hook init [OPTIONS] Scaffold the hooks directory, install bundled skills, and wire settings. Options: --help Show this message and exit. ``` ### capt-hook logs ``` Usage: capt-hook logs [OPTIONS] View a recent captain-hook session log. Options: --session TEXT Session id or transcript path (hashed) to view --tail INTEGER Show only the last N lines --help Show this message and exit. ``` ### capt-hook skills ``` Usage: capt-hook skills [OPTIONS] COMMAND [ARGS]... Manage the bundled Claude Code skills. Options: --help Show this message and exit. Commands: install Copy the bundled skills into .claude/skills/. ``` ### capt-hook skills install ``` Usage: capt-hook skills install [OPTIONS] Copy the bundled skills into .claude/skills/. Options: --force Replace skills that already exist in .claude/skills --help Show this message and exit. ```