## Primitives


## gate()


Register a blocking gate -- `nudge(message, block=True, ...)` with an explicit signature.


Usage

``` python
gate(
    message,
    *,
    when=None,
    signals=None,
    only_if=(),
    skip_if=(),
    events=None,
    max_fires=DEFAULT_FIRES,
    tests=None,
    async_=False,
    skip_planning_agents=None
)
```


A gate keeps enforcing: it defaults to unlimited fires (once per turn) and skips while the session is `~captain_hook.types.Waiting`, additively with any `skip_if` given. Because it blocks, `skip_planning_agents` resolves (via `None`) to `False` -- enforcement runs on every agent type, including delegated `general-purpose` subagents at `SubagentStop`.


#### Example

``` python
>>> gate("Run tests before committing", when=lambda evt: not has_tests(evt))
```


## nudge()


Register a nudge that warns (or blocks) when conditions or signals match.


Usage

``` python
nudge(
    message,
    *,
    when=None,
    signals=None,
    only_if=(),
    skip_if=(),
    block=False,
    events=None,
    max_fires=DEFAULT_FIRES,
    tests=None,
    async_=False,
    skip_planning_agents=None
)
```


`when` and `signals` compose: signals decide whether the message fires, and `when` is a veto checked first (both must pass). The triggering event defaults to `Stop | SubagentStop` for a blocking gate, `PostToolUse` for a signal-scored nudge, and `PreToolUse` otherwise; pass `events=` to override. Blocking Stop/SubagentStop gates additionally skip while `~captain_hook.types.Waiting`, on top of any `skip_if`.

`skip_planning_agents` defaults to `None`, resolving to `not block`: a blocking gate enforces on every agent type, while a warning nudge skips planning/exploration subagents (only `SubagentStop`/`SubagentStart` are affected). Pass `True`/`False` to override.


#### Example

``` python
>>> nudge("Remember to run tests", only_if=[TouchedFile("**/*.py")])
```

With signal scoring:

``` python
    >>> nudge("Stop retrying",
    ...       signals=Signals([Signal(r"retry", weight=2)], threshold=2, window=5))
```


## lint()


Register a lint check that runs on source-file edits and writes.


Usage

``` python
lint(check: Callable[[str], list[str]], message: str, lang: str = ..., trigger: str | None = ..., sep: str = ..., block: bool = ..., events: Event | None = ..., tests: InlineTests | None = ..., max_shown: int = ...) -> None
lint(check: Callable[[ast.AST], Iterator[str]], message: str, lang: str = ..., trigger: str | None = ..., sep: str = ..., block: bool = ..., events: Event | None = ..., tests: InlineTests | None = ..., max_shown: int = ...) -> None
lint(pattern: str, message: str, lang: str = ..., sep: str = ..., block: bool = ..., events: Event | None = ..., tests: InlineTests | None = ..., max_shown: int = ...) -> None
```


Three modes, exactly one of `check` or `pattern`: - **String mode**: `check` receives the file content as `str`, returns violation strings. - **AST mode**: `check` receives each `ast.AST` node, yields violation strings (Python only). - **ast-grep mode**: `pattern` is an ast-grep pattern string; every structural match becomes a `"{snippet} (line N)"` violation. `lang` (default `"py"`) selects the language *and* the file guard, so a `lang="ts"` lint fires on `*.ts` edits.


#### Example

``` python
>>> 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}")
>>> lint(pattern="console.log($$$)", message="Drop console.log: {violations}", lang="ts")
```


## diff_lint()


Register a diff lint: flag only constructs the edit introduces.


Usage

``` python
diff_lint(check: DiffCheck, message: str, sep: str = ..., block: bool = ..., events: Event | None = ..., tests: InlineTests | None = ..., max_shown: int = ..., only_if: Sequence[TCondition] = ..., skip_if: Sequence[TCondition] = ...) -> None
diff_lint(pattern: str, message: str, lang: str = ..., sep: str = ..., block: bool = ..., events: Event | None = ..., tests: InlineTests | None = ..., max_shown: int = ..., only_if: Sequence[TCondition] = ..., skip_if: Sequence[TCondition] = ...) -> None
```


