Skip to main content
Hooks let you run custom logic at defined points in the agent lifecycle and react to committed runtime, session, and comms facts. Use them for approval workflows, responsive UI, audit logging, telemetry, content filtering, or typed denials.
This page is the task-first guide. For the low-level type and payload inventory, see Hooks reference.

What this guide is for

Use this guide when you want to:
  • add approval gates
  • react to committed input and peer-delivery outcomes
  • update application UI from typed interaction completions
  • log or audit agent behavior
  • block unsafe tool inputs or outputs
  • choose between in-process, command, and HTTP hook runtimes

Hook points

Fourteen extension points are available: Classification is determined by the is_pre() and is_post() methods on HookPoint. Foreground hooks block loop progression; background hooks run asynchronously for observation. The six post-commit points are intrinsically observe-only. Their session-scoped dispatcher owns spawned tasks, reaps completed work, and aborts remaining work when the dispatcher is dropped. A returned denial, timeout, or hook runtime failure cannot change or delay the committed fact.

Capabilities

The former Rewrite capability is deleted entirely; semantic patch authority is removed.

Execution modes

All background hooks must be observe-only. A background hook with any capability other than Observe is rejected with HookEngineError::InvalidConfiguration, regardless of hook point.The six post-commit points also require Observe capability in either execution mode. They are always dispatched outside the outcome path; configuring one as Foreground only orders matching hook handlers within its owned observation task.
Semantic patch publication is retired. RuntimeHookResponse rejects unknown fields, so a runtime response carrying any legacy patches field fails closed during deserialization.

How to add a hook

1

Choose hook point and capability

Decide which HookPoint to fire at (e.g., PreToolExecution for tool guardrails) and whether the hook needs Observe or Guardrail.
2

Choose a runtime

Pick one of the three runtimes: in-process (Rust closure), command (subprocess), or HTTP (remote endpoint).
3

Add configuration

Add a [[hooks.entries]] block to the active realm config.toml (or register programmatically via HooksConfig).
4

Implement the handler

Write the handler that receives a HookInvocation and returns a RuntimeHookResponse with an optional decision.
5

Test with overrides

Use HookRunOverrides to test your hook in isolation through RPC, REST, MCP, or embedded builders before committing the config.
The docs below show run-scoped hook overrides conceptually, but the current CLI does not expose run-scoped hook override flags in normal builds. For now, use RPC, REST, MCP, or the SDK/embedded surfaces when you want per-run hook overrides.

Runtimes

Calls a registered Rust closure. Config:
(name is accepted as a legacy alias for handler.)The handler is registered at engine construction time via DefaultHookEngine::with_in_process_handler() or register_in_process_handler(). The handler type is:
Foreground hook runtime failures fail closed through typed HookEngineError values; they are never converted into warning-only success or a hook-local Deny decision. Every background hook is required to be Observe, because asynchronous work cannot safely publish a guardrail decision after loop progression.

Decisions and patches

Reason codes:These are reason codes a hook response can supply with Deny, not automatic engine-denial outcomes. Engine timeouts and execution failures remain typed HookEngineError values, mapped to HookFailureReason::Timeout or HookFailureReason::ExecutionFailed in foreground HookFailed events.
The HookPatch type is deleted entirely. Older payloads such as llm_request, assistant_text, tool_args, tool_result, and run_result fail closed during deserialization instead of being ignored or applied.Hooks can observe typed projections and return Allow or Deny. Canonical provider parameters, assistant text, tool arguments, tool results, and final run text are owned by the runtime and tool execution path.

Runtime hook response

All three runtimes return the same RuntimeHookResponse structure:
decision is optional and is the only field. The struct is declared with deny_unknown_fields, so any legacy patches field — even an empty one — is rejected at deserialization.

Invocation payload

Hooks receive a HookInvocation struct containing contextual data. Fields are populated based on the hook point: For command and HTTP hooks, the serialized wire payload additionally carries prompt and error strings. These are serialize-only projections of prompt_input and error_report; they are never stored fields and are never deserialized back as authority.
  • HookLlmRequest: max_tokens, temperature, provider_params, message_count
  • HookLlmResponse: assistant_text, tool_call_names, stop_reason, usage, server_tool_content (typed ServerToolKind projection of provider-native server-tool blocks in the response, e.g. web search — empty when none; lets a foreground hook classify provider-native content ingestion synchronously)
  • HookToolCall: tool_use_id, name, args, provenance (typed ToolProvenance of the called tool — kind + source id, e.g. an MCP server name — projected from the tool’s ToolDef; absent for tools without declared provenance)
  • HookToolResult: tool_use_id, name, content_blocks, is_error, provenance (as on HookToolCall; the wire payload adds a serialize-only content text projection)
  • HookObservation: a tagged typed union for accepted, rejected, or deduplicated runtime input; committed peer ingress or egress; and completed interaction facts
HookToolResult.content_blocks carries the typed tool-result blocks, including image blocks, in original order. The wire payload also serializes a content string — a text projection derived from content_blocks for external command/HTTP hooks; in Rust, use HookToolResult::text_projection(). Both are observational projections and cannot be returned as a rewrite.

Hooks versus event streams

Use hooks as the default in-process application reaction API. Event streams remain the canonical ordered transport for remote observers, replay-aware pipelines, transcript/audit projection, and resynchronization after stream truncation. Command receipts remain the direct result for the caller that initiated an action; the matching committed outcome is also projected through the hook surface. Post-commit hooks are process-local best-effort reactions, not durable delivery. A crash after the canonical event is published may lose a hook invocation, and recovery of a finalized outbox may replay one. Use event streams for durable or replay-aware consumption; hook handlers with external side effects should deduplicate by canonical ids such as interaction_id. Post-commit hooks are not synchronous policy seams. They cannot close an in-turn race or gate a same-turn write. Keep using synchronous points such as PostToolExecution when policy must join the agent loop before it advances.

Priority ordering and deny short-circuiting

Foreground hooks are sorted by priority (ascending), then by registration_index (ascending, for determinism when priorities are equal). Lower numeric priority values run first. When a foreground hook returns Deny:
1

Record denial

The denial is recorded as the merged decision.
2

Skip remaining foreground hooks

All remaining foreground hooks are skipped (short-circuit).
3

Skip background hooks

All background hooks are skipped (they only fire when no foreground Deny occurred).
A priority-1 guardrail that denies will prevent a priority-100 observer from running.

Configuration

Hook configuration lives under the [hooks] table:
Hook loading is runtime-root aware. In workspace-derived realms, global and project hook entries are layered (~/.rkat/config.toml then .rkat/config.toml) and merged with active realm config hooks. In non-workspace realms, realm config is primary and no project config may be discovered.

Per-run overrides

The HookRunOverrides struct allows per-request hook customization:
Disabled hooks are removed from the effective entry list. Override entries are appended after the filtered base entries. All resulting entries are re-validated.
Run-scoped hook override flags are not currently exposed on the normal rkat run surface. Use JSON-RPC, REST, MCP, or SDK/embedded surfaces for per-run hook override testing.

Agent events

For ordinary agent-loop hook points, the session event stream can include: Ordinary background work does not promise a terminal HookCompleted or HookFailed session event. Later failures and capacity skips are observable through DefaultHookEngine::background_dispatch_ledger() and tracing; a capacity-skipped hook has not started and does not earn HookStarted. Post-commit reactions use a separate isolated dispatch path and do not promise these normal lifecycle session events.

SDK usage

See also