> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runlayer.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Scanners

> Send MCP traffic to your own scanning service and enforce its allow/deny verdict

A custom scanner is an HTTPS endpoint you host. For each covered event, Runlayer sends it a signed request describing the event, and your endpoint answers `allow` or `deny`.

Runlayer keeps enforcement. A `deny` goes through the same block or alert flow, audit log, and [incidents](/platform-incidents) as Runlayer's built-in scanners, so you can plug in an existing DLP or classification service without building the plumbing around it.

## What gets scanned

Each scanner covers one or both directions:

| Direction | What your scanner sees |
| - | - |
| **Tool input** | MCP tool calls, before they reach the connector. Local tool calls from monitored agents, such as terminal commands and file edits. Also inbound prompts from monitored agents. |
| **Tool output** | MCP tool results, before they reach the client. Local tool results from monitored agents. |

Custom scanners run alongside the built-in scanners. A call is blocked if any scanner blocks it.

## Add a scanner

1. Go to **Settings → Security Scanners → Custom scanners** and click **Add scanner**.
2. Enter:
   * **Name**: how the scanner appears in settings, the audit log, and incidents.
   * **Endpoint URL**: the `https` URL Runlayer posts to, exactly as entered.
   * **Scan**: tool input, tool output, or both.
   * **Action**: what Runlayer does when your scanner denies a call. **Block** rejects it, **Alert** records the deny and lets the call through, and **Disabled** turns the scanner off. You can also change it from the scanner's row, like any other scanner.
   * **Timeout (ms)**: 1 to 10000. The default is 5000.
   * **When it fails or times out**: fail open (the call goes through) or fail closed (the failure counts as a deny, so the call is blocked when the action is Block and recorded when it's Alert).
   * **Custom headers** (optional): static headers sent on every request, for example an API key for your gateway. They are stored encrypted and never shown again.
3. Copy the signing secret. Runlayer shows it once. Your endpoint uses it to verify requests.
4. Click the settings button on the scanner's row, then **Test connection**. Runlayer sends a test request and shows the verdict, latency, and status code.

A workspace can have up to 10 custom scanners.

## Build the endpoint

The full request and response format is in the [custom scanner OpenAPI spec](/custom-scanner-protocol.openapi.yaml). In short:

**Request.** Runlayer sends one `POST` per event with a JSON body:

```json theme={null}
{
  "type": "prompt",
  "request_id": "scan_7c0f3b2a9d4e4e1b8a6f2c5d1e0a9b37",
  "tenant_id": "3f6c1a52-9b7e-4d0a-8c21-5e4f7a9b0c13",
  "actor": { "type": "user", "id": "9a1d7e44-…", "email_address": "dev@example.com" },
  "source": { "application": "cursor" },
  "messages": [
    {
      "role": "assistant",
      "content": [
        {
          "type": "tool_use",
          "id": "0e8a3c5f-…",
          "tool_name": "create_issue",
          "input": { "repo": "acme/payments", "body": "Current value is hunter2" }
        }
      ]
    }
  ],
  "session_id": "5d2e8f1a-…",
  "model": null,
  "metadata": {
    "direction": "input",
    "scan_path": "other",
    "tool_name": "create_issue",
    "server_name": "github",
    "client_name": "cursor"
  }
}
```

Tool output requests carry the `tool_use` message followed by a `user` message with a `tool_result` block. Ignore fields and block types you don't recognize; new optional ones may be added.

**Response.** Return HTTP `200` with a verdict:

```json theme={null}
{ "action": "deny", "deny_reason": "Contains a database password.", "reference_id": "case-8f3a21" }
```

`deny_reason` (up to 500 characters) and `reference_id` (up to 50 characters of `A-Z a-z 0-9 . _ : / -`) are optional and recorded in the audit log. For a pass, return `{"action": "allow"}`.

**Failures.** Anything other than a `200` with a valid verdict counts as a scanner failure: other status codes (including redirects, which Runlayer doesn't follow), an invalid or oversized body (over 64 KiB), a timeout, or a connection error. The scanner's failure mode then decides whether the call goes through. If the connection can't be opened, Runlayer retries once after 100 ms within the same timeout. Nothing else is retried.

**Connection tests** have a `request_id` starting with `test_` and `metadata.scan_path` set to `config_test`. Return `allow` for them.

### Verify requests

Requests are signed with the [Standard Webhooks](https://www.standardwebhooks.com/) scheme, so any Standard Webhooks library can verify them. Each request carries `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers.

To verify by hand:

1. Base64-decode the part of the secret after `whsec_`. That is the HMAC key.
2. Compute `base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}"))` over the exact bytes received.
3. Accept the request if any `v1,<signature>` entry in `webhook-signature` matches, using a constant-time comparison.
4. Reject requests whose `webhook-timestamp` is more than 5 minutes from your clock.

### Rotate the secret

Click **Rotate secret** on the scanner's page. For 5 minutes after rotation, Runlayer signs every request with both the new and the old secret, so you can switch your verifier over without rejecting traffic.

### Network requirements

* The endpoint must use `https`. Credentials in the URL aren't accepted; use a header instead.
* Private-network addresses are allowed, so the scanner can run next to a self-hosted Runlayer. Loopback, link-local, and cloud metadata addresses are refused.
* Don't compress responses. Runlayer sends `Accept-Encoding: identity`.

Your scanner sits on the request path of every covered call, so keep it fast.

## Overrides

Custom scanners can be overridden in the same places as the built-in scanners:

* On a connector's **Security** tab, for the whole connector or for a single tool on it.
* Under **Local Tools and Client Overrides**, for a client, a local tool family such as Terminal, or a single local tool such as `bash`.

Only the action can be overridden. The failure mode and timeout always come from the scanner's own settings. When several overrides apply to a call, the most specific one wins, the same as for built-in scanners.

## Monitor a scanner

The scanner's settings page, opened from its row under **Custom scanners**, charts its activity over the last 7, 30, or 90 days, filterable by direction:

* Calls, denies, timeouts, and failure rate.
* Failures over time, split into timeouts and errors.
* Median and 95th-percentile latency.

Charts update within a few minutes. Each failure is also written to the [audit log](/platform-audit-logs) with its cause and latency, and each deny appears like any other scanner finding.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.