Hooks & Events
Events
Hooks are triggered by events — lifecycle moments in Claude Code’s execution. Each event type is a member of the Event flag enum:
| Event | When it fires | Typical use |
|---|---|---|
| PreToolUse | Before a tool runs | Block dangerous commands, enforce policies |
| PostToolUse | After a tool succeeds | Lint output, nudge about conventions |
| PostToolUseFailure | After a tool fails | Suggest debugging steps |
| UserPromptSubmit | User sends a message | Detect multi-request patterns |
| Stop | Agent is about to stop | Gate on test execution, review quality |
| SubagentStop | A subagent finishes | Verify subagent work quality |
| SubagentStart | A subagent launches | Capture initial state |
| Notification | Informational event | Logging, metrics |
| PreCompact | Before context compaction | Preserve critical context |
Events can be combined with |:
from captain_hook import hook, Event
hook(Event.Stop | Event.SubagentStop, message="Run tests first", block=True)Event classes
Each Event flag maps to a strongly-typed event class:
| Flag | Class | Key properties |
|---|---|---|
| PreToolUse | PreToolUseEvent | tool_name, tool_input |
| PostToolUse | PostToolUseEvent | tool_name, tool_input |
| PostToolUseFailure | PostToolUseFailureEvent | tool_name, tool_input, error |
| Stop | StopEvent | — |
| SubagentStop | SubagentStopEvent | agent_type |
| SubagentStart | SubagentStartEvent | agent_type |
| UserPromptSubmit | UserPromptSubmitEvent | prompt |
| Notification | NotificationEvent | — |
| PreCompact | PreCompactEvent | — |
All events provide a ctx property returning a HookContext with access to transcript, settings, session state, CLI helpers, and LLM invocation.
Every event also exposes:
evt.transcript_path—Path | Noneto the active transcript file (from the raw payload)evt.user_prompt— the user’s prompt text (set on UserPromptSubmit, otherwiseNone)evt.stop_hook_active— whether the stop hook is currently active
Registration
There are three ways to register hooks, from simplest to most flexible.
Declarative: hook()
The most common pattern — register a hook with a message and conditions:
from captain_hook import hook, Event, Tool, Command
hook(
Event.PreToolUse,
message="git stash is not allowed; use jj",
block=True,
only_if=[Tool("Bash"), Command(r"git\s+stash")],
)Parameters:
| Parameter | Description |
|---|---|
events |
Which events to match (required) |
| message | Text shown to Claude (required for declarative hooks) |
| block | True to block, False to warn (default: False) |
only_if / skip_if |
Condition lists — see Conditions |
max_fires |
Limit how many times this hook fires per session |
Primitives
Convenience wrappers for common patterns. One function call handles event targeting, conditions, and defaults:
from captain_hook import block_command, nudge, gate, lint, TouchedFile, RanCommand
block_command(["git", "stash"], reason="Use jj instead")
nudge("Remember to run tests", only_if=[TouchedFile("**/*.py")])
gate("Run tests before stopping", skip_if=[RanCommand(r"pytest")])
lint("no_print", r"print\(", message="Use logger instead")See the Primitives Guide for the full list.
Handler: @on()
For hooks needing complex logic, use a handler function:
from captain_hook import on, Event, Tool
@on(Event.PreToolUse, only_if=[Tool("Bash")])
def no_sudo(evt):
cmd = evt.tool_input.get("command", "")
if cmd.startswith("sudo"):
return evt.block(f"sudo is not allowed: {cmd}")The handler receives the event and returns HookResult | None:
| Return | Effect |
|---|---|
evt.block(message) |
Block the action |
evt.warn(message) |
Warn (allow with context) |
evt.allow() |
Explicitly allow |
None |
Skip (no action) |
Start with primitives — they cover most hooks in one line. Use declarative hook() when you need custom condition combinations. Use handlers only when you need runtime logic (conditionals, state access, dynamic messages).
Conditions
Conditions filter when a hook fires. See the dedicated Conditions guide for the full reference, or the Conditions Cheatsheet for a quick-reference card.
from captain_hook import hook, Event, Tool, Command, RanCommand
hook(
Event.PreToolUse,
message="blocked",
block=True,
only_if=[Tool("Bash"), Command(r"rm\s+-rf")], # AND — all must match
skip_if=[RanCommand(r"confirm-delete")], # OR — any skips
)Dispatch pipeline
When Claude Code fires an event, the dispatch pipeline processes it:
- Parse — JSON from stdin is parsed into a typed event object
- Match — each registered hook is checked against
skip_ifthenonly_if - Execute — matching hooks run their handler (or return their message)
- Collect — first block wins; warnings accumulate
- Format — results are formatted as JSON and written to stdout
See How It Works for a detailed step-by-step walkthrough with actual JSON input/output.