> ## 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

> Typed extension points within the agent loop for observation and guardrails

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.

<Note>
  This page is the task-first guide. For the low-level type and payload inventory, see [Hooks reference](/reference/hooks-reference).
</Note>

## 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:

| HookPoint           | Classification | When it fires                                       |
| ------------------- | -------------- | --------------------------------------------------- |
| `RunStarted`        | pre            | Start of `Agent::run()`, before any LLM call        |
| `PreLlmRequest`     | pre            | Before each LLM streaming call                      |
| `PreToolExecution`  | pre            | Before each individual tool call is dispatched      |
| `TurnBoundary`      | pre            | Between turns, after all tool results are collected |
| `PostLlmResponse`   | post           | After the LLM response is received                  |
| `PostToolExecution` | post           | After each tool execution completes                 |
| `RunCompleted`      | post           | After a successful run completes                    |
| `RunFailed`         | post           | After 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

| HookCapability | Purpose                                                          |
| -------------- | ---------------------------------------------------------------- |
| `Observe`      | Read-only logging and metrics                                    |
| `Guardrail`    | Can issue `Deny` decisions to block execution                    |
| `Rewrite`      | Retired compatibility label; semantic patch authority is removed |

## Execution modes

| HookExecutionMode | Behavior                                                                       |
| ----------------- | ------------------------------------------------------------------------------ |
| `Foreground`      | Blocks loop progression. `Allow` / `Deny` decisions are handled synchronously. |
| `Background`      | Runs asynchronously for observation.                                           |

<Warning>
  **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`.
</Warning>

Semantic patch publication is retired. Legacy non-empty patch payloads fail closed during runtime response deserialization.

## How to add a hook

<Steps>
  <Step title="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`.
  </Step>

  <Step title="Choose a runtime">
    Pick one of the three runtimes: in-process (Rust closure), command (subprocess), or HTTP (remote endpoint).
  </Step>

  <Step title="Add configuration">
    Add a `[[hooks.entries]]` block to the active realm `config.toml` (or register programmatically via `HooksConfig`).
  </Step>

  <Step title="Implement the handler">
    Write the handler that receives a `HookInvocation` and returns a `RuntimeHookResponse` with an optional decision.
  </Step>

  <Step title="Test with overrides">
    Use `HookRunOverrides` to test your hook in isolation through RPC, REST, MCP, or embedded builders before committing the config.
  </Step>
</Steps>

<Note>
  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.
</Note>

## Runtimes

<Tabs>
  <Tab title="In-process">
    Calls a registered Rust closure. Config:

    ```json theme={null}
    { "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:

    ```rust theme={null}
    type InProcessHookHandler = Arc<dyn Fn(HookInvocation) -> HandlerFuture + Send + Sync>;
    ```
  </Tab>

  <Tab title="Command (subprocess)">
    Spawns a subprocess. The `HookInvocation` is serialized as JSON to stdin; the process must write a `RuntimeHookResponse` JSON object to stdout. Config:

    ```json theme={null}
    {
      "type": "command",
      "command": "python3",
      "args": ["hooks/safety_check.py"],
      "env": { "LOG_LEVEL": "debug" }
    }
    ```

    The subprocess payload size is capped at `payload_max_bytes` (default 128 KiB). Non-zero exit codes produce `HookEngineError::ExecutionFailed`.
  </Tab>

  <Tab title="HTTP">
    Sends a POST request to a URL. The `HookInvocation` is the JSON body; the response must be a `RuntimeHookResponse` JSON object. Config:

    ```json theme={null}
    {
      "type": "http",
      "url": "https://hooks.example.com/safety",
      "method": "POST",
      "headers": { "Authorization": "Bearer $TOKEN" }
    }
    ```

    The `method` defaults to `"POST"` if not specified. Response body size is capped at `payload_max_bytes`.
  </Tab>
</Tabs>

## 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

<Accordion title="HookDecision">
  ```rust theme={null}
  pub enum HookDecision {
      Allow,
      Deny {
          hook_id: HookId,
          reason_code: HookReasonCode,
          message: String,
          payload: Option<Value>,  // optional structured data
      },
  }
  ```

  **Reason codes:**

  | HookReasonCode    | Meaning                                  |
  | ----------------- | ---------------------------------------- |
  | `PolicyViolation` | Business rule or policy constraint       |
  | `SafetyViolation` | Content safety check                     |
  | `SchemaViolation` | Schema or format validation failure      |
  | `Timeout`         | Hook timed out (system-generated)        |
  | `RuntimeError`    | Hook execution failed (system-generated) |
</Accordion>

<Accordion title="Retired patch payloads">
  `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.
