Conditions

Every condition type, in one place. Use conditions in only_if (all must match) or skip_if (any match skips the hook). Filter hooks with conditions shows how to pick and combine them.

Current event

These match the event being processed right now.

Condition Matches when Pattern Example
Tool(*names) The tool name is one of names Exact names (aliases + MCP suffixes auto-match); a \|-joined string still works; Tool.EditTools is the prebuilt edit-shaped set (Edit, MultiEdit, NotebookEdit, Write) Tool.EditTools
FilePath(*p, project_only=True) The event’s file path matches any glob Glob FilePath("**/*.py")
Command(p) The raw Bash line or any parsed command matches Regex Command(r"curl.*\|.*sh")
Runs(*argv) Any parsed command’s argv starts with argv Argv prefix (structural) Runs("git", "stash")
Content(p, project_only=True) The written/edited file content matches Regex (multiline) Content(r"print\(")
ToolInput(**fields) Every named top-level tool-input field matches (any tool; scalars coerced to text) Regex per field (multiline) ToolInput(model=r"(?i)\bhaiku\b")
WorkflowScript(pattern=None, **opts) A Workflow tool’s inline script (or script_path file): pattern searches the raw source, each opt key’s regex matches a value pinned for that agent() opt; all AND Regex WorkflowScript(model="haiku")
Agent(*names) The subagent type is one of names Exact names; a \|-joined string still works Agent("cleanup", "refactor")
FromSubagent() The event originates from a subagent or teammate, i.e. the payload carries an agent_id; matches the event’s origin, where Agent matches its type FromSubagent()
TestFile(project_only=True) The file is a test file (test_*.py, conftest.py, or any .py under a tests/ directory) TestFile()
SourceEdits(lang="py", include_tests=False, paths=None, project_only=True) The event edits/writes a source file in lang (and not a test, unless include_tests) Language glob SourceEdits(lang="ts", paths="src/**")

FilePath, Content, TestFile, and SourceEdits match project files only by default. Pass project_only=False to target external scratch files, attachments, or logs.

Session state

These check what happened earlier in the session, from the transcript.

Condition Matches when Pattern Example
ReadFile(*p, subagents=True) A file matching the glob was read Glob ReadFile("TESTING.md")
TouchedFile(*p, subagents=False) A file matching the glob was edited or written Glob TouchedFile("**/*.py")
RanCommand(*argv, subagents=True) A Bash command running the argv prefix ran (wrapper-transparent, launcher-literal) Argv tokens RanCommand("uv", "run", "pytest")
UsedSkill(*names, subagents=True, scope="turn") A skill named one of names was invoked in the current turn (a bare name also matches plugin:name); scope="session" searches the whole session Exact names; a \|-joined string still works UsedSkill("codex")
UsedTool(*names, subagents=True, scope="turn") A tool use named one of names exists in the current turn; scope="session" searches the whole session Exact names; a \|-joined string still works UsedTool("EnterPlanMode")
UserSaid(*patterns, scope="turn") The current turn’s prompt matches a pattern; scope="session" scans every prompt in the session Case-insensitive regex strings and/or Clauses UserSaid("just commit")
InPlanMode() The agent is in plan mode InPlanMode()
Waiting() A background or async tool is still pending. Durable Workflow and async sub-agent launches count across turns until their completion notification lands Waiting()
SkipPermissions() The session’s claude process was launched with --dangerously-skip-permissions or --allow-dangerously-skip-permissions, found by walking the process tree to the nearest claude ancestor. Either spelling means the user made bypass available at launch, which counts as consent even while the active permission_mode is something else, such as plan SkipPermissions()

subagents=True also searches subagent transcripts; TouchedFile defaults to False (the main agent’s edits only).

Combining conditions

only_if=[A, B, C]   # A AND B AND C must all match
skip_if=[X, Y]      # X OR Y — any match skips the hook

A matching skip_if skips the hook even when every only_if condition matches. For OR within one condition, list the names (Tool("Bash", "Execute")). To combine across condition types, wrap them: Or(*conds) (any), And(*conds) (all — useful nested inside Or), and Not(cond) (negate). All three are exported from captain_hook.

Custom conditions

CustomCondition is a runtime_checkable protocol. Any frozen dataclass with a check(self, evt: BaseHookEvent) -> bool method satisfies it and slots into only_if/skip_if. See Write a custom condition for a worked example.

Two typed base classes narrow check to one shape of input. Subclass and implement the inner method; the base handles narrowing and skips events that don’t apply.

Base Implement Fires when
CustomInputTypeCondition[T] check_input(self, evt, call: T) -> bool evt.input is a T (e.g. ReadCall) and check_input returns True; skips every other tool
CustomCommandLineCondition check_command_line(self, evt, cl) -> bool the event has a parsed Bash CommandLine and check_command_line returns True; returns False for non-Bash tools

CustomInputTypeCondition[T] narrows evt.input to the parameterized *Call type, so call arrives typed for direct field access like call.limit and call.file_path. CustomCommandLineCondition receives the parsed CommandLine; query it via cl.q, whose methods are runs, has_subcommand, contains_token, and uses_redirect. See Match a typed tool call and Match a structured command line.

Signals (scoring)

Signal and Signals aren’t only_if/skip_if filters — they pass to a primitive’s signals= parameter to score patterns against recent transcript prose. By default (scope="text") a single message must meet the threshold on its own matched weights, and the window only bounds how far back it may sit. Under scope="window" the score pools distinct signals across the window instead, counting each once. origin filters candidates upstream of either scope: the default "assistant" scores only the agent’s own prose, while origin="any" also scores user messages and the just-submitted UserPromptSubmit prompt. See Score patterns with signals.

Type Purpose Example
Signal(pattern=..., weight=1, flags=0) A regex worth weight points when it matches (fields are keyword-only) Signal(pattern=r"\bretry\b", weight=2)
Signals(patterns, threshold, window=15, scope="text", origin="assistant") A bundle that fires at threshold; scope="text" (default) requires a single text in the window to meet the threshold alone, scope="window" counts each matching signal once across the whole window; origin="assistant" (default) scores only the agent’s own prose, origin="any" also scores user text and the UserPromptSubmit prompt; vetoes match window-wide under either scope; window="turn" scores the whole current turn Signals([Signal(pattern=r"retry", weight=2), Signal(pattern=r"again")], threshold=3)