Hooks API
Call your own HTTP endpoint or MCP tool when a thread starts, a message arrives, a tool runs, or the reply is ready, and feed the result to the agent.
On this page
Agent hooks let you run your own code at five points in a conversation — when a thread starts, when a user message arrives, before and after each tool call, and when the reply is ready — and hand the result back to the agent as context, block a message or a tool call, or ask the agent to keep working. Use them to load per-user context from your systems before the model answers, to enforce a policy on incoming messages or on which tools the agent may call, to annotate tool results, or to check a reply before it goes out.
The hooks document uses the Claude Code hooks schema: the same event names, matcher groups, handler list, timeout, and JSON output, so a hook server written for Claude Code or Codex can be pointed at Runbear unchanged. Runbear narrows the schema to what it runs: five 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#
| 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. Through the API, hooks are managed on Anthropic and Claude Agent SDK agents. Classic agents run hooks as well, but their document is managed from the dashboard only.
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.
| 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; PreToolUse and PostToolUse match on the tool name as the model sees it (see Tool names); UserPromptSubmit and Stop have 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 |
server | mcp_tool only. The app namespace of one of the agent's Custom MCP 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 |
timeout | Seconds. Defaults to 5 on SessionStart and 3 on every other event; at most 10. Every hook sits on the reply's critical path, so keep it short |
failClosed | UserPromptSubmit and PreToolUse only. Block the reply, or deny the tool call, 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.
PreToolUse runs before each tool call the model makes, with the tool's name and arguments. A handler can allow the call, deny it, or rewrite its arguments. A denied call never runs: the model receives the reason as the tool's error result and can relay it to the user. Tools the model provider runs inside its own request never pass through Runbear and are not hookable: on Anthropic agents, web search and code execution; on Classic agents, Vertex google_search and code_execution. On Claude Agent SDK agents the SDK's built-in tools (including its web search and fetch) are ordinary tool calls and are hooked.
PostToolUse runs after each tool call that succeeded, with the arguments the tool ran with and its result; a call the runtime marked as an error (a denied call, a tool that threw) is not hooked, while a tool that reports a failure inside an otherwise normal result still is. Handlers can add context the model reads next to the result. The tool has already run, so a block cannot undo it: its reason is fed back to the model as guidance to act on, as in Claude Code. Always fails open.
Stop runs when the agent's reply is ready, with the reply text. A handler can let the reply go out or, with decision: "block", send reason back to the agent and have it continue working — at most once per turn. On the continuation the input carries stop_hook_active: true and a further block is ignored, so a handler that checks the flag never loops. Always fails open.
Tool names#
PreToolUse and PostToolUse matchers are evaluated against tool_name, the name the tool is registered under for the model. That name depends on where the tool comes from and on the agent's runtime, so treat a tool_name you have observed as the source of truth: add a PostToolUse group with no matcher whose handler logs tool_name, run the agent once, then narrow the matcher.
- Anthropic agents register every tool with characters other than letters, digits, and
_replaced by_. Runbear's own tools:runbear_file_search,runbear_slack_send_message. A Custom MCP tool: the integration'sservernamespace,_, the tool's name on the server, after that replacement — servercontext-server, toollookup→context_server_lookup. An app integration action: its key in upper case —SLACK_SEND_MESSAGE. - Claude Agent SDK agents use the SDK's form
mcp__<server>__<tool>. Runbear's own tools sit under a server namedrunbear:mcp__runbear__authorize_integration. A Custom MCP tool:mcp__context-server__lookup, where<server>is the namespace trimmed, with any run of characters other than letters, digits, and-collapsed to_, leading and trailing_removed (__crm__→crm), plus a_2,_3, … suffix when that name is already taken by another server — another Custom MCP integration, an app or managed integration, or Runbear's ownrunbearserver (a Custom MCP integration namedrunbearbecomesrunbear_2). An app integration action:mcp__pipedream_<app>__<action>. The SDK's built-in tools keep their SDK names:Bash,Read,WebSearch. - Classic agents register names as they are:
runbear_file_search,runbear-slack-send-message,context-server_lookup,SLACK-SEND-MESSAGE.
A matcher made only of letters, digits, _, -, whitespace, |, and , is an exact-name list, so refund matches a tool named exactly refund and nothing else. To match across spellings, write a regular expression — a matcher with any other character (., [, ^, …) is one, and it is unanchored: refund.* matches every tool whose name contains refund, send[-_]message matches both send-message and send_message, and ^mcp__context-server__ matches every tool of that server on a Claude Agent SDK agent.
Example: gate a tool and check the reply#
{
"hooks": {
"PreToolUse": [
{
"matcher": "issue_refund|delete_record",
"hooks": [
{
"type": "http",
"url": "https://hooks.example.com/runbear/tool-policy",
"headers": { "Authorization": "Bearer …" },
"failClosed": true
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "mcp_tool",
"server": "context-server",
"tool": "review_reply",
"input": {
"reply": "${last_assistant_message}",
"already_continued": "${stop_hook_active}"
}
}
]
}
]
}
}The PreToolUse group's matcher names two tools, so the policy endpoint is called only when the model tries to call issue_refund or delete_record; every other tool runs without a hook. To refuse a call the endpoint answers with a deny:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Refunds over $500 need a human approver."
}
}With failClosed: true, an unreachable policy endpoint denies the call rather than letting it through. The Stop handler receives the finished reply; returning { "decision": "block", "reason": "The reply does not cite the ticket number." } sends the agent back to work once, and when already_continued is true the tool should return {} so the reply goes out.
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. Every event carries the shared fields; each event adds its own.
{
"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, UserPromptSubmit, PreToolUse, PostToolUse, or Stop |
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 | SessionStart and UserPromptSubmit. The text of the message being answered. On SessionStart it is the thread's first message, when present |
source | SessionStart only. Always startup — Runbear threads never resume or clear |
tool_name | PreToolUse and PostToolUse. The tool's name as the model sees it; also the value the group's matcher is evaluated against |
tool_input | PreToolUse and PostToolUse. The tool's arguments as a JSON object: on PreToolUse what the model passed, on PostToolUse what the tool ran with (after any updatedInput) |
tool_use_id | PreToolUse and PostToolUse. Identifies the call, so a PostToolUse handler can pair a result with what PreToolUse saw |
tool_response | PostToolUse only. What the tool returned: its text, or its structured result when it had one |
stop_hook_active | Stop only. true when this reply is already the continuation an earlier Stop block asked for, so a handler can avoid asking again |
last_assistant_message | Stop only. The reply the agent is about to send — the same field the Claude Agent SDK provides, always populated here since there is no transcript file to read it from |
A PreToolUse input, for example, adds the tool fields to the shared ones:
{
"hook_event_name": "PreToolUse",
"tool_name": "issue_refund",
"tool_input": { "order_id": "4821", "amount": 640 },
"tool_use_id": "toolu_01…"
}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 the following 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": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": { "order_id": "4821", "amount": 640, "requested_by": "U0123456789" }
}
}hookSpecificOutput.additionalContext— text the agent should see next to the event that fired the hook. Read onSessionStart,UserPromptSubmit, andPostToolUse. An empty string counts as no context.PreToolUseandStopare decision events: context returned there is not injected.decision: "block"withreason— what it does depends on the event. OnUserPromptSubmitthe reply is not generated andreasonis shown to the user. OnPostToolUsethe tool has already run, soreasonis fed back to the model next to the tool result. OnStopthe reply is held and the agent continues withreasonas its instruction, once per turn. OnPreToolUsea top-levelblockis Claude Code's older spelling ofdenyand is honoured as one (approvelikewise meansallow). OnSessionStartablockis ignored.hookSpecificOutput.permissionDecision—PreToolUseonly.allowlets the call run;denystops it and handspermissionDecisionReasonto the model as the tool's error result.askis treated asdeny: Runbear has no per-call approval prompt, so a handler that would prompt the user in Claude Code denies here, with its reason, and the model can ask the user itself.deferis a CLI-only value and is ignored.hookSpecificOutput.updatedInput—PreToolUseonly. Replaces the tool's arguments; the tool validates them the way it validates the model's own.
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 on SessionStart, UserPromptSubmit, or PostToolUse; on PreToolUse and Stop plain text carries no decision and is ignored. 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
additionalContextis kept, concatenated in handler order (the order handlers appear in the document, across groups). - On
UserPromptSubmit,PostToolUse, andStop, a singleblockblocks. The first blocking handler in document order supplies thereason. - On
PreToolUse, the strictest permission wins —denyoveraskoverallow— with the first reason given for that decision in document order. AfailClosedhandler that fails counts asdenyat its own position, so an earlier handler's explicit reason is kept over the generic notice.updatedInputis taken from the last handler that set one, and dropped when the call is denied. On Classic agents the conversation history keeps the model's original arguments while the tool ran with the rewritten ones; Anthropic agents record the rewritten arguments. - 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 operation proceeds with whatever the other handlers returned — the reply is generated, the tool call runs with its original arguments, the reply goes out. Failures are logged and visible in the agent's traces.
Set failClosed: true on a UserPromptSubmit or PreToolUse handler to invert that for a handler that acts as a policy gate. On UserPromptSubmit a failure blocks the reply and the user sees a generic notice that a policy check configured by your organization did not allow the request; on PreToolUse a failure denies the tool call and the model receives the same generic notice as the tool's error result. SessionStart, PostToolUse, and Stop cannot fail closed; failClosed is rejected on them.
The default timeouts (5 s for SessionStart, 3 s for every other event) are deliberately a fraction of Claude Code's: a user is waiting on the other end, and the tool events fire once per tool call. A handler may raise its timeout to at most 10 seconds.
Budget the tool events with care: PreToolUse and PostToolUse each add one round trip per tool call, handlers run four at a time, and a turn can make many tool calls in sequence. With the defaults a tool-heavy turn adds a few seconds per call; with eight handlers at the 10 s ceiling it can add up to 40 s per call. Keep tool-event handlers fast and few.
Where the context lands#
UserPromptSubmit context is placed next to the user message it was fetched for, inside a <hook_context> 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.
PostToolUse context is appended to the tool result the model reads, after the tool's own text and inside the same <hook_context> block, so a model that has learned the block next to a user message reads it the same way next to a tool result. A block reason on PostToolUse follows it in its own <hook_feedback> block, framed as guidance the model must act on rather than background. On Claude Agent SDK agents both are handed to the SDK as the hook's additionalContext, which the SDK places next to the result. Nothing is cached for the tool events or Stop: a tool call and a finished reply happen once, and their outcome is carried by the turn's own messages.
mcp_tool handlers run only on turns where the agent's MCP servers are connected. On Anthropic and Classic agents, a turn the agent answers because a user (not the agent) was mentioned connects no MCP server: UserPromptSubmit, PreToolUse, PostToolUse, and Stop run their http handlers and skip their 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. Claude Agent SDK agents connect their MCP servers on every turn, so nothing is skipped there.
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, so ${tool_input.order_id} reads one argument of the call on the tool events and ${last_assistant_message} reads the reply on Stop.
{
"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 —
"${tool_input}"passes the whole arguments object,"${stop_hook_active}"a boolean. - 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 exampleuser_emailon 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.