# Hooks API

> Call your own HTTP endpoint or MCP tool when a thread starts or a message arrives, and feed the result to the agent.

Source: https://docs.runbear.io/api/hooks

Last updated: 2026-09-07

**Agent hooks** let you run your own code at two points in a conversation — when a thread starts and when a user message arrives — and hand the result back to the agent as context, or stop the reply altogether. Use them to load per-user context from your systems before the model answers, to enforce a policy on incoming messages, or to route a message through a tool that already knows the user.

The hooks document uses the **[Claude Code hooks schema](https://docs.anthropic.com/en/docs/claude-code/hooks)**: the same event names, matcher groups, handler list, `timeout`, and JSON output. Codex, Cursor, and Copilot CLI have converged on the same shape, so a hook server you already run for those tools can be pointed at Runbear unchanged. Runbear narrows the schema to what it runs: two events, and `http` and `mcp_tool` handlers (no shell commands run on Runbear's workers).

Authenticate with a bearer API key from the Runbear **Manage API Keys** menu. Full request/response schemas live in the [OpenAPI reference](https://api.runbear.io/v1/docs).

## Endpoints

| Endpoint                            | Purpose                                                              |
| ----------------------------------- | -------------------------------------------------------------------- |
| `GET /v1/agents/{agentId}/hooks`    | Read the agent's hooks document (`{ "hooks": {} }` when none is set) |
| `PUT /v1/agents/{agentId}/hooks`    | Replace the agent's hooks document as one unit                       |
| `DELETE /v1/agents/{agentId}/hooks` | Remove the document and the header secrets it referenced; idempotent |

`PUT` and `DELETE` require an API key with the `manageAgents` capability. Hooks are supported on Anthropic and Claude Agent SDK agents.

Once the feature is enabled for your organization, the same document can be edited in the dashboard under **Agent → Settings → Hooks**, a JSON editor over exactly this format with the same validation and secret handling as `PUT`.

Removing the document (`DELETE`, or **Remove hooks** in the dashboard) also stops replaying the context earlier hooks added to a thread: its next message is sent without those blocks, which costs one prompt-cache miss on that thread.

> **Feature availability**
>
> Agent hooks are not generally available yet. `PUT` returns `403` with `error: "hooks_not_enabled"` for organizations without the feature; `GET` and `DELETE` keep working so a document written earlier stays readable and removable. The legacy `/v1/agents/{agentId}/session-start-hook` endpoint (see the [OpenAPI reference](https://api.runbear.io/v1/docs)) is the single-hook form of `SessionStart` and remains available; the two share the per-thread session-start cache, so only one of them runs for a given thread.

## The hooks document

```json
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "mcp_tool",
            "server": "context-server",
            "tool": "load_user_context",
            "input": {
              "slack_user_id": "${channel_user_id}",
              "first_user_message": "${prompt}"
            }
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "http",
            "url": "https://hooks.example.com/runbear",
            "headers": { "Authorization": "Bearer …" },
            "timeout": 3,
            "failClosed": true
          }
        ]
      }
    ]
  }
}
```

Each event maps to a list of **matcher groups**; each group holds the **handlers** that run when its matcher applies.

| Field          | Meaning                                                                                                                                                                                                                                                                                                                                                                            |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `matcher`      | Which invocations the group applies to, evaluated the way Claude Code does. Omitted, empty, or `*` matches everything; `A\|B` is an exact-name list; anything else is an unanchored regular expression. `SessionStart` matches on how the session started, which is always `startup` on Runbear; `UserPromptSubmit` has no matcher and a value there is ignored, as in Claude Code |
| `hooks[].type` | `http` or `mcp_tool`                                                                                                                                                                                                                                                                                                                                                               |
| `url`          | `http` only. Must be `https` and resolve to a public address — a private or loopback address is refused when the hook runs                                                                                                                                                                                                                                                         |
| `headers`      | `http` only. Request headers, typically an authorization token. See [Header secrets](#header-secrets)                                                                                                                                                                                                                                                                              |
| `server`       | `mcp_tool` only. The `app` namespace of one of the agent's [Custom MCP](/agents/tools/custom-mcp.md) integrations. The integration must exist when the document is written                                                                                                                                                                                                         |
| `tool`         | `mcp_tool` only. The tool to call on that server                                                                                                                                                                                                                                                                                                                                   |
| `input`        | `mcp_tool` only. Arguments for the tool; string values may use `${field}` templates. See [Templates](#templates-for-mcp-tools)                                                                                                                                                                                                                                                     |
| `timeout`      | Seconds. Defaults to 5 on `SessionStart` and 3 on `UserPromptSubmit`; at most 10. Every hook sits on the reply's critical path, so keep it short                                                                                                                                                                                                                                   |
| `failClosed`   | `UserPromptSubmit` only. Block the reply when this handler fails instead of ignoring the failure                                                                                                                                                                                                                                                                                   |

A document may hold at most 8 handlers per event across all groups. `PUT` validates the whole document and returns `422` with `error: "invalid_hooks"` and a per-field `issues` list on any violation, or `error: "unknown_mcp_server"` when an `mcp_tool` handler names a server that is not a Custom MCP integration on the agent. Unknown events and unknown handler fields are rejected rather than ignored, so a typo cannot be saved as a hook that silently never fires.

## Events

**`SessionStart`** runs once per thread, when the first message arrives and before the agent replies to it. Whatever context the handlers return is added to the agent's system prompt for the life of the thread — later messages and later participants in the same thread reuse it without calling the hook again. `SessionStart` is context-only: it always fails open, and `failClosed` is rejected on it.

**`UserPromptSubmit`** runs for every user message, before the model sees it. Handlers can add context to that message or block the reply. The result is stored with the message and replayed byte-for-byte on every later turn of the thread, so the model's view of earlier turns never changes.

## What your handler receives

Both handler types receive the same JSON input. Field names follow Claude Code's hook input so a handler written for Claude Code reads it unchanged; the Runbear-specific fields are additive.

```json
{
  "session_id": "6b3d…",
  "hook_event_name": "UserPromptSubmit",
  "agent_id": "f47ac10b-…",
  "agent_name": "Support",
  "org_id": "0c9e…",
  "thread_id": "6b3d…",
  "turn_id": "1720000000.000100",
  "channel": "slack",
  "channel_id": "C0123456789",
  "channel_user_id": "U0123456789",
  "user_name": "Dana",
  "user_email": "dana@example.com",
  "prompt": "Can I get a refund for order 4821?"
}
```

| Field                              | Meaning                                                                                                                                                                                                                                                                              |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `session_id`, `thread_id`          | The thread. `session_id` is kept for Claude Code parity and equals `thread_id`                                                                                                                                                                                                       |
| `hook_event_name`                  | `SessionStart` or `UserPromptSubmit`                                                                                                                                                                                                                                                 |
| `agent_id`, `agent_name`, `org_id` | The agent and organization the hook is configured on                                                                                                                                                                                                                                 |
| `turn_id`                          | Identifier of the message that triggered this turn, when the channel has one                                                                                                                                                                                                         |
| `channel`                          | Channel family the thread lives in. Anthropic agents report the connection's channel type (`slack`, `teams`, `discord`, `web`, `api`, `external_trigger`, …); Claude Agent SDK agents report `slack`, `teams`, `playground`, or `api` for every request that arrives through the API |
| `channel_id`, `channel_user_id`    | The channel's own identifiers for the conversation and the sender (a Slack channel and user id, for example), when known                                                                                                                                                             |
| `user_name`, `user_email`          | The sender, when the channel provides them                                                                                                                                                                                                                                           |
| `prompt`                           | The text of the message being answered. On `SessionStart` it is the thread's first message, when present                                                                                                                                                                             |

`SessionStart` input also carries `"source": "startup"` — Runbear threads never resume or clear, so the value is constant.

An `http` handler receives this as the body of a `POST` with `Content-Type: application/json` and an `X-Runbear-Hook-Event` header naming the event, plus any headers you configured. An `mcp_tool` handler receives it through its `input` template — see [Templates](#templates-for-mcp-tools).

## What your handler returns

Handlers answer with the Claude Code JSON output. Runbear reads two things from it:

```json
{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Dana is on the Enterprise plan; order 4821 shipped on 2026-08-30."
  }
}
```

```json
{
  "decision": "block",
  "reason": "This channel is for billing questions only."
}
```

- `hookSpecificOutput.additionalContext` — text the agent should see next to the event that fired the hook. An empty string counts as no context.
- `decision: "block"` with `reason` — `UserPromptSubmit` only. The reply is not generated; `reason` is shown to the user instead. On `SessionStart` a `block` is ignored.

Everything else in the Claude Code output (`continue`, `suppressOutput`, `systemMessage`, …) is accepted and ignored, so a hook written for Claude Code never fails to parse.

For an **`http` handler**, only a `2xx` response with a JSON object body carries a result. A `2xx` with an empty body is a successful no-op. Any other status, a body over 1 MB, or a redirect is a failure. Redirects are not followed.

For an **`mcp_tool` handler**, the tool's text content is read the way Claude Code reads a command hook's output: JSON when it looks like a JSON object, otherwise the text is the context itself. A tool that returns plain text therefore needs no wrapper. A tool result flagged `isError` is a failure.

## How results are merged

All handlers registered for an event run concurrently (up to 4 at a time) and every one of them runs to completion. Their outputs are merged the way Claude Code merges them:

- Every `additionalContext` is kept, concatenated in handler order (the order handlers appear in the document, across groups).
- A single `block` blocks. The first blocking handler in document order supplies the `reason`.
- Any one context string is capped at 10,000 characters; longer values are truncated with a marker.
- On `UserPromptSubmit`, a context string identical to one already injected earlier in the thread is not injected again, so a handler may statelessly return every fragment relevant to the question on every message and only new fragments reach the model. On Anthropic agents the comparison covers the messages still in the uncompacted window; after a context compaction a fragment summarized away may be injected again.

## Failures and timeouts

Hooks **fail open** by default: a handler that times out, cannot be reached, answers with a non-`2xx` status, or returns something Runbear cannot parse is ignored, and the reply proceeds with whatever the other handlers returned. Failures are logged and visible in the agent's traces.

Set `failClosed: true` on a `UserPromptSubmit` handler to invert that for a handler that acts as a policy gate: when it fails, the reply is blocked and the user sees a generic notice that a policy check configured by your organization did not allow the request. `SessionStart` cannot fail closed.

The default timeouts (5 s for `SessionStart`, 3 s for `UserPromptSubmit`) are deliberately a fraction of Claude Code's: a user is waiting on the other end. A handler may raise its `timeout` to at most 10 seconds.

## Where the context lands

`UserPromptSubmit` context is placed next to the user message it was fetched for, inside a block the agent is told to use as background rather than answer or repeat. The block is stored with that message and replayed on every later turn of the thread exactly as it was first rendered, which keeps the prompt cache intact and means a hook that fails on one message does not alter how earlier messages were seen.

`SessionStart` context is added to the system prompt as a **Session Context** entry, once per thread. For organizations with the session-start hook feature, its outcome is also reported in the [Traces API](/api/traces.md#session-start-hook-outcome) `sessionStartHook` field.

`mcp_tool` handlers run only on turns where the agent's MCP servers are connected. On a turn the agent answers because a user (not the agent) was mentioned, no MCP server is connected: `UserPromptSubmit` runs its `http` handlers and skips its `mcp_tool` handlers for that turn, and a `SessionStart` that has any `mcp_tool` handler is deferred in full to the thread's first ordinary turn so none of its handlers is lost.

## Templates for MCP tools

An `mcp_tool` handler's `input` is rendered against the hook input before the call. String values may reference input fields as `${field}`; dot paths are allowed.

```json
{
  "type": "mcp_tool",
  "server": "context-server",
  "tool": "load_user_context",
  "input": {
    "slack_user_id": "${channel_user_id}",
    "first_user_message": "${prompt}",
    "source": "runbear"
  }
}
```

- A value that is exactly one placeholder is replaced by the referenced value and keeps its JSON type.
- A string that embeds placeholders (`"user ${user_name} on ${channel}"`) is interpolated as text.
- A top-level key whose whole-value placeholder resolves to nothing is dropped rather than sent as `null`, so an optional field the channel cannot supply (for example `user_email` on Slack) is simply omitted from the call.
- Keys the tool's input schema does not declare are dropped with a warning, so a template written for one tool version does not fail the call when a slot is renamed.

## Header secrets

`http` header values are stored in your organization's vault, never in the document itself, and never returned. `GET` shows every header value as `"<redacted>"`. To change a document without re-sending the token, send `"<redacted>"` for that header on `PUT` — it keeps the value already stored for the same handler URL and header name (matched case-insensitively). A `"<redacted>"` value for a header that has nothing stored is rejected. Secrets for headers that a `PUT` drops, and every secret on `DELETE`, are deleted from the vault in the same transaction. `content-type`, `content-length`, `host`, and `x-runbear-hook-event` cannot be overridden.

## HIPAA agents

Hooks run for HIPAA-mode agents. For those agents the hook invocation itself is traced without content — only which handler ran, its status, and its latency are recorded. Context a hook returns becomes part of the prompt and is handled like any other prompt content for that agent.
