## Conditions


## Tool


Condition matching the current event's tool name against one or more names.


Usage

``` python
Tool(*names)
```


Matching is exact membership, honoring cross-editor aliases (`Bash`/`Execute`, `Write`/`Create`, …) and MCP suffixes (`mcp__github__Grep` matches `Grep`) -- NOT a regex, so `Tool("Edit.*")` never matches. Pass names variadically, or as a single `|`-joined string for back-compat (`Tool("Bash|Execute")` is `Tool("Bash", "Execute")`). `Tool.EditTools` is the prebuilt full edit-shaped set -- `Tool("Edit", "MultiEdit", "NotebookEdit", "Write")`.


#### Parameter Attributes


`names: str = ()`  


#### Example

``` python
>>> hook(Event.PreToolUse, only_if=[Tool("Bash", "Execute")], message="...", block=True)
>>> hook(Event.PreToolUse, only_if=[Tool.EditTools], message="...", block=True)
```


## FilePath


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


Usage

``` python
FilePath(*patterns, project_only=True)
```


Accepts one or more glob patterns as positional arguments.


#### Parameter Attributes


`patterns: str = ()`  

`project_only: bool = ``True`  


#### Example

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


## types.Command


Condition matching the current event's bash command against a regex.


Usage

``` python
types.Command(pattern)
```


Searched against the raw command line *and* each parsed command's argv join, so a pattern spanning pipes/operators/redirects (`curl ... | sh`) matches. Only relevant for `PreToolUse` events targeting the Bash/Execute tool. The regex is compiled at construction, so a malformed pattern raises immediately rather than at dispatch.


#### Parameter Attributes


`pattern: str`  


#### Example

``` python
>>> hook(Event.PreToolUse, only_if=[Command(r"git\s+stash")], message="blocked", block=True)
```


## Content


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


Usage

``` python
Content(pattern, project_only=True)
```


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


#### Parameters


`pattern: str`  
A regular expression searched against the content with `re.MULTILINE`.

`project_only: bool = ``True`  
Only fire on files inside the repository root (default `True`); set `False` to also match scratch files, attachments, and other paths outside the repo.


## Pattern


Condition matching the edit's new content against an ast-grep **structural** pattern.


Usage

``` python
Pattern(pattern, lang=None, project_only=True)
```


