If you’ve built an agent in production, you know the failure mode: the model “forgets” the rules in the system prompt, calls a destructive tool, and there’s nothing between the call and the user. agentlib’s hook system is the thing between.
There are 21 hooks. Each one is a deterministic interception point that runs in Dart, not in the prompt. Each one
returns a HookOutcome that tells the agent loop what to do next: carry on, deny, or replace with a different action.
The full set
Loop hooks — fire around tool calls and the model turn.
PreToolUse— before any tool dispatches. Can mutate args, deny, replace tool.PostToolUse— after a tool returns successfully. Can mutate result.PostToolUseFailure— after a tool throws or returns an error. Can retry, replace, propagate.PermissionRequestHook— when a tool with capabilities (microphone, camera, contacts) is about to run. Surface a native consent sheet.
Subagent hooks — fire around subagent dispatch.
SubagentStart— before a subagent spawns. Log, mutate prompt, deny.SubagentMessage— each message the subagent emits.SubagentStop— when a subagent finishes.
Compaction hooks — fire around message compaction.
PreCompact— before compaction runs. Can supply a custom compaction strategy.PostCompact— after compaction. Can review the new state.
Lifecycle hooks — fire on OS events.
OnSuspend— app is about to background. Persist state.OnResume— app is in foreground again. Reload, refresh tokens.OnLowMemory— OS is warning you about memory. Trim caches, evict skills.OnLowBattery— battery dropped below threshold. Route to a smaller model.OnNetworkChange— connectivity changed. Re-route, retry, queue.
Snapshot hooks.
OnSnapshot— a snapshot was taken. Mirror to remote, log.OnRevert— a revert happened. Reset UI state, log analytics.
Shell hooks.
OnShellParse— an Sh pipeline was parsed but not yet dispatched. Audit the AST.
Misc.
UserPromptSubmit— user submitted a prompt. Sanitise, prepend instructions.StopHook— agent finished (terminal stop). Final cleanup.NotificationHook— agent wants to surface a notification.OnHandoff— agent is about to hand off to another agent.
What each does in practice
A few load-bearing examples.
PreToolUse — deny a destructive action
hooks.register(PreToolUse((call, ctx) async {
if (call.tool == 'whatsapp.send' && !userHasAcceptedTos) {
return HookOutcome.deny('TOS not accepted');
}
if (call.tool == 'gmail.send' && (call.args['to'] as String).endsWith('@example.com')) {
return HookOutcome.deny('External domain restricted');
}
return HookOutcome.carryOn();
}));
PermissionRequestHook — native consent sheet
hooks.register(PermissionRequestHook((req, ctx) async {
final granted = await showCupertinoModalPopup<bool>(
context: ctx.uiCtx!,
builder: (_) => ConsentSheet(capability: req.capability, reason: req.reason),
);
return granted == true
? HookOutcome.carryOn()
: HookOutcome.deny('user denied');
}));
OnLowBattery — re-route the model
hooks.register(OnLowBattery((event, _) async {
if (event.percent < 20) {
return HookOutcome.replace(
modelOverride: FllamaProvider(llm: tinyAdapter, modelName: 'Qwen3-0.6B'),
);
}
return HookOutcome.carryOn();
}));
OnNetworkChange — drop cloud when offline
hooks.register(OnNetworkChange((event, ctx) async {
if (!event.isReachable) {
return HookOutcome.replace(modelOverride: ctx.config.model.withoutCloud());
}
return HookOutcome.carryOn();
}));
PostToolUse — audit log
hooks.register(PostToolUse((call, result, ctx) async {
await audit.log(
runId: ctx.threadId,
tool: call.tool,
args: call.args,
ok: result is ToolResultOk,
at: DateTime.now(),
);
return HookOutcome.carryOn();
}));
How they compose
Hooks fire in registration order. The first hook that returns HookOutcome.deny(...) short-circuits the chain — no
further hooks run, the tool doesn’t dispatch. A HookOutcome.replace(...) mutates the call and continues. A
HookOutcome.carryOn() just observes and passes through.
This means hook order matters. Register policy hooks first (consent, deny lists) and observability hooks last (audit, analytics).
When to reach for a hook vs. a prompt instruction
Prompts give the model intent. “Don’t send messages to external domains.” The model usually does what the prompt says — usually. Hooks give guarantees. The agent loop in Dart enforces them regardless of what the model decides.
Rule of thumb:
- Prompt for soft guidance: tone, style, framing.
- Hook for hard rules: deny-lists, consent, audit, anything safety-critical.
Reference
- All hooks are sealed classes — pattern-match on them in your registry.
HookOutcomeis a sealed sum:carryOn,deny(reason),replace(...).- Hooks run on the host thread. Keep them quick; if you need long-running work, dispatch it asynchronously and let the hook return early.
Read next
- Snapshots, revert, fork — the other safety net.
- Suspend, resume, push-resume — lifecycle hooks in production.