Skip to content
GitHub

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.


On this page

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

Endpoints#

EndpointPurpose
GET /v1/agents/{agentId}/hooksRead the agent's hooks document ({ "hooks": {} } when none is set)
PUT /v1/agents/{agentId}/hooksReplace the agent's hooks document as one unit
DELETE /v1/agents/{agentId}/hooksRemove 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.

The hooks document#

{
  "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.

FieldMeaning
matcherWhich 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[].typehttp or mcp_tool
urlhttp only. Must be https and resolve to a public address — a private or loopback address is refused when the hook runs
headershttp only. Request headers, typically an authorization token. See Header secrets
servermcp_tool only. The app namespace of one of the agent's Custom MCP integrations. The integration must exist when the document is written
toolmcp_tool only. The tool to call on that server
inputmcp_tool only. Arguments for the tool; string values may use ${field} templates. See Templates
timeoutSeconds. 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
failClosedUserPromptSubmit 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.

{
  "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?"
}
FieldMeaning
session_id, thread_idThe thread. session_id is kept for Claude Code parity and equals thread_id
hook_event_nameSessionStart or UserPromptSubmit
agent_id, agent_name, org_idThe agent and organization the hook is configured on
turn_idIdentifier of the message that triggered this turn, when the channel has one
channelChannel 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_idThe channel's own identifiers for the conversation and the sender (a Slack channel and user id, for example), when known
user_name, user_emailThe sender, when the channel provides them
promptThe 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.

What your handler returns#

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

{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Dana is on the Enterprise plan; order 4821 shipped on 2026-08-30."
  }
}
{
  "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 reasonUserPromptSubmit 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 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.

{
  "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.