Hooks

Observe or block tool calls without editing the tool.

A hook observes or blocks a tool call without the tool knowing. Two flavours: Python callbacks, which run in-process, and shell commands, which run as a subprocess.

from agentino.safety.hooks import HookManager

hooks = HookManager()
hooks.register("PostToolUse", matcher={"tool_name": "chat"},
               callback=lambda ctx: audit_db.insert(ctx))

The events

Thirteen, covering a turn from the moment a message arrives to the moment a subagent finishes.

EventWhenCan block
UserPromptSubmitA message arrives, before the model sees ityes
SessionStartA session begins
PreToolUseBefore a tool runsyes
PostToolUseAfter it returns
PostToolUseFailureAfter it raises
PermissionDeniedA permission check blocked a call
PreCompactBefore history is compactedyes
PostCompactAfter compaction
SubagentStart · SubagentStopA forked worker starts or finishes
StopThe agent finishes a turn
StopFailureIt did not finish cleanly
NotificationAnything else worth emitting

The blocking ones are where policy lives. PreToolUse is the obvious one — an allowlist, a rate limit, a check that this caller may touch this record. PreCompact is the quieter one: compaction discards history, and a hook can refuse when something in it must not be lost.

PostToolUseFailure is worth wiring early. A tool that raises is handed back to the model as an error result, so the turn continues and nothing in your logs says a call failed unless something is listening here.

Blocking

A shell hook blocks by exiting 2; its stderr becomes the message the model sees. Any other non-zero exit is a warning shown to the user only.

A Python callback blocks by returning a string containing REJECTED, BLOCKED or WRONG TOOL — the same semantics, so the two kinds of hook behave identically from the model's side.

Python or shell

A Python callback is fast and shares your process — right for anything on the hot path, and for anything that needs your objects.

A shell hook is a subprocess, which is slower but does not have to be written in Python and cannot take your process down. Right for ops integrations and external validators someone else maintains.

hooks.register("PreToolUse", matcher={"tool_name": "shell"},
               command="./scripts/check-command.sh")

A non-zero exit blocks the call and the hook's stderr becomes the message the model sees.

Why not put this in the tool

Because the tool would then be doing two jobs, and because the policy usually applies to several tools that have nothing else in common. An audit trail belongs to the deployment, not to any one function.