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();
}));
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.
  • HookOutcome is 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.