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

  1. The model emits a tool call.
  2. ModelRelay calls your pre_tool_use webhook.
  3. ModelRelay executes the tool (built-in or tool_execute).
  4. ModelRelay calls your post_tool_use webhook.

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_closed to fail the request if the hook is unavailable.
  • Use fail_open to skip hook errors and continue.

Defaults:

  • pre_tool_use and tool_executefail_closed
  • post_tool_usefail_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_ms if your endpoint needs more time
  • Consider using fail_open for 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_at order (oldest first)
  • If a hook returns block, subsequent hooks are skipped
  • Each hook type runs independently (pre, post, execute)