</Accordion>

## Runtime hook response

All three runtimes return the same `RuntimeHookResponse` structure:

```json theme={null}
{
  "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:

| Field          | Type                      | Populated at        |
| -------------- | ------------------------- | ------------------- |
| `point`        | `HookPoint`               | Always              |
| `session_id`   | `SessionId`               | Always              |
| `turn_number`  | `Option<u32>`             | Most points         |
| `prompt`       | `Option<String>`          | `RunStarted`        |
| `error`        | `Option<String>`          | `RunFailed`         |
| `llm_request`  | `Option<HookLlmRequest>`  | `PreLlmRequest`     |
| `llm_response` | `Option<HookLlmResponse>` | `PostLlmResponse`   |
| `tool_call`    | `Option<HookToolCall>`    | `PreToolExecution`  |
| `tool_result`  | `Option<HookToolResult>`  | `PostToolExecution` |

<Accordion title="Supporting types">
  * **`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`
</Accordion>

`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`:

<Steps>
  <Step title="Record denial">
    The denial is recorded as the merged decision.
  </Step>

  <Step title="Skip remaining foreground hooks">
    All remaining foreground hooks are skipped (short-circuit).
  </Step>

  <Step title="Skip background hooks">
    All background hooks are skipped (they only fire when no foreground `Deny` occurred).
  </Step>
</Steps>

<Note>
  A priority-1 guardrail that denies will prevent a priority-100 observer from running.
</Note>

## Configuration

<Accordion title="Config file example (realm config.toml)">
  Hook configuration lives under the `[hooks]` table:

  ```toml theme={null}
  [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"]
  ```
</Accordion>

<Accordion title="HookEntryConfig fields">
  | Field            | Type                        | Default          | Description                                                      |
  | ---------------- | --------------------------- | ---------------- | ---------------------------------------------------------------- |
  | `id`             | `HookId`                    | `"hook"`         | Unique identifier for the hook                                   |
  | `enabled`        | `bool`                      | `true`           | Whether the hook is active                                       |
  | `point`          | `HookPoint`                 | `TurnBoundary`   | Which hook point to fire at                                      |
  | `mode`           | `HookExecutionMode`         | `Foreground`     | Foreground or background                                         |
  | `capability`     | `HookCapability`            | `Observe`        | What the hook can do                                             |
  | `priority`       | `i32`                       | `100`            | Execution order (lower runs first)                               |
  | `failure_policy` | `Option<HookFailurePolicy>` | `None`           | Compatibility field; runtime failures remain typed engine errors |
  | `timeout_ms`     | `Option<u64>`               | `None`           | Override default timeout                                         |
  | `runtime`        | `HookRuntimeConfig`         | in\_process/noop | Runtime configuration                                            |
</Accordion>

<Accordion title="Layered config loading">
  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.
</Accordion>

## Per-run overrides

The `HookRunOverrides` struct allows per-request hook customization:

```rust theme={null}
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.

<Tabs>
  <Tab title="CLI">
    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.
  </Tab>

  <Tab title="RPC / REST / MCP">
    Hook overrides are passed via `HookParams`:

    ```json theme={null}
    {
      "hooks_override": {
        "disable": ["safety-check"],
        "entries": []
      }
    }
    ```
  </Tab>
</Tabs>

## Agent events

The hook engine emits `AgentEvent` variants during execution:

| Event           | When                             |
| --------------- | -------------------------------- |
| `HookStarted`   | A hook begins execution          |
| `HookCompleted` | A hook finishes successfully     |
| `HookFailed`    | A hook encounters an error       |
| `HookDenied`    | A hook returns a `Deny` decision |

## SDK usage

<Accordion title="Registering an in-process hook">
  ```rust theme={null}
  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())
              })
          }),
      );
  ```
</Accordion>

<Accordion title="Using hook overrides in AgentBuildConfig">
  ```rust theme={null}
  use meerkat_core::HookRunOverrides;

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

  // Pass via AgentBuildConfig.hooks_override
  ```
</Accordion>

## See also

* [Examples: Hooks](/examples/hooks)
* [Concepts: Tools](/concepts/tools)
* [Structured output](/guides/structured-output)
