---
title: "Hooks"
description: "Lifecycle hooks for audit and guardrails at 8 hook points."
icon: "anchor"
---

For the full guide, see [Hooks](/guides/hooks).

<Note>
Start here after you understand the normal session/tool flow. If you are new to Meerkat overall, read [Examples: Sessions](/examples/sessions) first.
</Note>

## Observer hook (background)

A `post_tool_execution` background hook that logs every tool call to a file. Background hooks run asynchronously and never block the agent loop.

<Tabs>
  <Tab title="CLI">
    Run-scoped hook override flags are not currently exposed on the normal `rkat run` surface. Use the JSON-RPC, REST, MCP, or SDK examples below for per-run hook override testing.
  </Tab>
  <Tab title="JSON-RPC">
    ```json
    {
      "jsonrpc": "2.0", "id": 1,
      "method": "session/create",
      "params": {
        "prompt": "Summarize this repo",
        "hooks_override": {
          "entries": [{
            "id": "audit-log",
            "point": "post_tool_execution",
            "mode": "background",
            "capability": "observe",
            "runtime": {
              "type": "command",
              "command": "python3",
              "args": ["hooks/log_tool.py"]
            },
            "timeout_ms": 3000
          }]
        }
      }
    }
    ```
  </Tab>
  <Tab title="REST">
    ```bash
    curl -X POST http://127.0.0.1:8080/sessions \
      -H "Content-Type: application/json" \
      -d '{
        "prompt": "Summarize this repo",
        "hooks_override": {
          "entries": [{
            "id": "audit-log",
            "point": "post_tool_execution",
            "mode": "background",
            "capability": "observe",
            "runtime": {
              "type": "command",
              "command": "python3",
              "args": ["hooks/log_tool.py"]
            },
            "timeout_ms": 3000
          }]
        }
      }'
    ```
  </Tab>
  <Tab title="MCP">
    ```json
    {
      "name": "meerkat_run",
      "arguments": {
        "prompt": "Summarize this repo",
        "enable_builtins": true,
        "hooks_override": {
          "entries": [{
            "id": "audit-log",
            "point": "post_tool_execution",
            "mode": "background",
            "capability": "observe",
            "runtime": {
              "type": "command",
              "command": "python3",
              "args": ["hooks/log_tool.py"]
            },
            "timeout_ms": 3000
          }]
        }
      }
    }
    ```
  </Tab>
  <Tab title="Python">
    ```python
    result = await client.create_session(
        "Summarize this repo",
        hooks_override={
            "entries": [{
                "id": "audit-log",
                "point": "post_tool_execution",
                "mode": "background",
                "capability": "observe",
                "runtime": {
                    "type": "command",
                    "command": "python3",
                    "args": ["hooks/log_tool.py"],
                },
                "timeout_ms": 3000,
            }]
        },
    )
    ```
  </Tab>
  <Tab title="TypeScript">
    ```typescript
    const result = await client.createSession("Summarize this repo", {
      hooksOverride: {
        entries: [{
          id: "audit-log",
          point: "post_tool_execution",
          mode: "background",
          capability: "observe",
          runtime: {
            type: "command",
            command: "python3",
            args: ["hooks/log_tool.py"],
          },
          timeoutMs: 3000,
        }],
      },
    });
    ```
  </Tab>
</Tabs>

## Guardrail hook (foreground)

A `pre_tool_execution` foreground hook that must approve each tool call before it runs. If the hook returns `deny`, the tool call is skipped.