Where \[[Content](conditions.md#captain_hook.Content)\]\[captain_hook.types.Content\] matches text with a regex, [Pattern](conditions.md#captain_hook.Pattern) matches code *shape* across languages -- `print($$$)` matches a print call however its arguments are spelled, ignoring matches inside strings or comments.


#### Parameters


`pattern: str`  
An ast-grep pattern string with metavariables, e.g. `"os.system($CMD)"` or `"print($$$)"`.

`lang: str | None = None`  
ast-grep language id (the short keys of \[`LANG_GLOBS`\]\[captain_hook.langs.LANG_GLOBS\], e.g. `"py"`, `"go"`); inferred from the edited file's extension when omitted.

`project_only: bool = ``True`  
Only fire on files inside the repository root (default `True`), like \[[Content](conditions.md#captain_hook.Content)\]\[captain_hook.types.Content\].


#### Example

``` python
>>> hook(Event.PreToolUse, only_if=[Pattern("eval($$$)")], message="no eval", block=True)
```


## TouchedFile


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


Usage

``` python
TouchedFile(*patterns, subagents=False)
```


Accepts one or more glob patterns as positional arguments. Defaults to the main agent's edits only (subagent edits rarely gate the main session); pass `subagents=True` to include sidechains.


#### Parameter Attributes


`patterns: str = ()`  

`subagents: bool = ``False`  


## TestFile


Condition that matches when the current event targets a test file.


Usage

``` python
TestFile(project_only=True)
```


A test file is `test_*.py`, `*_test.py`, `conftest.py`, any `.py` under a `tests/` directory, `*_test.go`, `*.test.*`, or `*.spec.*`.


#### Parameter Attributes


`project_only: bool = ``True`  


## ReadFile


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


Usage

``` python
ReadFile(*patterns, subagents=True)
```


Accepts one or more `fnmatch` glob patterns as positional arguments, matched against each Read's full path and its basename -- so `ReadFile("*.md")` matches by extension and a bare `ReadFile("TESTING.md")` matches that file in any directory. Reads are recorded as absolute paths, so anchor a directory match with a leading `**/` (`ReadFile("**/docs/**")`), the same idiom the test-file globs use.


#### Parameter Attributes


`patterns: str = ()`  

`subagents: bool = ``True`  


## RanCommand


Transcript-history condition: true when a Bash tool use running `argv` exists.


Usage

``` python
RanCommand(*argv, subagents=True)
```


`argv` is matched as a leading token prefix of a parsed command's argv via `Session.has_command` -- wrapper-transparent (`sudo`/`env`/`timeout` are stripped) and quote-exact, so `RanCommand("git", "push")` matches `sudo git push -f` but not `echo "git push"`. Launchers are literal: `RanCommand("pytest")` does not match `uv run pytest` -- list each spelling as its own entry.


#### Parameter Attributes


`argv: str = ()`  

`subagents: bool = ``True`  


## Runs


Structural Bash condition: true when a parsed command's argv starts with `argv`.


Usage

``` python
Runs(*argv)
```


Where \[[Command](files-commands.md#captain_hook.Command)\]\[captain_hook.types.Command\] regex-matches the command text (and can false-fire on `echo git stash`), [Runs](conditions.md#captain_hook.Runs) matches the argv prefix of any command in the line -- so `Runs("git", "stash")` fires on `git stash`, `git stash pop`, and `a && git stash` but not `echo git stash`. Only relevant for Bash/Execute events.


#### Parameter Attributes


`argv: str = ()`  


#### Example

``` python
>>> hook(Event.PreToolUse, only_if=[Runs("git", "stash")], message="no stashing", block=True)
```


## UsedSkill


Transcript-history condition: true when a Skill tool use named one of `names`.


Usage

``` python
UsedSkill(*names, subagents=True, scope="turn")
```


Pass names variadically; a single `|`-joined string still works for back-compat. A bare name also matches the plugin-qualified spelling, so `UsedSkill("codex")` matches a skill reported as `codex:codex` and `UsedSkill("slop-cop-check")` matches `slop-cop:slop-cop-check`. The search covers the current turn by default -- the idiom for skipping a guard once the agent has complied; `scope="session"` widens it to the whole session.


#### Parameter Attributes


`names: str = ()`  

`subagents: bool = ``True`  

`scope: Literal[``"session", `<span class="st">`"turn"``]`</span>` = ``"turn"`  


#### Example

``` python
>>> nudge("run codex first", skip_if=[UsedSkill("codex")])
```


## UsedTool


Transcript-history condition: true when a tool use named one of `names` exists.


Usage

``` python
UsedTool(*names, subagents=True, scope="turn")
```


Pass names variadically; a single `|`-joined string still works for back-compat. The search covers the current turn by default -- the idiom for skipping a guard once the agent has complied; `scope="session"` widens it to the whole session.


#### Parameter Attributes


`names: str = ()`  

`subagents: bool = ``True`  

`scope: Literal[``"session", `<span class="st">`"turn"``]`</span>` = ``"turn"`  


#### Example

``` python
>>> hook(Event.PreToolUse, skip_if=[UsedTool("EnterPlanMode")], message="...", block=True)
```


## UserSaid


Matches when a user prompt matches one of `patterns`.


Usage

``` python
UserSaid(*patterns, scope="turn")
```


A string pattern is a case-insensitive regex, so plain keywords keep their substring behavior; a `~captain_hook.Clause` runs the dependency-clause scan against each prompt. The default scans only what the user said in the current turn -- the idiom for reacting to a directive the user just gave; `scope="session"` widens the scan to every prompt in the session.


#### Example

``` python
>>> UserSaid(Clause(noun=Phrase("work"), verb=Phrase("stop", "halt")))
>>> UserSaid("just commit", Clause(noun=Phrase("test"), verb=Phrase("skip")), scope="session")
```


## InPlanMode


Matches when the agent is in plan mode.


Usage

``` python
InPlanMode()
```


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


Condition matching while the session is parked on out-of-band work.


Usage

``` python
Waiting()
```


Any of three signals counts as waiting. The Stop payload's [background_tasks](events-results.md#captain_hook.BaseHookEvent.background_tasks) and [session_crons](events-results.md#captain_hook.BaseHookEvent.session_crons) arrays (Claude Code 2.1.145+, `Stop`/`SubagentStop` only) report in-flight registry work: background shells, sub-agents, workflows, monitors, teammates, and scheduled wake-ups -- so a long-lived background shell such as a dev server keeps the session waiting, by the platform's own "this will wake the session back up" semantics. The transcript's notification queue holds a `<task-notification>` that was enqueued but not yet delivered to the agent -- a finished task the agent hasn't seen. Or an async [Agent](events-results.md#captain_hook.Agent)/[Task](tasks.md#captain_hook.Task)/[Workflow](state-sessions.md#captain_hook.Workflow) launch (tracked across turns, so a launch in an earlier turn still counts) has no delivered notification, and current-turn waits such as `run_in_background` Bash or `ScheduleWakeup`/`Monitor` are in flight.

Blocking Stop gates built with [gate](primitives.md#captain_hook.gate)/[llm_gate](primitives.md#captain_hook.llm_gate) skip on it automatically, additively with any `skip_if` given, so the agent isn't nagged for pausing on work it is correctly waiting on.


#### Example

``` python
>>> gate("Run tests before stopping", skip_if=[Waiting()])
```


## FromSubagent


Condition matching when the current event originates from a subagent or teammate.


Usage

``` python
FromSubagent()
```


True when the event payload carries a non-empty [agent_id](events-results.md#captain_hook.BaseHookEvent.agent_id), which Claude Code sends only for subagent and teammate events. Distinct from [Agent](events-results.md#captain_hook.Agent), which matches the subagent *type*: this matches the event's *origin*.


#### Example

``` python
>>> approve("teammate bash", only_if=[Tool("Bash"), FromSubagent(), SkipPermissions()])
```


## SkipPermissions


Condition matching when the session was launched with permission bypass available.


Usage

``` python
SkipPermissions()
```


Walks the process tree to the nearest `claude` ancestor and matches when that one process was launched with `--dangerously-skip-permissions` **or** `--allow-dangerously-skip-permissions` -- either spelling means the user made bypass *available* at launch, which counts as consent even while the active `permission_mode` is something else (e.g. `plan`).


#### Example

``` python
>>> approve("teammate bash", only_if=[Tool("Bash"), FromSubagent(), SkipPermissions()])
```


## FromTeammate


Matches when the current turn's in-flight subagent spawn is a named teammate.


Usage

``` python
FromTeammate()
```


A teammate spawn carries a [name](files-commands.md#captain_hook.Call.name) in its Agent/Task input, which cc-transcript surfaces as `TaskCall.agent_name`; a plain subagent leaves it unset. Scoped to the current turn so a teammate still running from an earlier turn never re-fires against a later unnamed spawn.


#### Example

``` python
>>> nudge("Return tight digests", only_if=[FromTeammate()], events=Event.SubagentStart)
```


## FreshSession


Matches a `SessionStart` from a fresh `startup` or `clear`, not a `resume` or `compact`.


Usage

``` python
FreshSession()
```


#### Example

``` python
>>> nudge("Preload your tools", only_if=[FreshSession()], events=Event.SessionStart)
```


## Headless


Matches a headless `claude -p` / SDK run (`CLAUDE_CODE_ENTRYPOINT` in the `sdk-*` family).


Usage

``` python
Headless()
```


#### Example

``` python
>>> llm_gate("Check docs freshness", skip_if=[Headless()], events=Event.Stop)
```


## RewritingExistingPlan


Matches a `Write` to a plan file (`.md` under `plans/` or `specs/`) already written this


Usage

``` python
RewritingExistingPlan()
```


session, with no new plan cycle (`EnterPlanMode`) since the last `Write` to it.

Reads from `evt.ctx.prior` (the window before the current turn's last exchange) so the pending `Write` being evaluated is never itself counted as the prior edit. A write to the file this session already implies it exists, so no filesystem check is needed.


#### Example

``` python
>>> hook(Event.PreToolUse, only_if=[Tool("Write"), RewritingExistingPlan()],
...      message="Edit the plan instead of rewriting it", block=True)
```


## ScratchPath


Matches a file-tool target resolving into a system temp root or a scratch-named directory.


Usage

``` python
ScratchPath()
```


Resolves a relative path against `evt.cwd` (a relative path with no cwd never matches), then fully resolves it -- following a final-component symlink -- before the scratch-path heuristic. The full resolve is a security boundary: this gates the skip-permissions auto-approver, so a symlink that sits in a scratch directory but points at a real file must not be treated as scratch.


#### Example

``` python
>>> approve("scratch writes", only_if=[Tool("Write"), ScratchPath(), SkipPermissions()])
```


## EditedSource


Matches when the session edited a non-test, in-repo source file (docs and config excluded).


Usage

``` python
EditedSource(*, exclude_suffixes=NON_SOURCE_SUFFIXES)
```


`exclude_suffixes` tailors which extensions count as non-source (prose/config); it defaults to `NON_SOURCE_SUFFIXES`.


#### Example

``` python
>>> llm_gate("Review the diff before stopping", only_if=[EditedSource()], events=Event.Stop)
```


## Commits


Matches a command line whose primary command explicitly names a path ending in `suffix`.


Usage

``` python
Commits(suffix)
```


Pairs with a `git commit` filter to gate a language's test run only when the commit stages a file of that language -- `Commits(".py")`, `Commits(".go")`.


#### Example

``` python
>>> gate("Run pytest first", only_if=[Runs("git", "commit"), Commits(".py")])
```


## Redirects


Matches when any command in the line carries a shell redirect (input or output).


Usage

``` python
Redirects()
```


Detects file redirects on the parsed commands -- `>`, `>>`, `2>`, `<`, and fd duplications such as `2>&1`. A bare pipe (`echo x | cat`) moves data between commands but is not a redirect, so it never matches.


#### Example

``` python
>>> hook(Event.PreToolUse, only_if=[Tool("Bash"), Redirects()], message="no redirects", block=True)
```


## Or


Match if any of the inner conditions matches.


Usage

``` python
Or(*conditions)
```


## And


Match only if every inner condition matches (useful nested inside [Or](conditions.md#captain_hook.Or)/[Not](conditions.md#captain_hook.Not)).


Usage

``` python
And(*conditions)
```


## Not


Match only if the inner condition does not match.


Usage

``` python
Not(condition)
```


#### Parameter Attributes


`condition: TCondition`  


## Signal


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


Usage

``` python
Signal(*, pattern, weight=1, flags=0)
```


Signals are matched against transcript text via `re.search`. In a [Signals](conditions.md#captain_hook.Signals) bundle each matched signal contributes `weight` once toward the threshold; pattern weights must be positive, and false positives are suppressed with `Signals.vetoes`, not negative weights.


#### Parameter Attributes


`pattern: str`  

`weight: int = ``1`  

`flags: int = ``0`  


#### Example

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


## Signals


Bundle of signal patterns with a scoring threshold.


Usage

``` python
Signals(
    patterns,
    threshold,
    window=15,
    vetoes=(),
    *,
    scope="text",
    origin="assistant"
)
```


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.

`scope` selects how `threshold` is met across the scored candidate texts. The default `"text"` requires a single candidate text to meet `threshold` on its own; `window` then only bounds how far back a qualifying text may sit. `"window"` instead sums by presence-union across the window: each signal counts once toward `threshold` however many entries it matches, and the union score is the sum of the distinct matching signals' weights. Reach for `"window"` only when one tell is deliberately split across turns (a checklist in one message, its sign-off in the next). Pattern weights must be positive under either scope.

`origin` selects which candidate texts are eligible before scoring -- an upstream filter orthogonal to `scope` (which decides how `threshold` is met among the eligible texts). The default `"assistant"` scores only the agent's own prose (assistant messages, thinking blocks, prose-carrying tool calls), so a stance nudge never fires on the user's words quoted back into the transcript. Pass `"any"` to also score user messages -- required for a hook that scores the user's prompt or correction (a `UserPromptSubmit` nudge, say), where the just-submitted prompt is prepended to the scan only under `"any"`.

`vetoes` are presence-only suppressors: if any veto matches any window entry -- including already-consumed ones -- the bundle does not fire and consumes nothing. A veto's `weight` is meaningless and must be left at its default.

`window` bounds how much transcript each scoring pass reads: the last `window` events by default, or the whole current turn with the `"turn"` sentinel. Use `"turn"` when the tell can sit hundreds of events before the turn ends -- a fixed window at `Stop` only reaches the last few tool exchanges.


#### Parameter Attributes


`patterns: Sequence[`<a href="conditions.html#captain_hook.Signal" class="gdls-link gdls-code"><code>Signal</code></a>` | `<a href="signals.html#captain_hook.NlpSignal" class="gdls-link gdls-code"><code>NlpSignal</code></a>`]`  

`threshold: int`  

`window: int | Literal[``"turn"]`` = ``15`  

`vetoes: Sequence[`<a href="conditions.html#captain_hook.Signal" class="gdls-link gdls-code"><code>Signal</code></a>` | `<a href="signals.html#captain_hook.NlpSignal" class="gdls-link gdls-code"><code>NlpSignal</code></a>`] = ()`    

`scope: Literal[``"text", `<span class="st">`"window"``]`</span>` = ``"text"`  

`origin: Literal[``"assistant", `<span class="st">`"any"``]`</span>` = ``"assistant"`  


## CustomCondition


Protocol for user-defined hook conditions.


Usage

``` python
CustomCondition()
```


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

Override `valid_events` to restrict to the events your condition can read.


#### Example

``` python
>>> 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)
```


## CustomCommandLineCondition


CustomCondition that fires only when a parsed Bash command line is present.


Usage

``` python
CustomCommandLineCondition()
```


Implement `check_command_line` to match against the structured command line; `check` returns False for events without one (non-Bash tools).


## CustomInputTypeCondition


CustomCondition that fires only for a specific typed tool call.


Usage

``` python
CustomInputTypeCondition()
```


Parameterize with the tool call type and implement `check_input`; `check` narrows the event's input to that type and skips events of any other tool.


#### Example

``` python
>>> class BigRead(CustomInputTypeCondition[ReadCall]):
...     def check_input(self, evt: BaseHookEvent, call: ReadCall) -> bool:
...         return call.limit is not None and call.limit > 1000
```


## workflow_script_source()


The pending [Workflow](state-sessions.md#captain_hook.Workflow) call's script source, or `None`.


Usage

``` python
workflow_script_source(evt)
```


Returns the inline `script` when present, else reads the file at `scriptPath`. A non-Workflow event, a missing/unreadable path, or a file over ~1 MiB yields `None`; never raises.


## workflow_opt_values()


Best-effort raw-source scan for values pinned to `key` in agent() opts. Never raises.


Usage

``` python
workflow_opt_values(source, key)
```


Values are single-line: the whitespace around the colon is horizontal only, so a valueless `key:` at end of line does not swallow the next line's token.
