# Cheatsheet

Every hook you reach for, on one page. Copy a line, swap the strings, drop it in `.claude/hooks/`. Pick the action first, then narrow with conditions. Full signatures live in the [primitives reference](../../docs/reference/primitives.md) and the [conditions reference](../../docs/reference/conditions.md).

> **Tip: Run any snippet**
>
> Save it to `.claude/hooks/x.py` and run `uvx capt-hook --hooks .claude/hooks test` to exercise its inline tests.


# 🛑 Block before it happens

**block_command** -- stop a dangerous Bash command before it runs

``` python
block_command(["git", "stash"], reason="Use the team VCS workflow")
```

**gate** -- refuse to let the agent stop until a condition holds

``` python
gate("Run the tests before finishing", skip_if=[RanCommand("pytest"), RanCommand("uv", "run", "pytest")])
```

**hook(…, block=True)** -- a hand-rolled always-on guard when no primitive fits

``` python
hook(Event.PreToolUse, only_if=[Tool("Bash"), Command(r"curl.*\|.*sh")],
     message="Don't pipe curl into a shell", block=True)
```


# ✏️ Rewrite in place

**rewrite_command** -- swap a command for a safer form before it runs

``` python
rewrite_command(r"^pip install ", replace="uv pip install ")
```

**rewrite_command_occurrences** -- rewrite each occurrence of one program inside a compound Bash line -- [worked example](../../docs/examples/per-occurrence-rewrite.md)

**rewrite_code** -- structurally rewrite edited code before it is written, ast-grep pattern to fix template

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

**set_tool_input** -- fill a missing tool-input field, never clobbering an explicit one

``` python
set_tool_input("model", "sonnet", tool="Agent|Task", note="defaulted the subagent model")
```


# 🧭 Walk the command

**evt.command** -- the parsed, walkable Bash line: match structure, judge blast radius, rewrite calls -- [the how-to](../../docs/guide/commands.md)

``` python
for call in evt.command.calls("rm"):
    return call.sub("rm", "trash", args=call.targets)
```


# 🔑 Answer permission dialogs

**approve** -- auto-answer a permission dialog for a scoped, consented case

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


# 💬 Nudge in the moment

**nudge** -- advisory message on an edit or stop, no block

``` python
nudge("Prefer the project logger over print()", only_if=[Tool("Edit")])
```

**nudge(signals=…)** -- fire only when the transcript crosses a score

``` python
nudge("You keep retrying -- stop and read the error", signals=RETRIES)
```

**warn_command** -- flag a smell after a command has run

``` python
warn_command(r"rm -rf", message="Double-check that path next time")
```


# 🔍 Lint the code

**lint** -- run a string or AST check on every Python edit

``` python
lint(find_bare_excepts, message="Bare except swallows errors: {violations}")
```

**diff_lint** -- flag only what this edit newly introduced

``` python
diff_lint(widened_to_any, message="New Any annotation: {violations}")
```

**styleguide** -- run composable matcher rules on the lines you changed

``` python
styleguide(NoPrint, NoBareExcept)
```


# 🤖 Ask a model

**llm_gate** -- block on a model's judgment of intent or quality

``` python
llm_gate("Is the agent making excuses instead of finishing?",
         message=lambda r: r.reasoning, max_fires=1)
```

**llm_nudge** -- warn on a model's judgment, without blocking

``` python
llm_nudge("Does this edit assert behavior, or only that code exists?",
          message=lambda r: r.reasoning, only_if=[Tool("Edit")])
```

**prompt_check** -- call the model from inside your own handler

``` python
prompt_check(evt, Prompt.from_template("Review {code}", code=evt.content),
             prefix="Test quality")
```

**evt.llm** -- a typed answer from the model, from inside any handler

``` python
verdict = evt.llm("Rate the risk of this action.", RiskVerdict)
```


# 🎯 Decide when (conditions)

Stack conditions in `only_if` (all must match) or `skip_if` (any match skips).

**Tool** -- the tool name matches

``` python
Tool("Edit|Write")
```

**FilePath** -- the event's file path matches a glob

``` python
FilePath("src/**/*.py")
```

**Command** / **Content** -- the Bash command, or the written content, matches a regex

``` python
Command(r"git\s+push\s+--force")
Content(r"\bprint\(")
```

**Runs** -- a parsed command's argv starts with these tokens