<Tabs>
  <Tab title="CLI">
    Run-scoped hook override flags are not currently exposed on the normal `rkat run` surface. Use the JSON-RPC, REST, MCP, or SDK examples below for per-run hook override testing.
  </Tab>
  <Tab title="JSON-RPC">
    ```json
    {
      "jsonrpc": "2.0", "id": 1,
      "method": "session/create",
      "params": {
        "prompt": "Delete old logs",
        "hooks_override": {
          "entries": [{
            "id": "safety-gate",
            "point": "pre_tool_execution",
            "mode": "foreground",
            "capability": "guardrail",
            "runtime": {
              "type": "command",
              "command": "python3",
              "args": ["hooks/approve_tool.py"]
            },
            "timeout_ms": 5000
          }]
        }
      }
    }
    ```
  </Tab>
  <Tab title="REST">
    ```bash
    curl -X POST http://127.0.0.1:8080/sessions \
      -H "Content-Type: application/json" \
      -d '{
        "prompt": "Delete old logs",
        "hooks_override": {
          "entries": [{
            "id": "safety-gate",
            "point": "pre_tool_execution",
            "mode": "foreground",
            "capability": "guardrail",
            "runtime": {
              "type": "command",
              "command": "python3",
              "args": ["hooks/approve_tool.py"]
            },
            "timeout_ms": 5000
          }]
        }
      }'
    ```
  </Tab>
  <Tab title="MCP">
    ```json
    {
      "name": "meerkat_run",
      "arguments": {
        "prompt": "Delete old logs",
        "enable_builtins": true,
        "hooks_override": {
          "entries": [{
            "id": "safety-gate",
            "point": "pre_tool_execution",
            "mode": "foreground",
            "capability": "guardrail",
            "runtime": {
              "type": "command",
              "command": "python3",
              "args": ["hooks/approve_tool.py"]
            },
            "timeout_ms": 5000
          }]
        }
      }
    }
    ```
  </Tab>
  <Tab title="Python">
    ```python
    result = await client.create_session(
        "Delete old logs",
        hooks_override={
            "entries": [{
                "id": "safety-gate",
                "point": "pre_tool_execution",
                "mode": "foreground",
                "capability": "guardrail",
                "runtime": {
                    "type": "command",
                    "command": "python3",
                    "args": ["hooks/approve_tool.py"],
                },
                "timeout_ms": 5000,
            }]
        },
    )
    ```
  </Tab>
  <Tab title="TypeScript">
    ```typescript
    const result = await client.createSession("Delete old logs", {
      hooksOverride: {
        entries: [{
          id: "safety-gate",
          point: "pre_tool_execution",
          mode: "foreground",
          capability: "guardrail",
          runtime: {
            type: "command",
            command: "python3",
            args: ["hooks/approve_tool.py"],
          },
          timeoutMs: 5000,
        }],
      },
    });
    ```
  </Tab>
</Tabs>

## In-process hook (Rust only)

Register a Rust closure directly -- no subprocess, no serialization overhead. Only available in the Rust SDK.

<Tabs>
  <Tab title="Rust">
    ```rust
    let engine = DefaultHookEngine::new(hooks_config)
        .with_in_process_handler(
            "pii-guard",
            Arc::new(|invocation| Box::pin(async move {
                let text = invocation.tool_call.as_ref()
                    .map(|call| call.args.as_value().to_string()).unwrap_or_default();
                let decision = if contains_pii(&text) {
                    Some(HookDecision::deny(
                        HookId::new("pii-guard"),
                        HookReasonCode::PolicyViolation,
                        "tool arguments contain PII",
                        None,
                    ))
                } else {
                    None
                };
                Ok(RuntimeHookResponse {
                    decision,
                    ..Default::default()
                })
            })),
        );
    ```
  </Tab>
</Tabs>

Hooks receive typed projections of the request, response, tool call, and tool result. They can allow, deny, or observe those facts; they cannot rewrite canonical LLM, tool, or run output.

<Tabs>
  <Tab title="Rust">
    ```rust
    let engine = DefaultHookEngine::new(hooks_config)
        .with_in_process_handler(
            "tool-audit",
            Arc::new(|invocation| Box::pin(async move {
                let content = invocation.tool_result.as_ref()
                    .map(|r| r.content.as_str()).unwrap_or("");
                audit_tool_result(content).await;
                Ok(RuntimeHookResponse {
                    ..Default::default()
                })
            })),
        );
    ```
  </Tab>
</Tabs>

## HTTP hook

A hook that POSTs the invocation payload to a remote URL. Useful for centralized policy servers.

