captain_hook
captain_hook ¶
InlineTests ¶
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
¶
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
¶
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.
BaseHookEvent
dataclass
¶
Base class for all hook events, providing access to raw payload, context, and convenience methods.
tasks
cached
property
¶
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 ¶
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 |
()
|
Returns:
| Type | Description |
|---|---|
HookResult
|
A warn :class: |
NotificationEvent
dataclass
¶
Bases: BaseHookEvent
Fires on system notifications, providing message, title, and notification type.
PostToolUseEvent
dataclass
¶
PostToolUseFailureEvent
dataclass
¶
PreCompactEvent
dataclass
¶
Bases: BaseHookEvent
Fires before context compaction, providing the trigger and custom instructions.
PreToolUseEvent
dataclass
¶
StopEvent
dataclass
¶
Bases: BaseHookEvent
Fires when the agent is about to stop. Return a block result to prevent stopping.
SubagentStartEvent
dataclass
¶
SubagentStopEvent
dataclass
¶
ToolHookEvent
dataclass
¶
Bases: BaseHookEvent
Event for tool-related hooks, adding tool name, input, command, and file access.
UserPromptSubmitEvent
dataclass
¶
File
dataclass
¶
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
¶
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 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 |
required |
base
|
str | Path | None
|
Optional directory to search instead of the caller-relative |
None
|
**vars
|
object
|
Template variables substituted into the file via |
{}
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
PromptMessage
|
class: |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If no matching file exists in any searched directory. |
KeyError
|
If the file references a placeholder not supplied in |
SessionSlot ¶
Bases: Generic[M]
A typed slot for reading/writing a single Pydantic model in a session directory.
SessionStore ¶
Class-keyed store providing typed SessionSlot access via store[ModelClass].
load ¶
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 |
M
|
stored state exists for this session. |
track
classmethod
¶
Register model so it appears in tracked_models() and tracked_paths().
untrack
classmethod
¶
Reverse track — primarily for test isolation.
tracked_models
classmethod
¶
Return the registered tracked-state models as an immutable tuple.
tracked_paths ¶
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
¶
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.
Allow
dataclass
¶
Inline test expectation: the hook should allow (return None or action "allow").
Block
dataclass
¶
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 ¶
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
¶
Inline test expectation: the hook should warn. Optional regex pattern matches the warning message.
EditOp
dataclass
¶
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
¶
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
¶
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 ¶
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
¶
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
¶
BashInput
dataclass
¶
EditInput
dataclass
¶
FileInputBase
dataclass
¶
Bases: InputBase
Base for tool inputs that reference a file, providing a cached file property returning a File.
GenericInput
dataclass
¶
Bases: InputBase
Fallback typed input for unrecognized tools, providing dict-like get() access to raw data.
GlobInput
dataclass
¶
GrepInput
dataclass
¶
InputBase
dataclass
¶
Base class for typed tool inputs. Provides from_raw() parsing and as_() type narrowing.
ReadInput
dataclass
¶
SkillInput
dataclass
¶
TaskCreateInput
dataclass
¶
TaskUpdateInput
dataclass
¶
WriteInput
dataclass
¶
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
¶
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
¶
Condition matching the current event's subagent type against a name pattern.
Content
dataclass
¶
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 ¶
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
¶
The return value from a hook handler, specifying the action and optional message.
of
classmethod
¶
Build a HookResult, dedenting and stripping message for readable triple-quoted handler returns.
InPlanMode
dataclass
¶
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
¶
Transcript-history condition: true when a Bash tool use with a matching command exists.
ReadFile ¶
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
¶
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
¶
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
¶
Condition that matches when the current event targets a test file (test_*.py, conftest.py).
Tool
dataclass
¶
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 ¶
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
¶
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 ¶
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 |
'py'
|
Returns:
| Type | Description |
|---|---|
list[str]
|
A |
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 ¶
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 ¶
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 |
None
|
filename
|
Callable[[datetime], str]
|
|
lambda d: f'{d}.jsonl'
|
fields
|
Callable[[BaseHookEvent], dict[str, Any]]
|
|
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]]
| 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 ¶
Decorator that registers a Pydantic model for collective SessionStore introspection.
Example
@session_state ... class Snapshot(BaseModel): ... op_id: str
build_settings ¶
Build a settings instance from a conf module, using an explicit HooksSettings subclass or auto-inferring fields.
cite_message ¶
Append trigger context to a message when signal matches are found.
resolve_signals ¶
Normalize signals input into a Signals bundle, or None.
A bare list[Signal] is wrapped with threshold=1 (any single match triggers).
transcript_texts ¶
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 raw tool input into a typed dataclass based on tool name.