Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.rkat.ai/llms.txt

Use this file to discover all available pages before exploring further.

Hooks let you run custom logic at defined points in the agent lifecycle — before/after tool execution, LLM calls, or at turn boundaries. Use them for approval workflows, audit logging, 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
  • log or audit agent behavior
  • block unsafe tool inputs or outputs
  • choose between in-process, command, and HTTP hook runtimes

Hook points

Eight extension points are available:
HookPointClassificationWhen it fires
RunStartedpreStart of Agent::run(), before any LLM call
PreLlmRequestpreBefore each LLM streaming call
PreToolExecutionpreBefore each individual tool call is dispatched
TurnBoundarypreBetween turns, after all tool results are collected
PostLlmResponsepostAfter the LLM response is received
PostToolExecutionpostAfter each tool execution completes
RunCompletedpostAfter a successful run completes
RunFailedpostAfter a run fails with an error
Classification is determined by the is_pre() and is_post() methods on HookPoint. Foreground hooks block loop progression; background hooks run asynchronously for observation.

Capabilities

HookCapabilityPurpose
ObserveRead-only logging and metrics
GuardrailCan issue Deny decisions to block execution
RewriteRetired compatibility label; semantic patch authority is removed

Execution modes

HookExecutionModeBehavior
ForegroundBlocks loop progression. Allow / Deny decisions are handled synchronously.
BackgroundRuns asynchronously for observation.
Pre-point background hooks must be observe-only. A background hook on a is_pre() point with any capability other than Observe is rejected with HookEngineError::InvalidConfiguration.
Semantic patch publication is retired. Legacy non-empty patch payloads fail closed during runtime response 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:
{ "type": "in_process", "name": "my-handler" }
The handler is registered at engine construction time via DefaultHookEngine::with_in_process_handler() or register_in_process_handler(). The handler type is:
type InProcessHookHandler = Arc<dyn Fn(HookInvocation) -> HandlerFuture + Send + Sync>;

Failure policies

HookFailurePolicy is retained as a compatibility field on hook configuration. Current hook runtime failures fail closed through typed HookEngineError values; they are not converted into warning-only success or hook-local Deny decisions by FailOpen / FailClosed. Background hook failure policy is inert compatibility configuration. Pre-point background hooks are still required to be Observe, because they cannot safely block a pre-execution boundary after the loop has moved on.

Decisions and patches

pub enum HookDecision {
    Allow,
    Deny {
        hook_id: HookId,
        reason_code: HookReasonCode,
        message: String,
        payload: Option<Value>,  // optional structured data
    },
}
Reason codes:
HookReasonCodeMeaning
PolicyViolationBusiness rule or policy constraint
SafetyViolationContent safety check
SchemaViolationSchema or format validation failure
TimeoutHook timed out (system-generated)
RuntimeErrorHook execution failed (system-generated)
HookPatch no longer has valid variants. 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": { "decision": "deny", "hook_id": "my-guard", "reason_code": "policy_violation", "message": "blocked" }
}
decision is optional. The compatibility patches field may be empty, but non-empty legacy semantic patch payloads are rejected.

Invocation payload

Hooks receive a HookInvocation struct containing contextual data. Fields are populated based on the hook point:
FieldTypePopulated at
pointHookPointAlways
session_idSessionIdAlways
turn_numberOption<u32>Most points
promptOption<String>RunStarted
errorOption<String>RunFailed
llm_requestOption<HookLlmRequest>PreLlmRequest
llm_responseOption<HookLlmResponse>PostLlmResponse
tool_callOption<HookToolCall>PreToolExecution
tool_resultOption<HookToolResult>PostToolExecution
  • HookLlmRequest: max_tokens, temperature, provider_params, message_count
  • HookLlmResponse: assistant_text, tool_call_names, stop_reason, usage
  • HookToolCall: tool_use_id, name, args
  • HookToolResult: tool_use_id, name, content, content_blocks, is_error
HookToolResult.content_blocks carries the typed tool-result blocks, including image blocks, in original order. HookToolResult.content remains as a legacy text projection for existing hooks. Both fields are observational projections and cannot be returned as a rewrite.

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:
[hooks]
default_timeout_ms = 5000       # Default: 5000
payload_max_bytes = 131072      # Default: 128 * 1024 (128 KiB)
background_max_concurrency = 32 # Default: 32

[[hooks.entries]]
id = "safety-check"
enabled = true
point = "pre_tool_execution"
mode = "foreground"
capability = "guardrail"
priority = 10
# failure_policy = "fail_closed"  # Compatibility field; runtime failures remain typed engine errors
# timeout_ms = 10000              # Optional; overrides default_timeout_ms

[hooks.entries.runtime]
type = "command"
command = "python3"
args = ["hooks/safety_check.py"]
FieldTypeDefaultDescription
idHookId"hook"Unique identifier for the hook
enabledbooltrueWhether the hook is active
pointHookPointTurnBoundaryWhich hook point to fire at
modeHookExecutionModeForegroundForeground or background
capabilityHookCapabilityObserveWhat the hook can do
priorityi32100Execution order (lower runs first)
failure_policyOption<HookFailurePolicy>NoneCompatibility field; runtime failures remain typed engine errors
timeout_msOption<u64>NoneOverride default timeout
runtimeHookRuntimeConfigin_process/noopRuntime configuration
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:
pub struct HookRunOverrides {
    pub entries: Vec<HookEntryConfig>,  // Additional hooks for this run
    pub disable: Vec<HookId>,           // Hook IDs to disable for this run
}
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

The hook engine emits AgentEvent variants during execution:
EventWhen
HookStartedA hook begins execution
HookCompletedA hook finishes successfully
HookFailedA hook encounters an error
HookDeniedA hook returns a Deny decision

SDK usage

use meerkat_hooks::{DefaultHookEngine, InProcessHookHandler, RuntimeHookResponse};
use meerkat_core::{HooksConfig, HookEntryConfig, HookId, HookPoint,
                   HookCapability, HookRuntimeConfig, HookRuntimeKind};
use std::sync::Arc;

let mut config = HooksConfig::default();
config.entries.push(HookEntryConfig {
    id: HookId::new("my-guardrail"),
    point: HookPoint::PreToolExecution,
    capability: HookCapability::Guardrail,
    priority: 10,
    runtime: HookRuntimeConfig::new(
        HookRuntimeKind::InProcess,
        Some(serde_json::json!({"name": "my-handler"})),
    )?,
    ..Default::default()
});

let engine = DefaultHookEngine::new(config)
    .with_in_process_handler(
        "my-handler",
        Arc::new(|invocation| {
            Box::pin(async move {
                // Inspect invocation.tool_call, return decision
                Ok(RuntimeHookResponse::default())
            })
        }),
    );
use meerkat_core::HookRunOverrides;

let overrides = HookRunOverrides {
    disable: vec![HookId::new("noisy-observer")],
    entries: vec![],
};

// Pass via AgentBuildConfig.hooks_override

See also