``` python
Runs("git", "stash")
```

**TestFile** / **SourceEdits** -- the edit hits a test, or a source file in a language

``` python
SourceEdits(lang="py", include_tests=False)
```

**RanCommand** / **ReadFile** / **TouchedFile** -- something already happened this session

``` python
RanCommand("uv", "run", "pytest")
```

**UsedSkill** / **UsedTool** / **InPlanMode** / **Waiting** -- session mode and history

``` python
only_if=[InPlanMode()]
```


# 🪜 Multi-step gate

**workflow** -- block a stop until an ordered checklist passes

``` python
workflow(label="QA-GATE", marker="QA COMPLETE", steps=[...], artifacts=[...])
```

**Step** -- one checkpoint: a `check` predicate plus stop and next messages

``` python
Step(name="run tests", check=text_matches(r"pytest.*passed"),
     message="Tests have not run. Run: pytest -x")
```

**Artifact** -- a file that must exist and parse against a Pydantic model

``` python
Artifact(path="reports/lint.json", model=LintReport)
```

**text_matches(pattern)** -- a prose-regex check for `Step.check` (scans narration, not commands)


# 💾 Session state

**evt.ctx.s\[Model\]** -- a typed slot keyed by a Pydantic model

``` python
state = evt.ctx.s[EditCounter].get() or EditCounter()
evt.ctx.s[EditCounter].set(state)
```

**<span class="citation" cites="workflow_state">@workflow_state</span>("name")** -- bundle one workflow's reads and writes

``` python
@workflow_state("review")
class ReviewState(WorkflowState):
    ran_tests: bool = False
```

**.load(evt)** / **.save(evt)** / **.reset(evt)** -- read-default, write, delete; **.mutate(evt)** -- locked read-modify-write for racing writers **<span class="citation" cites="session_state">@session_state</span>** -- register a plain model for introspection


# 📈 Signal scoring

**Signal** -- a regex worth `weight` points when it matches

``` python
Signal(pattern=r"\bretry\b", weight=2)
```

**Signals** -- a bundle that fires when a single message in the last `window` events scores `threshold`; `scope="window"` pools signals across the window, `origin="any"` scores user text too (the default scores only the agent's prose), `window="turn"` scores the whole current turn

``` python
Signals([Signal(pattern=r"retry", weight=2), Signal(pattern=r"again")], threshold=3, window=15)
```


# 🧱 AST matchers

For [style rules](../../docs/guide/primitives.md#style-rules). Compose with `&`, `|`, `~`, refine with `.where(...)`, run with `.over(tree)` or `.violations(tree)`. Import as `from captain_hook.style import matchers as M`.

**Node types** -- `M.cls` / `M.func` / `M.definition` · `M.call` · `M.imports` · `M.assignment` · `M.control_flow` · `M.type_checking` · `M.kind(*types)`

**Calls & references** -- `M.calls(name)` (bare-name call) · `M.kwarg(name)` · `M.ref(name)`

**Names** -- `M.named(pattern)` · `M.private` · `M.dunder` · `M.constant`

**Annotations** -- `M.annotated(inner=None)` · `M.forward_ref`

**Position** -- `M.under(other)` (any ancestor) · `M.child_of(other)` (immediate parent) · `M.following(boundary)`

**Combine & run** -- `a & b` · `a | b` · `~a` · `a.where(pred)` · `m.over(tree)` · `m.violations(tree)` · `m.diff(pre, post)` · `m.exists(tree)`

The [matchers reference](../../docs/reference/matchers.md) lists every matcher with signatures -- and the canonical worked rule (`NoNestedImports`).


# 🪝 Register by hand

**hook** / **on** -- when no primitive fits, target an event directly

``` python
@on(Event.PostToolUse, only_if=[Tool("Edit")])
def my_check(evt): ...
```


# See also

- How-tos: [Writing hooks](../../docs/guide/primitives.md) · [Inspect and rewrite commands](../../docs/guide/commands.md) · [LLM hooks & signals](../../docs/guide/llm-hooks.md) · [State & workflows](../../docs/guide/state.md)
- Reference: [Primitives & verdicts](../../docs/reference/primitives.md) · [Conditions](../../docs/reference/conditions.md) · [Events](../../docs/reference/events.md) · [Matchers](../../docs/reference/matchers.md)
