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.
| Event | When | Can block |
|---|---|---|
UserPromptSubmit | A message arrives, before the model sees it | yes |
SessionStart | A session begins | — |
PreToolUse | Before a tool runs | yes |
PostToolUse | After it returns | — |
PostToolUseFailure | After it raises | — |
PermissionDenied | A permission check blocked a call | — |
PreCompact | Before history is compacted | yes |
PostCompact | After compaction | — |
SubagentStart · SubagentStop | A forked worker starts or finishes | — |
Stop | The agent finishes a turn | — |
StopFailure | It did not finish cleanly | — |
Notification | Anything 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.