<Tabs>
  <Tab title="CLI">
    Run-scoped hook override flags are not currently exposed on the normal `rkat run` surface. Use the JSON-RPC, REST, MCP, or SDK examples below for per-run hook override testing.
  </Tab>
  <Tab title="JSON-RPC">
    ```json
    {
      "jsonrpc": "2.0", "id": 1,
      "method": "session/create",
      "params": {
        "prompt": "Analyze quarterly data",
        "hooks_override": {
          "entries": [{
            "id": "policy-server",
            "point": "pre_llm_request",
            "mode": "foreground",
            "capability": "guardrail",
            "runtime": {
              "type": "http",
              "url": "https://hooks.example.com/policy",
              "method": "POST"
            },
            "timeout_ms": 10000
          }]
        }
      }
    }
    ```
  </Tab>
  <Tab title="REST">
    ```bash
    curl -X POST http://127.0.0.1:8080/sessions \
      -H "Content-Type: application/json" \
      -d '{
        "prompt": "Analyze quarterly data",
        "hooks_override": {
          "entries": [{
            "id": "policy-server",
            "point": "pre_llm_request",
            "mode": "foreground",
            "capability": "guardrail",
            "runtime": {
              "type": "http",
              "url": "https://hooks.example.com/policy",
              "method": "POST"
            },
            "timeout_ms": 10000
          }]
        }
      }'
    ```
  </Tab>
  <Tab title="MCP">
    ```json
    {
      "name": "meerkat_run",
      "arguments": {
        "prompt": "Analyze quarterly data",
        "enable_builtins": true,
        "hooks_override": {
          "entries": [{
            "id": "policy-server",
            "point": "pre_llm_request",
            "mode": "foreground",
            "capability": "guardrail",
            "runtime": {
              "type": "http",
              "url": "https://hooks.example.com/policy",
              "method": "POST"
            },
            "timeout_ms": 10000
          }]
        }
      }
    }
    ```
  </Tab>
  <Tab title="Python">
    ```python
    result = await client.create_session(
        "Analyze quarterly data",
        hooks_override={
            "entries": [{
                "id": "policy-server",
                "point": "pre_llm_request",
                "mode": "foreground",
                "capability": "guardrail",
                "runtime": {
                    "type": "http",
                    "url": "https://hooks.example.com/policy",
                    "method": "POST",
                },
                "timeout_ms": 10000,
            }]
        },
    )
    ```
  </Tab>
  <Tab title="TypeScript">
    ```typescript
    const result = await client.createSession("Analyze quarterly data", {
      hooksOverride: {
        entries: [{
          id: "policy-server",
          point: "pre_llm_request",
          mode: "foreground",
          capability: "guardrail",
          runtime: {
            type: "http",
            url: "https://hooks.example.com/policy",
            method: "POST",
          },
          timeoutMs: 10000,
        }],
      },
    });
    ```
  </Tab>
</Tabs>

## All 8 hook points

| Hook point | Classification | When it fires |
|---|---|---|
| `run_started` | pre | Start of `Agent::run()`, before any LLM call |
| `pre_llm_request` | pre | Before each LLM streaming call |
| `pre_tool_execution` | pre | Before each individual tool call is dispatched |
| `turn_boundary` | pre | Between turns, after all tool results are collected |
| `post_llm_response` | post | After the LLM response is received |
| `post_tool_execution` | post | After each tool execution completes |
| `run_completed` | post | After a successful run completes |
| `run_failed` | post | After a run fails with an error |

<Note>
Pre-point hooks in `foreground` mode halt the agent loop until they return. Pre-point hooks in `background` mode are limited to observe-only capability.
</Note>

## Disable a hook

Use the `disable` list to turn off hooks defined in the realm config without removing them.

<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="JSON-RPC">
    ```json
    {
      "jsonrpc": "2.0", "id": 1,
      "method": "session/create",
      "params": {
        "prompt": "Quick test",
        "hooks_override": {
          "disable": ["safety-gate", "audit-log"]
        }
      }
    }
    ```
  </Tab>
  <Tab title="REST">
    ```bash
    curl -X POST http://127.0.0.1:8080/sessions \
      -H "Content-Type: application/json" \
      -d '{
        "prompt": "Quick test",
        "hooks_override": {
          "disable": ["safety-gate", "audit-log"]
        }
      }'
    ```
  </Tab>
  <Tab title="MCP">
    ```json
    {
      "name": "meerkat_run",
      "arguments": {
        "prompt": "Quick test",
        "enable_builtins": true,
        "hooks_override": {
          "disable": ["safety-gate", "audit-log"]
        }
      }
    }
    ```
  </Tab>
  <Tab title="Python">
    ```python
    result = await client.create_session(
        "Quick test",
        hooks_override={
            "disable": ["safety-gate", "audit-log"]
        },
    )
    ```
  </Tab>
  <Tab title="TypeScript">
    ```typescript
    const result = await client.createSession("Quick test", {
      hooksOverride: {
        disable: ["safety-gate", "audit-log"],
      },
    });
    ```
  </Tab>
</Tabs>

## Hook events

Hooks emit events into the session event stream. Subscribe to these to build dashboards, alerting, or audit trails.

```json
// HookStarted -- emitted when a hook begins execution
{"type": "hook_started", "hook_id": "safety-gate", "point": "pre_tool_execution"}

// HookCompleted -- emitted when a hook finishes successfully
{"type": "hook_completed", "hook_id": "safety-gate", "point": "pre_tool_execution", "duration_ms": 42}

// HookDenied -- emitted when a hook blocks execution
{"type": "hook_denied", "hook_id": "safety-gate", "point": "pre_tool_execution", "reason_code": "policy_violation", "message": "rm -rf not allowed"}

// HookFailed -- emitted when a hook errors or times out
{"type": "hook_failed", "hook_id": "audit-log", "point": "post_tool_execution", "error": "process exited with code 1"}
```

## Next step

- [Examples: Tools](/examples/tools)
- [Examples: Structured output](/examples/structured-output)
