Tool Hooks
Tool hooks are synchronous webhooks that fire during tool use. They let you validate, block, modify, or execute tool calls on your own infrastructure.
When Hooks Fire
- pre_tool_use: before tool execution. You can block or modify tool arguments.
- post_tool_use: after a tool returns. You can optionally modify the result.
- tool_execute: instead of built-in execution. Your webhook returns the tool result.
Typical Flow
- The model emits a tool call.
- ModelRelay calls your
pre_tool_usewebhook. - ModelRelay executes the tool (built-in or
tool_execute). - ModelRelay calls your
post_tool_usewebhook.
Example: Block a Tool Call
Return action: "block" when the end user is not authorized.
{
"action": "block",
"error": "user is not allowed to call search_inventory"
}
Example: Execute a Custom Tool
For tool_execute, respond with action: "result" and include the tool output:
{
"action": "result",
"result": {
"items": [{"id": "sku_123", "name": "Red Shoes"}]
}
}
Fail Behavior
- Use
fail_closedto fail the request if the hook is unavailable. - Use
fail_opento skip hook errors and continue.
Defaults:
pre_tool_useandtool_execute→fail_closedpost_tool_use→fail_open
Execution Order
When multiple hooks of the same type exist, they execute in order of created_at (oldest first). If any hook returns block, execution stops immediately.
Security
Tool hook requests are signed with HMAC SHA-256. Verify the signature to ensure requests originate from ModelRelay and haven’t been tampered with.
Headers included with every request:
X-ModelRelay-Timestamp(RFC3339 format)X-ModelRelay-Signature(sha256=+ hex HMAC)
Signature string: timestamp + "." + raw_body
Signature Verification Example
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"time"
)
func verifySignature(secret, timestamp string, body []byte, signature string) bool {
signedPayload := timestamp + "." + string(body)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
func hookHandler(w http.ResponseWriter, r *http.Request) {
timestamp := r.Header.Get("X-ModelRelay-Timestamp")
signature := r.Header.Get("X-ModelRelay-Signature")
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
if !verifySignature(secret, timestamp, body, signature) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
// Check timestamp to prevent replay attacks (5 minute tolerance)
hookTime, _ := time.Parse(time.RFC3339, timestamp)
if time.Since(hookTime).Abs() > 5*time.Minute {
http.Error(w, "request too old", http.StatusUnauthorized)
return
}
// Process the hook...
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"action":"allow"}`))
}
Replay Attack Protection
Important: Always validate the X-ModelRelay-Timestamp header to prevent replay attacks. Reject requests where the timestamp is more than 5 minutes from the current time. This ensures that captured requests cannot be replayed by attackers.
Configure Hooks
Use the dashboard or the Tool Hooks API to create hooks and rotate secrets.
Troubleshooting
Hook returns 401 Unauthorized
- Verify your signing secret matches the one from hook creation
- Ensure you’re using the raw request body (not parsed JSON) for signature verification
- Check that the timestamp header is included in the signature string
Hook times out
- Default timeout is 5000ms. Increase
timeout_msif your endpoint needs more time - Consider using
fail_openfor non-critical post-processing hooks - Optimize your endpoint response time for latency-sensitive pre_tool_use hooks
Tool calls are blocked unexpectedly
- Check
tools_allowlist- if set, only listed tools trigger the hook - Verify your endpoint returns
{"action": "allow"}for valid requests - Review hook events in the dashboard to see response details
Signature verification fails intermittently
- Ensure your server clock is synchronized (NTP)
- Use the raw request body bytes, not a re-serialized JSON object
- Verify you’re using constant-time comparison (timing-safe equals)
Multiple hooks not executing
- Hooks execute in
created_atorder (oldest first) - If a hook returns
block, subsequent hooks are skipped - Each hook type runs independently (pre, post, execute)