ExAthena.Hooks (ExAthena v0.3.0)

Copy Markdown View Source

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

hook_fun()

@type hook_fun() :: (map(), String.t() -> term())

matcher()

@type matcher() :: String.t() | Regex.t() | nil

matcher_group()

@type matcher_group() :: %{matcher: matcher(), hooks: [hook_fun()]}

t()

@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()]
}

Functions

run_lifecycle(hooks, event, payload)

@spec run_lifecycle(t(), atom(), map()) :: :ok | {:halt, term()}

Fire lifecycle hooks that aren't scoped to a tool (Stop, SessionStart, etc.).

run_post_tool_use(hooks, tool_name, result, tool_use_id)

@spec run_post_tool_use(t(), String.t(), map(), String.t() | nil) ::
  :ok | {:halt, term()}

Fire PostToolUse hooks matching tool_name.

run_pre_tool_use(hooks, tool_name, input, tool_use_id)

@spec run_pre_tool_use(t(), String.t(), map(), String.t() | nil) ::
  :ok | {:deny, term()} | {:halt, term()}

Fire PreToolUse hooks matching tool_name.