Two modes, exactly one of `check` or `pattern`: - **Tree mode**: `check` receives the parsed pre- and post-edit trees as `ast.AST` and returns violation strings for the delta (Python only). - **ast-grep mode**: `pattern` is an ast-grep pattern string; every structural match present in the new text but absent from the old becomes a `"{snippet} (line N)"` violation. Match identity is the whitespace-normalized text, so a construct the edit merely moved never fires. `lang` (default `"py"`) selects the language *and* the file guard, like [lint](primitives.md#captain_hook.lint).


#### Example

``` python
>>> diff_lint(pattern="print($$$)", message="This edit added a print() call: {violations}")
```


## block_command()


Register a declarative hook that blocks a Bash command matching a pattern.


Usage

``` python
block_command(
    pattern, *, reason, hint=None, only_if=(), skip_if=(), tests=None
)
```


Scope the block further with `only_if`/`skip_if` (ANDed onto the built-in `[Tool("Bash"), <pattern>]`), e.g. to allow it in plan mode or only under a path.


#### Example

``` python
>>> block_command(["git", "stash"], reason="git stash is not allowed", hint="Use jj")
```


## warn_command()


Register a declarative hook that warns on a Bash command matching a pattern.


Usage

``` python
warn_command(
    pattern,
    *,
    message,
    only_if=(),
    skip_if=(),
    tests=None,
    events=Event.PostToolUse
)
```


#### Example

``` python
>>> warn_command(["python", "-c", "*"], message="Prefer uv run mtest")
```


## rewrite_command()


Register a `PreToolUse` hook that rewrites a matching Bash command.


Usage

``` python
rewrite_command(
    pattern=None,
    replace=None,
    *,
    only_if=(),
    skip_if=(),
    to=None,
    block=None,
    note=None,
    tests=None
)
```


Two mutually exclusive forms:

- **(pattern, replace)** -- polymorphic by the shape of `pattern`. When it carries an ast-grep metavariable (`$NAME` / `$$$NAME`), the command is rewritten *structurally* over tree-sitter-bash via \[`ast_grep.rewrite`\]\[captain_hook.ast_grep.rewrite\]; otherwise it is a regex rewritten via `re.sub`. So `rewrite_command("cat $$$ARGS", "bat $$$ARGS")` is structural and `rewrite_command(r"^cat\s+(\S+)$", r"bat \1")` is regex. `note` surfaces as `additionalContext` so the model sees the substitution.
- **Conditional** -- pass `to`, a callable mapping the event to its new command (gated by `[Tool("Bash"), *only_if]`); the escape hatch when the heuristic isn't enough. When `to` returns None the hook blocks with [block](prompt-contexts.md#captain_hook.Excerpts.block) if given, otherwise passes through. `note` may be a callable computed from the event.

A structural single metavar binds only the first argument and drops the rest (`"cat $F"` turns `cat -n foo.txt` into `bat -n`); prefer `$$$ARGS` to carry the whole argument list. A `$VAR` in the replacement that the pattern never captured stays literal, so a shell variable like `$HOME` in the new command survives untouched.


#### Example

``` python
>>> rewrite_command("cat $$$ARGS", "bat $$$ARGS", note="bat renders with syntax highlighting")
>>> rewrite_command(r"^cat\s+(\S+)$", r"ccx read \1 --full", note="Rewrote cat to ccx read")
>>> rewrite_command(only_if=[Command("git push")], to=lambda e: None, block="Pushing is disabled")
```


## rewrite_code()


Register a `PreToolUse` hook that structurally rewrites edited code before it is written.


Usage

``` python
rewrite_code(
    pattern,
    replace,
    *,
    lang=None,
    only_if=(),
    skip_if=(),
    note=None,
    project_only=True,
    tests=None
)
```


The code counterpart to \[[rewrite_command](primitives.md#captain_hook.rewrite_command)\]\[captain_hook.rewrite_command\]: every ast-grep `pattern` match in the edit's new content is rewritten to `replace`, an ast-grep `$VAR` / `$$$VAR` fix template. It applies across Edit, Write, MultiEdit, and NotebookEdit, and is idempotent -- when nothing matches, the tool passes through untouched. `note` surfaces as `additionalContext` so the model sees that its input was rewritten.


#### Parameters


`pattern: str`  
The ast-grep pattern to match, e.g. `"os.system($CMD)"`.

`replace: str`  
The ast-grep fix template, e.g. `"subprocess.run([$CMD], check=True)"`.

`lang: str | None = None`  
ast-grep language id (`"py"`, `"ts"`, `"tsx"`, `"js"`, `"jsx"`, `"go"`, `"rs"`, `"java"`, `"bash"`); inferred from the edited file's extension when omitted. Pass it explicitly for an extension that carries no language, like a notebook's `.ipynb`.

`only_if: Sequence[TCondition] = ()`    
Extra conditions ANDed onto the built-in editing-tool guard.

`skip_if: Sequence[TCondition] = ()`    
Conditions that skip the rewrite when any matches (e.g. `[TestFile()]`).

`note: str | None = None`  
Advisory context surfaced alongside the rewrite.

`project_only: bool = ``True`  
Only rewrite files inside the repository root (default `True`).

`tests: InlineTests | None = None`  
Inline tests for the registered hook.


#### Example

``` python
>>> rewrite_code("os.system($CMD)", "subprocess.run([$CMD], check=True)", note="Use subprocess.run")
```


## llm_gate()


Register an LLM-powered blocking gate.


Usage

``` python
llm_gate(
    prompt,
    *,
    message,
    response_model=GateVerdict,
    verdict=lambda r: r.block,
    label=None,
    signals=None,
    when=None,
    contexts=(),
    only_if=(),
    skip_if=(),
    events=None,
    max_fires=DEFAULT_FIRES,
    tests=None,
    max_context=2000,
    specialty="review",
    model="small",
    agent=True,
    transcript=True,
    diff=False
)
```


`message` may be a literal string, a `{field}` template with the verdict model's fields splatted in (same placeholder rules as `~captain_hook.Prompt.from_template()`: only `{identifier}` substitutes, every other brace stays literal), or a callable taking the verdict.

Defaults are tuned for the common case: `agent=True` and `transcript=True` so the gate has tool access and a recent transcript window (the path lets the agent read full history). Pass `diff=True` to attach a compact working-tree diff as a `<diff>` block, or `agent=False, transcript=False` for cheap, stateless yes/no checks. An empty diff (or no repo) skips the LLM call entirely, consuming no fire.

`contexts` attaches declarative evidence blocks (`~captain_hook.contexts.PromptContext`), each rendered as an XML block after `<context>` in array order; a `required` context with empty content skips the LLM call entirely, consuming no fire. Passing your own `contexts` with no `signals`/`when` suppresses the implicit transcript `<context>` block -- you own context assembly. The ambient defaults (`~captain_hook.contexts.BeforeEdit`/`~captain_hook.contexts.AfterEdit`) attach to every gate without suppressing it, carrying the pending edit's before/after text on edit-shaped events and nothing elsewhere. A Write's pre-image is only knowable at `PreToolUse`, so contexts reading it over Writes ([Introduced](prompt-contexts.md#captain_hook.Introduced), `BeforeEdit(required=True)`) need `events=Event.PreToolUse`.


#### Parameters


`label: str | None = None`  
Stable identity for this gate. When set, the hook name derives from `label` instead of the prompt hash, so review verdicts and fire state survive prompt edits; two registrations sharing a `label` within a module resolve to the same hook name. Uniqueness within the module is the author's responsibility. Omit it to derive the name from the prompt (the name then shifts whenever the prompt text changes).


#### Example

``` python
>>> 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_gate("Does the new code hardcode a secret?",
...          message=lambda r: f"Secret detected: {r.reasoning}",
...          contexts=[Introduced(pattern='os.environ[$KEY] = $VALUE')],
...          events=Event.PreToolUse, only_if=[Tool("Edit", "Write", "MultiEdit")])
```


## llm_nudge()


Register an LLM-powered advisory nudge.


Usage

``` python
llm_nudge(
    prompt,
    *,
    message,
    response_model=NudgeVerdict,
    verdict=lambda r: r.fire,
    label=None,
    signals=None,
    when=None,
    contexts=(),
    only_if=(),
    skip_if=(),
    events=None,
    max_fires=DEFAULT_FIRES,
    tests=None,
    async_=False,
    max_context=2000,
    specialty="review",
    model="small",
    agent=True,
    transcript=True,
    diff=False
)
```


`message` may be a literal string, a `{field}` template with the verdict model's fields splatted in (same placeholder rules as `~captain_hook.Prompt.from_template()`: only `{identifier}` substitutes, every other brace stays literal), or a callable taking the verdict.

Defaults are tuned for the common case: `agent=True` and `transcript=True` so the nudge has tool access and a recent transcript window (the path lets the agent read full history). Pass `diff=True` to attach a compact working-tree diff as a `<diff>` block, or `agent=False, transcript=False` for cheap, stateless yes/no checks. An empty diff (or no repo) skips the LLM call entirely, consuming no fire.

`contexts` attaches declarative evidence blocks (`~captain_hook.contexts.PromptContext`), each rendered as an XML block after `<context>` in array order; a `required` context with empty content skips the LLM call entirely, consuming no fire. Passing your own `contexts` with no `signals`/`when` suppresses the implicit transcript `<context>` block -- you own context assembly. The ambient defaults (`~captain_hook.contexts.BeforeEdit`/`~captain_hook.contexts.AfterEdit`) attach to every nudge without suppressing it, carrying the pending edit's before/after text on edit-shaped events and nothing elsewhere. A Write's pre-image is only knowable at `PreToolUse`, so contexts reading it over Writes ([Introduced](prompt-contexts.md#captain_hook.Introduced), `BeforeEdit(required=True)`) need `events=Event.PreToolUse` -- the nudge default of `PostToolUse` leaves them empty on Writes.


#### Parameters


`label: str | None = None`  
Stable identity for this nudge. When set, the hook name derives from `label` instead of the prompt hash, so review verdicts and fire state survive prompt edits; two registrations sharing a `label` within a module resolve to the same hook name. Uniqueness within the module is the author's responsibility. Omit it to derive the name from the prompt (the name then shifts whenever the prompt text changes).


#### Example

``` python
>>> 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_nudge("Does any newly introduced comment narrate the edit itself?",
...           message="Tombstone comment: {reasoning}",
...           contexts=[Introduced(kind=COMMENT_TYPES)],
...           events=Event.PreToolUse, only_if=[Tool("Edit", "Write", "MultiEdit")])
```


## prompt_check()


Run an LLM check with a formatted prompt and return block/warn/None.


Usage

``` python
prompt_check(
    evt,
    template,
    fmt=None,
    *,
    prefix,
    suffix="",
    timeout=45,
    include_reasoning=True,
    diff=False,
    response_model=PromptCheckVerdict
)
```


## approve()


Register a hook that answers matching tool permissions with *allow*.


Usage

``` python
approve(
    label,
    *,
    events=Event.PreToolUse | Event.PermissionRequest,
    only_if=(),
    skip_if=(),
    tests=None
)
```


By default it registers on `PreToolUse | PermissionRequest`: the `PreToolUse` allow *pre-authorizes* the tool before Claude Code decides whether to prompt, covering paths where the dialog itself never runs hooks -- most importantly an in-process teammate whose dialog is forwarded to the lead and rendered as plain UI (CC \#73176) -- and clearing prompts the dialog stage would otherwise force (e.g. the multi-`cd` "for clarity" ask); the `PermissionRequest` half still answers any dialog that slips through. A `PreToolUse` allow skips the permission prompt but does not bypass permission rules: Claude Code evaluates explicit settings [deny](primitives.md#captain_hook.deny) and `ask` rules regardless of the hook's answer, so a matching deny rule still blocks and a matching ask rule still prompts. Pin `events=Event.PermissionRequest` to answer only dialogs that actually appear.

Fires on every match -- no fire cap. Warning: an unconditioned [approve()](primitives.md#captain_hook.approve) answers **every** call, equivalent to a permanent `--dangerously-skip-permissions` (and on `PreToolUse` it fires on every tool, not just prompting ones); always scope it with `only_if`/`skip_if`.


#### Parameters


`label: str`  
Short name for the hook, used in fire logs and decision records.

`events: `<a href="events-results.html#captain_hook.Event" class="gdls-link gdls-code"><code>Event</code></a>` = Event.PreToolUse | Event.PermissionRequest`    
Which event(s) to answer on; defaults to `PreToolUse | PermissionRequest`.

`only_if: Sequence[TCondition] = ()`    
Conditions that must all match for the approval to fire.

`skip_if: Sequence[TCondition] = ()`    
Conditions that veto the approval (the dialog shows normally).

`tests: InlineTests | None = None`  
Inline tests run by `capt-hook test`.


#### Example

``` python
>>> approve(
...     "teammate bash under skip-permissions",
...     only_if=[Tool("Bash"), FromSubagent(), SkipPermissions()],
...     skip_if=[ToolInput("command", r"\brm\b")],
... )
```


## deny()


Register a hook that answers matching tool permissions with *deny*.


Usage

``` python
deny(
    reason,
    *,
    events=Event.PreToolUse | Event.PermissionRequest,
    only_if=(),
    skip_if=(),
    tests=None
)
```


By default it registers on `PreToolUse | PermissionRequest`, so the deny lands even on paths where the dialog runs no hooks (a teammate dialog forwarded to the lead, CC \#73176). At `PreToolUse` a deny is a real guard, not just a dialog answer: it blocks calls that would otherwise proceed without prompting at all (auto-allowed by settings, or a session in bypass mode). Avoid pinning a deny to `events=Event.PermissionRequest` where approves may fire at `PreToolUse` -- a `PreToolUse` allow pre-empts the dialog stage (absent an explicit settings ask rule), so a dialog-only deny never gets a dialog to answer.

Fires on every match -- no fire cap -- returning a deny whose `message` is *reason*.

#### Warning

an unconditioned [deny()](primitives.md#captain_hook.deny) rejects **every** call, bricking every matching


#### Parameters


`reason: str`  
The denial message shown to the user, also the hook's label.

`events: `<a href="events-results.html#captain_hook.Event" class="gdls-link gdls-code"><code>Event</code></a>` = Event.PreToolUse | Event.PermissionRequest`    
Which event(s) to answer on; defaults to `PreToolUse | PermissionRequest`.

`only_if: Sequence[TCondition] = ()`    
Conditions that must all match for the denial to fire.

`skip_if: Sequence[TCondition] = ()`    
Conditions that veto the denial (the dialog shows normally).

`tests: InlineTests | None = None`  
Inline tests run by `capt-hook test`.


#### Example

``` python
>>> deny("No force pushes from subagents", only_if=[FromSubagent(), Command(r"push\s+--force")])
```


## llm_approve()


Register an LLM safety judge that auto-approves permission dialogs it deems safe.


Usage

``` python
llm_approve(
    label,
    *,
    events=Event.PermissionRequest,
    rubric=None,
    only_if=(),
    skip_if=(),
    model="small",
    tests=None
)
```


Replicates Claude Code's auto-mode classifier (which is not programmatically invocable): the judge's rubric is seeded from `claude auto-mode defaults` (cached globally, keyed by `claude --version`; a static built-in rubric stands in when the binary or verb is unavailable), and *rubric* appends to that base. A `safe` verdict answers the dialog with *allow*; an unsafe verdict or any LLM failure returns `None` so the real dialog shows -- it never auto-denies. Adds an LLM round-trip to every matching ask, so scope it tightly with `only_if`/`skip_if`.

Unlike [approve()](primitives.md#captain_hook.approve)/[deny()](primitives.md#captain_hook.deny), the default stays `PermissionRequest`-only: a dialog is a rare event, but `PreToolUse` fires on every matching tool call, which would put the judge's LLM round-trip on the hot path. Pass `events=Event.PreToolUse | Event.PermissionRequest` to pre-authorize anyway -- that also covers forwarded teammate dialogs, which run no `PermissionRequest` hooks (CC \#73176) -- and mind the cost.

Under `capt-hook test`, the stubbed LLM always returns `safe=False`, so inline tests deterministically expect [Ask()](testing.md#captain_hook.Ask).


#### Parameters


`label: str`  
Short name for the hook, used in fire logs and decision records.

`rubric: str | None = None`  
Extra rubric text appended to the seeded base.

`only_if: Sequence[TCondition] = ()`    
Conditions that must all match before the judge is consulted.

`skip_if: Sequence[TCondition] = ()`    
Conditions that veto the judge (the dialog shows normally).

`model: TModel = ``"small"`  
spawnllm model tier for the judge call.

`tests: InlineTests | None = None`  
Inline tests run by `capt-hook test`.


#### Example

``` python
>>> llm_approve("safe teammate commands", only_if=[Tool("Bash"), FromSubagent()])
```


## style.styleguide()


Register one change-scoped hook applying the given style rules to Python edits and writes.


Usage

``` python
style.styleguide(
    *rules, block=False, only_if=(), skip_if=(), events=None, max_shown=5
)
```


Each rule is a \[[StyleRule](primitives.md#captain_hook.style.StyleRule)\]\[captain_hook.style.StyleRule\] (or \[[StyleDiffRule](primitives.md#captain_hook.style.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](prompt-contexts.md#captain_hook.Excerpts.block) is set). Call again with different `only_if` / `skip_if` / `events` / [block](prompt-contexts.md#captain_hook.Excerpts.block) to register a separately scoped hook.


#### Parameters


`rules: type[`<a href="primitives.html#captain_hook.style.StyleRule" class="gdls-link gdls-code"><code>StyleRule</code></a>`] = ()`  
[StyleRule](primitives.md#captain_hook.style.StyleRule) / [StyleDiffRule](primitives.md#captain_hook.style.StyleDiffRule) subclasses to apply.

`block: bool = ``False`  
Block the tool call instead of warning.

`only_if: Sequence[TCondition] = ()`    
Extra conditions ANDed onto the built-in `Edit|Write` + `*.py` guards.

`skip_if: Sequence[TCondition] = ()`    
Extra conditions ORed onto the built-in test-file skip.

`events: `<a href="events-results.html#captain_hook.Event" class="gdls-link gdls-code"><code>Event</code></a>` | None = None`  
Override the default `PostToolUse` targeting.

`max_shown: int = ``5`  
Maximum violations shown per rule.


#### Example

``` python
>>> styleguide(NoPrint, NoBareExcept)
>>> styleguide(NoSqlInjection, block=True, only_if=[FilePath("api/**/*.py")])
```


## style.matchers


[style.matchers](primitives.md#captain_hook.style.matchers)


Usage


Composable AST matchers for style rules -- import this module as `M`.


Every factory ([kind](primitives.md#captain_hook.style.matchers.kind), [calls](files-commands.md#captain_hook.Cmd.calls), [under](primitives.md#captain_hook.style.matchers.under), …) and preset (`imports`, `control_flow`, …) is a module-level \[[Matcher](primitives.md#captain_hook.style.matchers.Matcher)\]\[captain_hook.style.matchers.Matcher\] builder or constant; author a rule by combining them with `&`, `|`, and `~`.


#### Example

``` python
>>> from captain_hook.style import matchers as M
>>> M.imports & M.child_of(M.control_flow) & ~M.under(M.type_checking)
```


#### Classes

| Name | Description |
|----|----|
| [Matcher](#captain_hook.style.matchers.Matcher) | A composable, immutable AST matcher: a node predicate that is also a tree selector. |


##### Matcher


A composable, immutable AST matcher: a node predicate that is also a tree selector.


Usage

``` python
Matcher(test, label="matcher", structural=False)
```


A [Matcher](primitives.md#captain_hook.style.matchers.Matcher) wraps a single `(node, parents) -> bool` test. *Node-local* matchers (e.g. \[[calls](files-commands.md#captain_hook.Cmd.calls)\]\[captain_hook.style.matchers.calls\]) ignore `parents`; *structural* matchers (e.g. \[[under](primitives.md#captain_hook.style.matchers.under)\]\[captain_hook.style.matchers.under\]) consult it. Compose with the boolean algebra `&` (intersection), `|` (union), and `~` (negation), refine with \[[where](primitives.md#captain_hook.style.matchers.Matcher.where)\]\[captain_hook.style.matchers.Matcher.where\], and finish with a terminal -- \[[over](primitives.md#captain_hook.style.matchers.Matcher.over)\]\[captain_hook.style.matchers.Matcher.over\], \[[violations](primitives.md#captain_hook.style.matchers.Matcher.violations)\]\[captain_hook.style.matchers.Matcher.violations\], or \[[exists](primitives.md#captain_hook.style.matchers.Matcher.exists)\]\[captain_hook.style.matchers.Matcher.exists\].

The module-level vocabulary (`M.imports`, `M.control_flow`, …) and factories (`M.kind`, `M.calls`, …) are the whole surface -- author a new rule by combining them, not by reaching for a framework helper.


##### Parameter Attributes


`test: Predicate`  

`label: str = ``"matcher"`  

`structural: bool = ``False`  


##### Example

``` python
>>> M.imports & M.child_of(M.control_flow) & ~M.under(M.type_checking)
```


##### Methods

| Name | Description |
|----|----|
| [diff()](#captain_hook.style.matchers.Matcher.diff) | Yield violations for matches in `post` whose `key` was absent from `pre`. |
| [exists()](#captain_hook.style.matchers.Matcher.exists) | Return whether any node in `tree` matches. |
| [matches()](#captain_hook.style.matchers.Matcher.matches) | Test a single `node` (node-local matchers only; raises for structural matchers). |
| [over()](#captain_hook.style.matchers.Matcher.over) | Yield every node in `tree` that matches. |
| [violations()](#captain_hook.style.matchers.Matcher.violations) | Yield a \[[Violation](primitives.md#captain_hook.style.Violation)\]\[captain_hook.style.Violation\] for each match, located at its line. |
| [where()](#captain_hook.style.matchers.Matcher.where) | Refine with a node-local one-off `predicate` -- the escape hatch for bespoke conditions. |


###### diff()


Yield violations for matches in `post` whose `key` was absent from `pre`.


Usage

``` python
diff(pre, post, key=ast.unparse, label=None)
```


###### exists()


Return whether any node in `tree` matches.


Usage

``` python
exists(tree)
```


###### matches()


Test a single `node` (node-local matchers only; raises for structural matchers).


Usage

``` python
matches(node)
```


###### over()


Yield every node in `tree` that matches.


Usage

``` python
over(tree)
```


###### violations()


Yield a \[[Violation](primitives.md#captain_hook.style.Violation)\]\[captain_hook.style.Violation\] for each match, located at its line.


Usage

``` python
violations(tree, label=None)
```


###### where()


Refine with a node-local one-off `predicate` -- the escape hatch for bespoke conditions.


Usage

``` python
where(predicate)
```


#### Functions

| Name | Description |
|----|----|
| [annotated()](#captain_hook.style.matchers.annotated) | Match an annotation owner (annotated variable, parameter, or function return). |
| [calls()](#captain_hook.style.matchers.calls) | Match a call to the bare-name function [name](files-commands.md#captain_hook.Call.name) (e.g. `calls("zip")`). |
| [child_of()](#captain_hook.style.matchers.child_of) | Match a node whose immediate parent matches `other`. |
| [following()](#captain_hook.style.matchers.following) | Match a body statement that follows the first sibling matching `boundary`. |
| [kind()](#captain_hook.style.matchers.kind) | Match any of the given AST node `types` -- the primitive for a category not shipped. |
| [kwarg()](#captain_hook.style.matchers.kwarg) | Match a call that passes the keyword argument [name](files-commands.md#captain_hook.Call.name) (combine with [calls](files-commands.md#captain_hook.Cmd.calls)). |
| [named()](#captain_hook.style.matchers.named) | Match a class/function/assignment/parameter whose bound name matches `pattern` (`re.search`). |
| [ref()](#captain_hook.style.matchers.ref) | Match the bare name reference [name](files-commands.md#captain_hook.Call.name) (e.g. `ref("Any")` for `x: Any`). |
| [under()](#captain_hook.style.matchers.under) | Match a node with any ancestor matching `other` (negate with `~` for "not inside"). |


##### annotated()


Match an annotation owner (annotated variable, parameter, or function return).


Usage

``` python
annotated(inner=None)
```


Excludes `*args`/`**kwargs`. When `inner` is given, the owner's annotation expression must also match it (e.g. `annotated(ref("Any"))`).


##### calls()


Match a call to the bare-name function [name](files-commands.md#captain_hook.Call.name) (e.g. `calls("zip")`).


Usage

``` python
calls(name)
```


##### child_of()


Match a node whose immediate parent matches `other`.


Usage

``` python
child_of(other)
```


##### following()


Match a body statement that follows the first sibling matching `boundary`.


Usage

``` python
following(boundary)
```


##### kind()


Match any of the given AST node `types` -- the primitive for a category not shipped.


Usage

``` python
kind(*types, label=None)
```


##### kwarg()


Match a call that passes the keyword argument [name](files-commands.md#captain_hook.Call.name) (combine with [calls](files-commands.md#captain_hook.Cmd.calls)).


Usage

``` python
kwarg(name)
```


##### named()


Match a class/function/assignment/parameter whose bound name matches `pattern` (`re.search`).


Usage

``` python
named(pattern)
```


##### ref()


Match the bare name reference [name](files-commands.md#captain_hook.Call.name) (e.g. `ref("Any")` for `x: Any`).


Usage

``` python
ref(name)
```


##### under()


Match a node with any ancestor matching `other` (negate with `~` for "not inside").


Usage

``` python
under(other)
```


## style.StyleRule


Base class for a single-tree AST style rule applied to Python edits and writes.


Usage

``` python
style.StyleRule()
```


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](primitives.md#captain_hook.style.matchers.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)
```


## style.StyleDiffRule


Base class for a diff rule: flags constructs newly introduced by the change.


Usage

``` python
style.StyleDiffRule()
```


Like \[[StyleRule](primitives.md#captain_hook.style.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))
```


## style.ast_grep_rule()


Build a \[[StyleRule](primitives.md#captain_hook.style.StyleRule)\]\[captain_hook.style.StyleRule\] from an inline ast-grep pattern.


Usage

``` python
style.ast_grep_rule(
    name, *, pattern, message, lang="py", label=None, tests=None
)
```


`pattern` is an ast-grep pattern string with metavariables (`print($$$)`). `message` is the rule's message (`{violations}` is substituted at fire time); `label` names each match, defaulting to the matched code. Register the result with \[[styleguide](primitives.md#captain_hook.style.styleguide)\]\[captain_hook.style.styleguide\].


#### Example

``` python
>>> NoPrint = ast_grep_rule("NoPrint", pattern="print($$$)", message="No print(): {violations}")
>>> styleguide(NoPrint)
```


## style.ast_grep_diff_rule()


Like \[[ast_grep_rule](primitives.md#captain_hook.style.ast_grep_rule)\]\[captain_hook.style.ast_grep_rule\], but flags only matches the edit newly introduces.


Usage

``` python
style.ast_grep_diff_rule(
    name, *, pattern, message, lang="py", label=None, tests=None
)
```


#### Example

``` python
>>> NoNewWildcard = ast_grep_diff_rule("NoNewWildcard", pattern="from $M import *", message="No `import *`")
```


## style.AstGrepStyleRule


A \[[StyleRule](primitives.md#captain_hook.style.StyleRule)\]\[captain_hook.style.StyleRule\] matched by an ast-grep pattern over source text.


Usage

``` python
style.AstGrepStyleRule()
```


Built by \[[ast_grep_rule](primitives.md#captain_hook.style.ast_grep_rule)\]\[captain_hook.style.ast_grep_rule\]; the engine runs `pattern` against the edited source in `lang` and scopes each match to the changed lines.


## style.AstGrepStyleDiffRule


An \[[AstGrepStyleRule](primitives.md#captain_hook.style.AstGrepStyleRule)\]\[captain_hook.style.AstGrepStyleRule\] that flags only constructs the edit introduces.


Usage

``` python
style.AstGrepStyleDiffRule()
```


## style.Violation


A single style violation, located by line so the runner can scope it to the edit.


Usage

``` python
style.Violation(line, label)
```


#### Attributes


`line: int`  
1-based line number of the offending construct in the post-edit file.

`label: str`  
Short human-readable description, rendered as `"{label} (line {line})"`.


## style.Change


The pre- and post-edit state of a file, passed to every style rule's `check`.


Usage

``` python
style.Change(source, pre, path)
```


#### Attributes


`source: str`  
The post-edit source text.

`pre: str`  
The pre-edit source text (`""` when there is nothing to diff against).

`path: str`  
The post-edit file's path as a string, so rules can inspect the filename.

`tree: ast.Module`  
The parsed post-edit module (an empty module when the source doesn't parse).

`pre_tree: ast.Module`  
The parsed pre-edit module (parsed lazily, only when a diff rule reads it).


## GateVerdict


LLM response model for [llm_gate](primitives.md#captain_hook.llm_gate). The LLM sets `block=True` to deny.


Usage

``` python
GateVerdict()
```


## NudgeVerdict


LLM response model for [llm_nudge](primitives.md#captain_hook.llm_nudge). The LLM sets `fire=True` to trigger the nudge.


Usage

``` python
NudgeVerdict()
```


## PromptCheckVerdict


LLM response model for [prompt_check](primitives.md#captain_hook.prompt_check). Action is `"ok"`, `"warning"`, or `"block"`.


Usage

``` python
PromptCheckVerdict()
```


## SafetyVerdict


LLM response model for [llm_approve](primitives.md#captain_hook.llm_approve). The LLM sets `safe=True` to auto-approve.


Usage

``` python
SafetyVerdict()
```
