Lifecycle hooks the loop fires at key transitions.
Shape mirrors Claude Code's SDK so existing hook code ports cleanly:
hooks = %{
PreToolUse: [%{matcher: "Write|Edit", hooks: [&deny_protected_paths/2]}],
PostToolUse: [%{matcher: "Bash", hooks: [&capture_test_output/2]}],
Stop: [&log_stop/2]
}Each hook callback receives (input_map, tool_use_id) and returns either:
:ok/{:allow, []}— continue{:deny, permission_decision_reason: reason}— deny the tool call{:halt, reason}— stop the loop
Hooks are matched by :matcher (regex run against tool_name); a nil
matcher or a missing :matcher key fires for every tool. Lifecycle-only
hooks (Stop, SessionStart, SessionEnd, Notification, PreCompact)
are passed as plain function lists, not wrapped in matcher maps.
Summary
Functions
Fire lifecycle hooks that aren't scoped to a tool (Stop, SessionStart, etc.).
Fire PostToolUse hooks matching tool_name.
Fire PreToolUse hooks matching tool_name.
Types
@type t() :: %{ optional(:PreToolUse) => [matcher_group()], optional(:PostToolUse) => [matcher_group()], optional(:PostToolUseFailure) => [matcher_group()], optional(:Stop) => [hook_fun()], optional(:Notification) => [hook_fun()], optional(:PreCompact) => [hook_fun()], optional(:SessionStart) => [hook_fun()], optional(:SessionEnd) => [hook_fun()] }