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 one of:
:ok— continue with no side effects.{:deny, reason}— only meaningful fromPreToolUse/PermissionRequest; deny the tool call.{:halt, reason}— stop the loop immediately.{:inject, message_or_messages}— append the given message (or list of messages) to the conversation. Useful forexperimental.chat.system.transform-style context injection.{:transform, new_prompt}— only valid fromUserPromptSubmit; rewrites the incoming user prompt before it enters the loop.{:augment, text}— only valid fromPostToolUse; appendstextto the tool result content visible to the model on the next turn. Multiple augments from different hooks are joined with"\n".
Hooks are matched by :matcher (regex run against tool_name); a nil
matcher or a missing :matcher key fires for every tool. Lifecycle-only
events are passed as plain function lists, not wrapped in matcher maps.
Catalog of supported events
- Session:
:SessionStart,:SessionEnd - Per-turn:
:UserPromptSubmit,:ChatParams,:Stop,:StopFailure - Per-tool:
:PreToolUse,:PostToolUse,:PostToolUseFailure,:PermissionRequest,:PermissionDenied - Subagent:
:SubagentStart,:SubagentStop - Compaction:
:PreCompact,:PreCompactStage,:PostCompact - Notification:
:Notification
Summary
Functions
Catalog of every hook event ex_athena fires today. Useful for hosts that want to enumerate or validate user-supplied hook tables.
Fire lifecycle hooks that aren't scoped to a tool (Stop, SessionStart,
etc.). Backward-compatible: returns :ok | {:halt, reason} like before.
Use run_lifecycle_with_outputs/3 for events that may inject messages
or transform prompts.
Like run_lifecycle/3 but returns a structured outputs map so callers
can act on {:inject, msg} and {:transform, prompt} returns. Used
by the kernel to thread UserPromptSubmit transforms and
:inject-driven message injection across hook events.
Fire PostToolUse hooks matching tool_name.
Fire PreToolUse hooks matching tool_name.
Types
@type lifecycle_outputs() :: %{ halt: nil | {:halt, term()}, injects: [ExAthena.Messages.Message.t()], transform: nil | String.t() }
@type t() :: %{optional(atom()) => [matcher_group()] | [hook_fun()]}
Functions
@spec events() :: [atom()]
Catalog of every hook event ex_athena fires today. Useful for hosts that want to enumerate or validate user-supplied hook tables.
Fire lifecycle hooks that aren't scoped to a tool (Stop, SessionStart,
etc.). Backward-compatible: returns :ok | {:halt, reason} like before.
Use run_lifecycle_with_outputs/3 for events that may inject messages
or transform prompts.
@spec run_lifecycle_with_outputs(t(), atom(), map()) :: lifecycle_outputs()
Like run_lifecycle/3 but returns a structured outputs map so callers
can act on {:inject, msg} and {:transform, prompt} returns. Used
by the kernel to thread UserPromptSubmit transforms and
:inject-driven message injection across hook events.
Returns %{halt:, injects:, transform:}. halt short-circuits the
remaining callbacks (denies / halts always win); injects accumulates
in order; transform is last-write-wins.
@spec run_post_tool_use(t(), String.t(), map(), String.t() | nil) :: :ok | {:halt, term()} | {:augment, String.t()}
Fire PostToolUse hooks matching tool_name.
Returns:
:ok— all hooks returned:ok(or ignored returns).{:halt, term()}— a hook requested a hard stop; no further hooks run.{:augment, String.t()}— one or more hooks returned{:augment, text}; multiple augments are joined with"\n".:halttakes priority over any accumulated augment text.
@spec run_pre_tool_use(t(), String.t(), map(), String.t() | nil) :: :ok | {:deny, term()} | {:halt, term()}
Fire PreToolUse hooks matching tool_name.