Hooks
Hooks observe the agent lifecycle. They don't gate execution — for that use permissions — but they can record state, write logs, or trigger side effects in response to specific events.
The shape
from mantis_agent import HookMatcher, ClaudeAgentOptions
async def log_tool(hook_input, hook_ctx):
print(f"[{hook_input.event}] {hook_input.tool_name}")
return None # Return None to do nothing; return HookJSONOutput to
# mutate the in-flight payload.
options = ClaudeAgentOptions(
model="qwen2.5:7b",
tools=[get_weather],
hooks=[
HookMatcher(event="PreToolUse", handler=log_tool),
HookMatcher(event="PostToolUse", handler=log_tool),
],
)A HookMatcher pairs an event name with an async handler. You can attach
multiple matchers for the same event — they all run, in declaration order.
Events
The 28 hook events the runtime emits:
Session lifecycle
SessionStart— agent is starting a new session.SessionEnd— final result message has been yielded.SessionResume— restored from a checkpoint.
Per-turn
Stop— agent loop is about to exit (e.g.stop_reason='end_turn').PreCompact/PostCompact— about to / just finished compacting.
Per-message
PreModelCall/PostModelCall— wrapping an outbound HTTP call.
Per-tool
PreToolUse— model emitted atool_use, before the executor runs it.PostToolUse— tool returned, before the result is threaded back.ToolUseDenied—can_use_toolreturned a Deny.
Per-block
PreContentBlockStart/PostContentBlockStopTextBlockComplete,ThinkingBlockComplete,ToolUseBlockComplete
Per-MCP server
McpServerStart,McpServerStop,McpServerError
Memory / settings
PreSettingsLoad/PostSettingsLoadPreMemoryLoad/PostMemoryLoadMemoryEntrySaved
HookInput
The first argument to every handler:
@dataclass
class HookInput:
event: str # event name
session_id: str
turn: int
tool_name: str | None
tool_input: dict | None
tool_result: dict | None
payload: dict # event-specific extrasHookJSONOutput
Return None to do nothing. Return a HookJSONOutput to mutate the
in-flight value:
from mantis_agent import HookJSONOutput
async def normalise(hook_input, hook_ctx):
if hook_input.event == "PreToolUse":
return HookJSONOutput(
updated_tool_input={"city": hook_input.tool_input["city"].title()},
)
return NoneAvailable mutations per event:
| Event | Mutable fields |
|---|---|
PreToolUse |
updated_tool_input, block |
PostToolUse |
updated_tool_result, block |
PreModelCall |
extra_headers, block |
PostModelCall |
nothing — observation only |
Stop |
continue_with (force one more turn) |
block=True cancels the in-flight action (denying the tool, dropping the
model call) without an exception.
Plugins ship hooks too
A Plugin bundles hooks alongside tools and a system-prompt addition:
from mantis_agent import Plugin
logging_plugin = Plugin(
tools=[],
system_prompt_addition="",
hooks=[
HookMatcher(event="PreToolUse", handler=log_tool),
HookMatcher(event="PostToolUse", handler=log_tool),
],
)
options = ClaudeAgentOptions(plugins=[logging_plugin])See Plugins.