---
title: 開發 Inference hooks 整合
url: https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint
description: 建置 AI 安全伺服器，用於接收已簽署的 Inference hooks 請求、驗證這些請求，並回傳允許或拒絕的判定。
---

<Note>
  「Inference hooks」（推論掛鉤）目前為 beta 版，僅供 Claude Enterprise 組織使用。在 beta 期間，欄位名稱、請求結構和標頭可能會變更。
</Note>

Inference hooks 整合是一個「AI security server」（AI 安全伺服器），也就是由 Anthropic 呼叫的 HTTPS 服務。對於每個受管控的請求，您的伺服器會收到一個已簽署的 `POST`，其中包含「transcript」（對話記錄）。您的伺服器需以允許或拒絕的「verdict」（判定）回應。本頁說明建置該伺服器所需的協定，包括請求與判定的結構描述、簽章驗證，以及運作合約。

若要啟用 Inference hooks 並將其指向您的端點，請參閱[設定 Inference hooks](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-configuration)。若要了解 Inference hooks 是什麼以及何時使用，請參閱 [Inference hooks 概覽](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks)。

## 完成第一次判定往返

最小可運作的整合是一個讀取每個請求並允許它的伺服器。執行以下其中一個伺服器，將其公開於一個公開的 `https://` URL（例如，放在您所控制主機上的 TLS 終止反向代理之後，而非反向通道服務；請參閱[接收請求](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint#receive-a-request)），然後請您的管理員[將其設為端點並測試連線](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-configuration)：**Test connection** 的結果會回報您的伺服器所傳回的允許判定。

<CodeGroup exclude="shell">
  ```python Python
  # 執行方式：python server.py
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


  class VerdictHandler(BaseHTTPRequestHandler):
      protocol_version = "HTTP/1.1"  # keep the connection open between verdicts

      def do_POST(self):
          # 讀完整個 body；逐字稿可能達數 MB。
          self.rfile.read(int(self.headers.get("Content-Length", 0)))
          verdict = b'{"action": "allow"}'
          self.send_response(200)
          self.send_header("Content-Type", "application/json")
          self.send_header("Content-Length", str(len(verdict)))
          self.end_headers()
          self.wfile.write(verdict)


  ThreadingHTTPServer(("", 8000), VerdictHandler).serve_forever()
  ```

  ```typescript TypeScript
  // 執行方式：node server.ts
  import { createServer } from "node:http";

  createServer((request, response) => {
    // 回應前先讀完請求主體；逐字稿可能達數 MB。
    request.resume();
    request.on("end", () => {
      response.writeHead(200, { "Content-Type": "application/json" });
      response.end('{"action": "allow"}');
    });
  }).listen(8000);
  ```

  ```csharp C#
  #:sdk Microsoft.NET.Sdk.Web
  #:property PublishAot=false
  // 執行方式：dotnet run server.cs

  var app = WebApplication.Create();

  app.MapPost("/{**path}", async (HttpRequest request) =>
  {
      // 讀完整個主體；逐字稿可能達數 MB。
      await request.Body.CopyToAsync(Stream.Null);
      return Results.Text("""{"action": "allow"}""", "application/json");
  });

  app.Run("http://0.0.0.0:8000");
  ```

  ```go Go
  // 執行方式：go run server.go
  package main

  import (
  	"io"
  	"log"
  	"net/http"
  )

  func main() {
  	http.HandleFunc("POST /", func(writer http.ResponseWriter, request *http.Request) {
  		// 讀完 body 以便重複使用連線；逐字稿可能達數 MB。
  		io.Copy(io.Discard, request.Body)
  		writer.Header().Set("Content-Type", "application/json")
  		writer.Write([]byte(`{"action": "allow"}`))
  	})
  	log.Fatal(http.ListenAndServe(":8000", nil))
  }
  ```

  ```java Java
  // 執行方式：java VerdictServer.java
  import com.sun.net.httpserver.HttpServer;

  void main() throws IOException {
      HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
      server.createContext("/", exchange -> {
          // 直接排空 body 而不緩衝；逐字稿可能達數 MB。
          exchange.getRequestBody().transferTo(OutputStream.nullOutputStream());
          byte[] verdict = "{\"action\": \"allow\"}".getBytes(StandardCharsets.UTF_8);
          exchange.getResponseHeaders().set("Content-Type", "application/json");
          exchange.sendResponseHeaders(200, verdict.length);
          try (OutputStream responseBody = exchange.getResponseBody()) {
              responseBody.write(verdict);
          }
      });
      server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
      server.start();
  }
  ```

  ```php PHP
  <?php
  // 執行方式：php -S 0.0.0.0:8000 server.php

  // 讀取完整個 body；逐字稿可能達數 MB。
  file_get_contents('php://input');

  http_response_code(200);
  header('Content-Type: application/json');
  echo '{"action": "allow"}';
  ```

  ```ruby Ruby
  # 在 Ruby 3.4 中，webrick 是一般的 gem：請執行 gem install webrick，或加入 gem "webrick"。
  # 執行方式：ruby server.rb
  require "webrick"

  server = WEBrick::HTTPServer.new(Port: 8000)
  server.mount_proc("/") do |request, response|
    request.body # Drain the body; transcripts can be megabytes.
    response.status = 200
    response["Content-Type"] = "application/json"
    response.body = '{"action": "allow"}'
  end
  server.start
  ```
</CodeGroup>

<Note>
  這些伺服器會接受所有請求，包括未簽署的請求。在開始強制執行之前，請先加入[簽章驗證](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint#verify-the-signature)。
</Note>

## 接收請求

Anthropic 會向您的管理員所設定的 URL 傳送 HTTPS `POST`。整個設定的 URL 就是端點：沒有固定的路徑後綴，因此您可以選擇任何適合您伺服器的路徑。

請將您的 AI 安全伺服器託管在 Anthropic 能夠連線的位置：一個使用連接埠 443 的 `https://` URL，位於可公開路由的主機上（私有、迴路及電信級 NAT 範圍會在連線時被拒絕），具備可通過公開 CA 信任存放區驗證的憑證，且回應時不使用重新導向。主機必須具有 IPv4 位址，即使主機同時具有 IPv6 位址，Anthropic 也會使用 IPv4 位址；主機為 `localhost` 或 IPv6 位址的 URL 會被拒絕。設定的 URL 必須是最終目的地。不支援反向通道主機（ngrok 及類似的通道服務）：Anthropic 的網路政策會封鎖它們。請將您的伺服器託管在您所控制的網域上。[設定 Inference hooks](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-configuration) 說明了您的管理員如何設定和測試該 URL。

每個請求都會攜帶以下固定標頭，以及您的管理員所設定的任何[自訂請求標頭](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-configuration)，並且在您的組織擁有簽署密鑰後，還會攜帶[驗證簽章](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint#verify-the-signature)中所述的 `webhook-*` 簽章標頭：

| 標頭                | 值                  |
| ----------------- | ------------------ |
| `Content-Type`    | `application/json` |
| `User-Agent`      | `anthropic-dlp/1`  |
| `Accept-Encoding` | `identity`         |

共有兩種掛鉤事件，以頂層的 `type` 欄位區分。「prompt frame」（提示訊框）會在每個受管控的推論請求開始推論之前傳送一次。「tool call frame」（工具呼叫訊框）會在模型回應包含工具呼叫時、任何工具呼叫執行之前傳送，僅適用於已開啟 **Validate tool calls** 的組織。無論哪種情況，Anthropic 都會等待，直到您的 AI 安全伺服器回應或判定逾時時間到期。

## 提示訊框

請求主體是一個包含以下欄位的 JSON 物件：

| 欄位           | 類型            | 說明                                                                                                                                                                                                                                                                                           |
| ------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`       | string        | 掛鉤事件：`"prompt"` 或 `"tool_call"`（請參閱[工具呼叫訊框](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint#the-tool-call-frame)）。未來將會推出其他事件類型，因此請妥善處理無法辨識的值（請參閱[向前相容性](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint#forward-compatibility)）。 |
| `request_id` | string        | 每個訊框的不透明識別碼，用於關聯。等同於 `webhook-id` 標頭。                                                                                                                                                                                                                                                        |
| `tenant_id`  | string 或 null | 請求所屬組織的不透明識別碼。                                                                                                                                                                                                                                                                               |
| `actor`      | object        | 請求所歸屬的主體，以 `type` 區分（目前唯一傳送的值為 `"user"`）：`id`（一個帶標記的識別碼，對同一帳戶的各請求保持穩定）和 `email_address`（若可取得）。`id` 和 `email_address` 都可能為 null。                                                                                                                                                              |
| `source`     | object        | 發起請求的應用程式：`application`（請參閱[來源值](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint#source-values)）。                                                                                                                                                              |
| `messages`   | array         | 截至推論時間點的對話記錄。請參閱[內容區塊](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint#content-blocks)。                                                                                                                                                                        |
| `session_id` | string 或 null | 不透明的對話識別碼（若存在）。請勿解析它。對於 Claude Code，這是一個盡力而為、由用戶端宣告的工作階段識別碼。                                                                                                                                                                                                                                 |
| `model`      | string 或 null | 此請求的公開模型識別碼（若可取得）。                                                                                                                                                                                                                                                                           |
| `metadata`   | object        | 保留的擴充對應表，鍵與值皆為字串，目前傳送時為空。請勿依賴其中的任何內容，並容許它不存在、存在，以及出現任何鍵。                                                                                                                                                                                                                                     |

請求主體範例：

```json
{
  "type": "prompt",
  "request_id": "req_abc123",
  "tenant_id": "11111111-1111-1111-1111-111111111111",
  "actor": {
    "type": "user",
    "id": "user_01AbCdEfGhIjKlMnOpQrStUv",
    "email_address": "alice@example.com"
  },
  "source": {
    "application": "claude-ai"
  },
  "session_id": "22222222-2222-2222-2222-222222222222",
  "model": "claude-sonnet-4-5",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Summarize the attached report."
        },
        {
          "type": "attachment",
          "file_name": "q2-report.pdf",
          "media_type": "application/pdf",
          "size_bytes": 48213,
          "text": "Q2 revenue grew 14% quarter over quarter..."
        }
      ]
    }
  ],
  "metadata": {}
}
```

### 內容區塊

`messages` 中的每個項目都有一個值為 `user` 或 `assistant` 的 `role`（工具結果出現在 `user` 角色下，與公開 Messages API 的內容模型一致），以及一個以 `type` 區分的區塊 `content` 陣列：

| 區塊 `type`     | 欄位                                                                                                                                                                                                                                     |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`        | `text`：文字內容。                                                                                                                                                                                                                           |
| `tool_use`    | `id`：對應工具結果所參照的識別碼。`tool_name`：工具的名稱。`input`：模型傳遞給工具的引數。`tool_info`：僅出現在工具呼叫訊框上（其他地方會省略，絕不為 `null`），是一個說明由誰執行或提供該工具的物件；請參閱[工具呼叫訊框](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint#the-tool-call-frame)。 |
| `tool_result` | `content`：工具的輸出文字，各部分以換行符號連接；圖片等二進位部分會以預留位置標記取代，且絕不傳送原始位元組。`is_error`：工具呼叫是否失敗。`tool_name`：工具的名稱，讓政策可以依據工具身分設定條件，而無需交叉參照先前的區塊。`tool_use_id`：對應 `tool_use` 區塊的 `id`。                                                                    |
| `attachment`  | `file_name`：原始檔案名稱或路徑。`media_type`：附件的媒體類型。`size_bytes`：原始檔案的大小。`text`：附件的文字內容（若可取得），例如擷取的文件文字、音訊逐字稿或連結中繼資料。絕不傳送原始附件位元組。                                                                                                               |

除了 `type`、`text` 區塊的 `text`，以及 `tool_result` 區塊的 `content` 和 `is_error` 之外，當值未知時，上述任何欄位都可能為 `null`；例如，圖片會以 `attachment` 區塊的形式送達，其 `file_name` 和 `text` 設為 `null`。

您無法辨識其 `type` 的區塊屬於向前相容的新增項目。它唯一保證的欄位是 `type`；您的政策可以檢查存在的任何其他欄位，但不得因為無法辨識的類型而拒絕請求。

### 對話記錄包含的內容

對話記錄是終端使用者所看到的對話，截至推論時間點為止。其中包含：

* 對話記錄文字
* 工具呼叫及其結果
* 擷取的附件文字
* 先前的回合

對話記錄絕不包含系統提示、工具定義、Anthropic 內部上下文、Claude 的隱藏推理或原始檔案位元組。

若某個回合的所有區塊都被排除，該回合會被完全省略。因此，請勿假設使用者與助理的回合會嚴格交替出現。

對話記錄會以未截斷的形式傳送，因此包含大型附件的長對話會產生很大的請求主體。實務上，模型的「context window」（上下文視窗）會讓主體維持在約 10 MB 以下，但協定允許最大 64 MiB。

許多常見的預設值遠小於此上限，例如 nginx 的 `client_max_body_size` 為 1 MB，Express 的 `express.json()` 為 100 kB。被拒絕的主體會被視為 webhook 失敗。因此，若失敗處理設定為 **Allow the request**，過大的提示會在未經檢查的情況下送達模型。

### 來源值

`source.application` 是一個開放字串，而非封閉的列舉。常見的值有 `claude-ai`、`claude-code` 和 `cowork`；[連線測試](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-configuration)和自動斷路器[復原檢查](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint#circuit-breaker)使用 `config-test`。可能會出現新的值，您的伺服器不得因為無法辨識的值而拒絕請求。

請將 `source.application` 視為建議性的路由中繼資料，而非信任邊界：請勿僅依據它做出攸關安全的政策決策。

## 工具呼叫訊框

當 Claude 產生工具呼叫時，Anthropic 會傳送一個列出這些呼叫的工具呼叫訊框。對 claude.ai 部分自有工具的呼叫可能會被省略，而若回應中的工具呼叫全部被省略，則不會產生工具呼叫訊框；請參閱[可用性](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks#availability)。一個判定涵蓋整個訊框：您無法允許部分工具呼叫而拒絕其他呼叫。此訊框會傳送到與提示訊框相同的端點，並使用相同的標頭、簽章和頂層欄位。由 Claude 在程式碼執行工具中執行的程式碼所發出的工具呼叫，也會以相同方式在執行前以個別的工具呼叫訊框傳送。

它與提示訊框有三點不同：

* `type` 為 `"tool_call"`。
* `messages` 只包含最新的訊息，也就是 Claude 剛產生的 `assistant` 訊息：任何 `text` 區塊，以及訊框所列每個工具呼叫各一個 `tool_use` 區塊，依模型產生的順序排列。較早的對話會被省略，因為在該次模型呼叫之前傳送的提示訊框已攜帶這些內容。請讀取 `messages` 的最後一個項目，因為協定日後可能會在其之前加入較早的訊息。
* 每個 `tool_use` 區塊都攜帶一個 `tool_info` 物件，說明由誰執行或提供該工具。

若有設定 `session_id`，兩種訊框上的值會相同。工具呼叫訊框有自己的 `request_id`，與提示訊框的一樣是不透明的。

`tool_info` 說明的是由誰執行或提供該工具，而非該工具能存取什麼。它屬於四種類別之一，以其 `type` 欄位區分，且每種類別都攜帶各自的欄位。Anthropic 會傳送以下類別中第一個符合該工具的類別。可能會出現新的類別：請接受您無法辨識的 `type`，並且對於此類類別僅依賴 `type`。

不適用的選用欄位會被省略，絕不為 `null`，因此 `tool_info` 可能只是 `{"type": "client"}`。`tool_name` 由定義該工具的一方選定，而伺服器的 `toolset_name` 由撰寫請求的一方選定，因此請勿將兩者視為信任邊界。

### 平台工具

平台工具是 Claude API 本身在處理請求時執行的工具，例如網頁搜尋或程式碼執行。

| 欄位             | 出現時機 | 說明                                                    |
| -------------- | ---- | ----------------------------------------------------- |
| `type`         | 一律   | `"platform"`                                          |
| `tool_type`    | 一律   | 工具的版本化類型，例如 `web_search_20250305`。請完全比對；請勿從中解析出名稱或日期。 |
| `toolset_name` | 選用   | 其所屬的工具群組。                                             |

### 應用程式工具

應用程式工具是發出請求的 Anthropic 應用程式本身所提供的工具，例如 claude.ai 的自有工具。

| 欄位             | 出現時機 | 說明              |
| -------------- | ---- | --------------- |
| `type`         | 一律   | `"application"` |
| `toolset_name` | 選用   | 其所屬的工具群組。       |

claude.ai 本身所提供的網頁搜尋工具的 `tool_use` 區塊：

```json
{
  "type": "tool_use",
  "id": "toolu_01LmNoPqRsTuVwXyZaBcDeFg",
  "tool_name": "web_search",
  "input": {
    "query": "comparable deals for unannounced 4.2B USD Acme Corp acquisition"
  },
  "tool_info": {
    "type": "application"
  }
}
```

claude.ai 並不一定將此工具命名為 `web_search`，因此請勿僅依名稱比對。`input` 一律包含搜尋查詢，也可能包含其他欄位。

### 第三方工具

第三方工具是位於 Anthropic 所知伺服器上的工具，例如 claude.ai 連接器，或請求在 `mcp_servers` 中指名的 MCP 伺服器。這並不代表 Anthropic 已審查過該伺服器。

| 欄位                | 出現時機            | 說明                                                                      |
| ----------------- | --------------- | ----------------------------------------------------------------------- |
| `type`            | 一律              | `"third_party"`                                                         |
| `toolset_name`    | 選用              | 請求為該伺服器指定的名稱。                                                           |
| `origin`          | 選用              | 伺服器 URL 的協定、主機及非預設連接埠。它可能是私有或本機位址，包括 `localhost`。`origin` 不存在代表未知，而非安全。 |
| `verified_origin` | 每當 `origin` 出現時 | 僅當 Anthropic 的伺服器自行連線到該伺服器時為 `true`。請將 `false` 視為 Anthropic 尚未確認的來源。    |

請求在 `mcp_servers` 中命名為 `crm` 的 MCP 伺服器上某個工具的 `tool_use` 區塊：

```json
{
  "type": "tool_use",
  "id": "toolu_01GhIjKlMnOpQrStUvWxYzAb",
  "tool_name": "crm_search",
  "input": {
    "query": "accounts renewing in Q4"
  },
  "tool_info": {
    "type": "third_party",
    "toolset_name": "crm",
    "origin": "https://mcp.crm.example.com",
    "verified_origin": true
  }
}
```

### 用戶端工具

用戶端工具是任何其他工具，通常是呼叫 Claude 的應用程式所執行的工具。由 Anthropic 定義但由呼叫端應用程式執行的工具（例如 bash 或電腦使用）也是用戶端工具，並在 `tool_type` 中攜帶其版本化類型。對請求未宣告之工具的呼叫也屬於 `"client"`。

Claude Code 直接從使用者電腦連線的 MCP 伺服器（例如本機 MCP 伺服器）上的工具是用戶端工具。其 `tool_info` 為 `{"type": "client"}`，而其 `tool_name` 是 Claude Code 為其指定的名稱，格式為 `mcp__<server>__<tool>`。

拒絕會在 Claude Code 執行呼叫之前將其停止。Claude Code 與本機伺服器之間的交換不會經過 Anthropic，因此您的 AI 安全伺服器只有在 Claude Code 將工具結果傳回時才會看到它。屆時其文字會出現在下一個提示訊框的 `tool_result` 區塊中。

| 欄位             | 出現時機 | 說明                                                                 |
| -------------- | ---- | ------------------------------------------------------------------ |
| `type`         | 一律   | `"client"`                                                         |
| `tool_type`    | 選用   | 當工具由 Anthropic 定義時，工具的版本化類型，例如 `bash_20250124`。請完全比對；請勿從中解析出名稱或日期。 |
| `toolset_name` | 選用   | 其所屬的工具群組，例如 `browser`。                                             |

呼叫端應用程式所宣告之工具的 `tool_use` 區塊：

```json
{
  "type": "tool_use",
  "id": "toolu_01AbCdEfGhIjKlMnOpQrStUv",
  "tool_name": "read_file",
  "input": {
    "path": "reports/q3.txt"
  },
  "tool_info": {
    "type": "client"
  }
}
```

bash 的 `tool_use` 區塊，該工具由 Anthropic 定義並由呼叫端應用程式執行：

```json
{
  "type": "tool_use",
  "id": "toolu_01HiJkLmNoPqRsTuVwXyZaBc",
  "tool_name": "bash",
  "input": {
    "command": "ls -la reports/"
  },
  "tool_info": {
    "type": "client",
    "tool_type": "bash_20250124"
  }
}
```

## 回傳判定

無論哪種結果，都請以 HTTP 200 和 JSON 判定主體回應；由 `action` 欄位區分。若要允許請求：

```json
{
  "action": "allow"
}
```

若要拒絕請求：

```json
{
  "action": "deny",
  "deny_reason": "This prompt appears to contain customer payment card data, which your organization's policy does not allow.",
  "reference_id": "scan_01HXPT4R9V"
}
```

| 欄位             | 限制                                               | 語意                                                                                                                                                                                     |
| -------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action`       | `"allow"` 或 `"deny"`；必填                          | `allow` 讓推論繼續進行；`deny` 拒絕推論。                                                                                                                                                           |
| `deny_reason`  | string 或 null；最多 500 個字元，超過的部分會被截斷               | 當 `action` 為 `deny` 時顯示給終端使用者；在 `allow` 時會被忽略。                                                                                                                                         |
| `reference_id` | string 或 null；最多 50 個字元，字元須來自 `[A-Za-z0-9._:/-]` | 您自己為此次評估設定的識別碼。它會記錄在該次拒絕的 `inference_hooks_request_denied` [合規活動](https://platform.claude.com/docs/zh-TW/manage-claude/compliance-activity-feed)中，且絕不會顯示給終端使用者。請保持其不透明：不含請求內容，也不含個人資料。 |

拒絕絕不會因為格式問題而被捨棄：過長的 `deny_reason` 會被截斷，格式錯誤的 `reference_id` 會被靜默捨棄，而 `action` 仍會被遵循。

反之則不成立。除了 HTTP 200 搭配可解析的判定之外，任何其他回應都是 webhook 失敗，此時會套用您組織的[失敗處理](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-configuration)，而非判定。特別是：

* 請勿以錯誤狀態表示拒絕。非 200 的回應是失敗，而非拒絕。
* 除了 `allow` 或 `deny` 之外的任何 `action` 值都會被視為 webhook 失敗。

Anthropic 最多讀取回應主體的 64 KiB，且主體必須未經壓縮。不會跟隨重新導向，且會忽略 cookie。判定主體中的未知欄位會被忽略，因此您可以在此處記錄的欄位之外回傳更豐富的物件。

工具呼叫訊框使用相同的判定主體，本節的每條規則都原封不動地適用於它。`allow` 讓工具呼叫執行，回應繼續進行；`deny` 會停止所有工具呼叫，並以與被拒絕提示相同的錯誤結束回應，包括您的 `deny_reason`。在第一個工具呼叫之前已傳遞的文字不會被收回。

## 驗證簽章

請求依照 [Standard Webhooks](https://www.standardwebhooks.com/) 規範進行簽署，並使用三個標頭。Anthropic 以小寫傳送標頭名稱，但代理伺服器可能會變更其大小寫，因此請以不區分大小寫的方式查找這些標頭。

| 標頭                  | 內容                                                                                                                                         |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `webhook-id`        | 此次傳遞的唯一識別碼，其值等於主體的 `request_id`。請將其用作冪等性金鑰，以及簽署酬載的第一個組成部分。                                                                                 |
| `webhook-timestamp` | 請求簽署時的 Unix 時間（以秒為單位），格式為十進位字串。若時間戳記與您伺服器時鐘的差距超過五分鐘（無論提前或落後），請拒絕該請求。                                                                       |
| `webhook-signature` | 一個或多個以空格分隔的 `v1,<base64>` 值，每個值都是對 `{webhook-id}.{webhook-timestamp}.{raw body bytes}` 計算的 HMAC-SHA256。只要任一值與您計算的結果相符，即可接受請求。比對時請使用常數時間比較。 |

有兩個細節造成了大多數的驗證錯誤：

* **驗證原始位元組。** 請在進行任何 JSON 解析或重新編碼之前，對收到的原始主體計算 HMAC。
* **使用標準 base64 解碼器解碼密鑰。** 簽署密鑰是 `whsec_` 前綴之後的值，以標準 base64 字母表（`+` 和 `/`）編碼，標頭中的簽章也是如此。若密鑰包含 `+` 或 `/`（大多數情況下都會包含），URL 安全解碼器會推導出錯誤的金鑰位元組。

當您的組織擁有簽署密鑰後，Anthropic 傳送的每個請求都會經過簽署，包括連線測試，因為設定流程會在第一次測試之前產生密鑰。由於[啟用 Inference hooks 需要密鑰](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-configuration)，請拒絕任何未簽署的請求。

唯一的例外是：若組織在密鑰成為必要條件之前就已啟用 Inference hooks，則在其管理員產生密鑰之前，會持續傳送未簽署的請求。請僅在管理員確認密鑰已存在之前接受未簽署的請求，確認之後即應拒絕。

[輪替密鑰](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-configuration#rotate-your-signing-secret)會立即切換，但在切換後約一分鐘內，仍可能收到以先前密鑰簽署的請求，此外還有已在傳輸中的請求。請讓您的 AI 安全伺服器在切換期間同時接受兩組密鑰的簽章，以免拒絕這些延遲抵達的請求。

下列範例是伺服器實作，因此沒有 shell 分頁，因為 AI 安全伺服器是長時間執行的 HTTPS 服務，而非一次性請求。每個範例都只使用該語言的標準函式庫。[Standard Webhooks](https://www.standardwebhooks.com/) 專案也為大多數語言發布了驗證函式庫。

<CodeGroup exclude="shell">
  ```python Python
  import base64
  import hashlib
  import hmac
  import time

  TOLERANCE_SECONDS = 300


  def verify(secret: str, headers: dict[str, str], body: bytes) -> bool:
      """Return True if the body was signed by Anthropic for this organization.

      Anthropic sends header names in lowercase, but proxies are free to
      re-case them, so normalize the lookup to lowercase.
      """
      lowercased = {name.lower(): value for name, value in headers.items()}
      try:
          message_id = lowercased["webhook-id"]
          timestamp = lowercased["webhook-timestamp"]
          signatures = lowercased["webhook-signature"]
      except KeyError:
          return False  # unsigned request: not from Anthropic

      try:
          signed_at = int(timestamp)
      except ValueError:
          return False
      if abs(time.time() - signed_at) > TOLERANCE_SECONDS:
          return False  # replayed, or the clocks disagree

      try:
          key = base64.b64decode(secret.removeprefix("whsec_"), validate=True)
      except ValueError:
          return False  # misconfigured secret: reject rather than crash

      payload = f"{message_id}.{timestamp}.".encode() + body
      expected = b"v1," + base64.b64encode(
          hmac.new(key, payload, hashlib.sha256).digest()
      )

      # 比較位元組：對 str 使用 compare_digest 時，若輸入含非 ASCII 字元會引發例外。
      return any(
          hmac.compare_digest(expected, candidate.encode())
          for candidate in signatures.split()
      )
  ```

  ```typescript TypeScript
  import { createHmac, timingSafeEqual } from "node:crypto";
  import type { IncomingHttpHeaders } from "node:http";

  const TOLERANCE_SECONDS = 300;

  /**
   * Returns true if the body was signed by Anthropic for this organization.
   *
   * Node lowercases incoming header names, matching how Anthropic sends
   * them, so look them up in lowercase.
   */
  export function verify(secret: string, headers: IncomingHttpHeaders, body: Buffer): boolean {
    const messageId = headers["webhook-id"];
    const timestamp = headers["webhook-timestamp"];
    const signatures = headers["webhook-signature"];
    if (
      typeof messageId !== "string" ||
      typeof timestamp !== "string" ||
      typeof signatures !== "string"
    ) {
      return false; // unsigned request: not from Anthropic
    }

    const signedAt = Number(timestamp);
    if (
      !Number.isFinite(signedAt) ||
      Math.abs(Date.now() / 1000 - signedAt) > TOLERANCE_SECONDS
    ) {
      return false; // replayed, or the clocks disagree
    }

    const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
    const payload = Buffer.concat([Buffer.from(`${messageId}.${timestamp}.`), body]);
    const expected = Buffer.from(
      "v1," + createHmac("sha256", key).update(payload).digest("base64")
    );

    return signatures.split(" ").some((candidate) => {
      const candidateBytes = Buffer.from(candidate);
      return (
        candidateBytes.length === expected.length && timingSafeEqual(candidateBytes, expected)
      );
    });
  }
  ```

  ```csharp C#
  using System.Security.Cryptography;
  using System.Text;

  static class InferenceHooks
  {
      private const int ToleranceSeconds = 300;

      /// <summary>
      /// 若本文是由 Anthropic 為此組織簽署，則傳回 true。
      /// Anthropic 以小寫傳送標頭名稱，但 proxy（代理伺服器）可自由
      /// 變更其大小寫，因此比對時不區分大小寫。
      /// </summary>
      public static bool Verify(string secret, IReadOnlyDictionary<string, string> headers, byte[] body)
      {
          // 若 proxy 傳來僅大小寫不同的重複名稱，TryAdd 會保留第一個值；
          // 複製建構函式則會因此擲回例外。
          var lookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
          foreach (var (name, value) in headers)
          {
              lookup.TryAdd(name, value);
          }

          if (!lookup.TryGetValue("webhook-id", out var messageId) ||
              !lookup.TryGetValue("webhook-timestamp", out var timestamp) ||
              !lookup.TryGetValue("webhook-signature", out var signatures))
          {
              return false; // unsigned request: not from Anthropic
          }

          if (!long.TryParse(timestamp, out var signedAt) ||
              Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - signedAt) > ToleranceSeconds)
          {
              return false; // replayed, or the clocks disagree
          }

          // 使用標準 base64 字母表：URL-safe 解碼器會推導出錯誤的金鑰位元組。
          var encodedKey = secret.StartsWith("whsec_") ? secret["whsec_".Length..] : secret;
          byte[] key;
          try
          {
              key = Convert.FromBase64String(encodedKey);
          }
          catch (FormatException)
          {
              return false; // misconfigured secret: reject rather than crash
          }

          byte[] payload = [.. Encoding.UTF8.GetBytes($"{messageId}.{timestamp}."), .. body];
          var expected = Encoding.UTF8.GetBytes(
              "v1," + Convert.ToBase64String(HMACSHA256.HashData(key, payload)));

          // FixedTimeEquals 為常數時間比較，長度不符時傳回 false。
          return signatures.Split(' ', StringSplitOptions.RemoveEmptyEntries).Any(candidate =>
              CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(candidate), expected));
      }
  }
  ```

  ```go Go
  package hooks

  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/base64"
  	"net/http"
  	"strconv"
  	"strings"
  	"time"
  )

  const toleranceSeconds = 300

  // verify 回報 body 是否由 Anthropic 為此組織簽署。
  // net/http 在查詢時會將標頭名稱正規化，因此大小寫不同的名稱仍能相符。
  func verify(secret string, header http.Header, body []byte) bool {
  	messageID := header.Get("webhook-id")
  	timestamp := header.Get("webhook-timestamp")
  	signatures := header.Get("webhook-signature")
  	if messageID == "" || timestamp == "" || signatures == "" {
  		return false // unsigned request: not from Anthropic
  	}

  	signedAt, err := strconv.ParseInt(timestamp, 10, 64)
  	if err != nil {
  		return false
  	}
  	age := time.Now().Unix() - signedAt
  	if age > toleranceSeconds || age < -toleranceSeconds {
  		return false // replayed, or the clocks disagree
  	}

  	// 使用標準 base64 字母表：URL-safe 解碼器會推導出錯誤的金鑰位元組。
  	key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
  	if err != nil {
  		return false
  	}

  	mac := hmac.New(sha256.New, key)
  	mac.Write([]byte(messageID + "." + timestamp + "."))
  	mac.Write(body)
  	expected := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil))

  	for _, candidate := range strings.Fields(signatures) {
  		if hmac.Equal([]byte(candidate), []byte(expected)) { // constant-time
  			return true
  		}
  	}
  	return false
  }
  ```

  ```java Java
  import java.nio.charset.StandardCharsets;
  import java.security.GeneralSecurityException;
  import java.security.MessageDigest;
  import java.time.Instant;
  import java.util.Base64;
  import java.util.HashMap;
  import java.util.Locale;
  import java.util.Map;
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;

  public final class InferenceHookVerifier {
      private static final long TOLERANCE_SECONDS = 300;

      /**
       * Returns true if the body was signed by Anthropic for this organization.
       *
       * <p>Anthropic sends header names in lowercase, but proxies are free to
       * re-case them, so normalize the lookup to lowercase.
       */
      public static boolean verify(String secret, Map<String, String> headers, byte[] body) {
          Map<String, String> lowercased = new HashMap<>();
          headers.forEach((name, value) -> lowercased.put(name.toLowerCase(Locale.ROOT), value));

          String messageId = lowercased.get("webhook-id");
          String timestamp = lowercased.get("webhook-timestamp");
          String signatures = lowercased.get("webhook-signature");
          if (messageId == null || timestamp == null || signatures == null) {
              return false; // unsigned request: not from Anthropic
          }

          long signedAt;
          try {
              signedAt = Long.parseLong(timestamp);
          } catch (NumberFormatException _) {
              return false;
          }
          if (Math.abs(Instant.now().getEpochSecond() - signedAt) > TOLERANCE_SECONDS) {
              return false; // replayed, or the clocks disagree
          }

          // 標準 base64 字母表：若使用 URL-safe 解碼器，會推導出錯誤的金鑰位元組。
          byte[] key;
          try {
              key = Base64.getDecoder().decode(
                      secret.startsWith("whsec_") ? secret.substring("whsec_".length()) : secret);
          } catch (IllegalArgumentException _) {
              return false; // misconfigured secret: reject rather than crash
          }

          byte[] expected;
          try {
              Mac mac = Mac.getInstance("HmacSHA256");
              mac.init(new SecretKeySpec(key, "HmacSHA256"));
              mac.update((messageId + "." + timestamp + ".").getBytes(StandardCharsets.UTF_8));
              expected = ("v1," + Base64.getEncoder().encodeToString(mac.doFinal(body)))
                      .getBytes(StandardCharsets.UTF_8);
          } catch (GeneralSecurityException impossible) {
              // 每個 JVM 都內建 HmacSHA256，因此執行時期絕不會觸發此情況。
              throw new IllegalStateException(impossible);
          }

          for (String candidate : signatures.split(" ")) {
              if (MessageDigest.isEqual(candidate.getBytes(StandardCharsets.UTF_8), expected)) {
                  return true; // MessageDigest.isEqual is constant-time
              }
          }
          return false;
      }
  }
  ```

  ```php PHP
  const TOLERANCE_SECONDS = 300;

  /**
   * Returns true if the body was signed by Anthropic for this organization.
   *
   * Anthropic sends header names in lowercase, but proxies are free to
   * re-case them, so normalize the lookup to lowercase.
   */
  function verify(string $secret, array $headers, string $body): bool
  {
      $lowercased = array_change_key_case($headers, CASE_LOWER);
      $messageId = $lowercased['webhook-id'] ?? null;
      $timestamp = $lowercased['webhook-timestamp'] ?? null;
      $signatures = $lowercased['webhook-signature'] ?? null;
      if ($messageId === null || $timestamp === null || $signatures === null) {
          return false; // unsigned request: not from Anthropic
      }

      $signedAt = filter_var($timestamp, FILTER_VALIDATE_INT);
      if ($signedAt === false || abs(time() - $signedAt) > TOLERANCE_SECONDS) {
          return false; // replayed, or the clocks disagree
      }

      // 標準 base64 字母表：若使用 URL-safe 解碼器，會推導出錯誤的金鑰位元組。
      $encodedKey = str_starts_with($secret, 'whsec_') ? substr($secret, strlen('whsec_')) : $secret;
      $key = base64_decode($encodedKey, strict: true);
      if ($key === false) {
          return false;
      }

      $payload = "{$messageId}.{$timestamp}." . $body;
      $expected = 'v1,' . base64_encode(hash_hmac('sha256', $payload, $key, binary: true));

      foreach (explode(' ', $signatures) as $candidate) {
          if (hash_equals($expected, $candidate)) { // constant-time
              return true;
          }
      }
      return false;
  }
  ```

  ```ruby Ruby
  # base64 在 Ruby 3.4 中是 bundled gem：由 Bundler 管理的應用程式需加入 gem "base64"。
  require "base64"
  require "openssl"

  TOLERANCE_SECONDS = 300

  # 若本文是由 Anthropic 為此組織簽署，則傳回 true。
  #
  # Anthropic 以小寫傳送標頭名稱，但代理伺服器可自由
  # 變更其大小寫，因此將查詢統一轉為小寫。
  def verify(secret, headers, body)
    lowercased = headers.transform_keys(&:downcase)
    message_id = lowercased["webhook-id"]
    timestamp = lowercased["webhook-timestamp"]
    signatures = lowercased["webhook-signature"]
    if message_id.nil? || timestamp.nil? || signatures.nil?
      return false # unsigned request: not from Anthropic
    end

    signed_at = Integer(timestamp, exception: false)
    if signed_at.nil? || (Time.now.to_i - signed_at).abs > TOLERANCE_SECONDS
      return false # replayed, or the clocks disagree
    end

    # 使用標準 base64 字母表：URL-safe 解碼器會推導出錯誤的金鑰位元組。
    begin
      key = Base64.strict_decode64(secret.delete_prefix("whsec_"))
    rescue ArgumentError
      return false # misconfigured secret: reject rather than crash
    end

    # 將本文分開傳入，使其編碼永遠不必與前綴的編碼相符。
    hmac = OpenSSL::HMAC.new(key, "SHA256")
    hmac.update("#{message_id}.#{timestamp}.")
    hmac.update(body)
    expected = "v1," + Base64.strict_encode64(hmac.digest)

    signatures.split(" ").any? do |candidate|
      # fixed_length_secure_compare 在長度不符時會引發例外，因此先檢查長度。
      candidate.bytesize == expected.bytesize &&
        OpenSSL.fixed_length_secure_compare(candidate, expected)
    end
  end
  ```
</CodeGroup>

## 運作語意

### 逾時與重試

您的管理員會設定介於 1 到 10,000ms 之間的判定逾時（預設為 5,000ms）。此時間預算涵蓋整個交換過程：連線、TLS 交握、請求和回應。相同的逾時也適用於工具呼叫訊框。

Anthropic 只會重試一次，在延遲 100ms 之後，且僅在連線嘗試失敗時進行。重試共用相同的逾時預算，並攜帶相同的 `webhook-id` 和相同的簽章。一旦您的 AI 安全伺服器已回應，該交換就絕不會被重試。

### Webhook 失敗

下列情況都屬於 webhook 失敗：

* 逾時
* 非 200 狀態碼（包括重新導向）
* 無法解析或過大的回應主體
* 無法連線的端點

Webhook 失敗絕不會變成拒絕。受影響的請求會被封鎖，或在未經檢查的情況下繼續進行，由您組織的[失敗處理](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-configuration)設定決定。

### 斷路器

可歸因於您 AI 安全伺服器的持續 webhook 失敗會觸發「circuit breaker」（斷路器），停止強制執行：Anthropic 會停止聯繫您的伺服器，且失敗處理會套用到每個請求。

從觸發後 10 分鐘開始，Anthropic 會檢查您的伺服器是否已復原：大約每分鐘最多一次，它會向您的伺服器傳送與 **Test connection** 相同的合成測試請求（`source.application` 為 `config-test`），該請求與其他請求一樣經過簽署，且不攜帶任何使用者內容。請正常回應它。有效的判定（允許或拒絕）會重設斷路器並恢復強制執行；webhook 失敗則會讓斷路器保持觸發狀態，檢查也會持續進行。管理員也可以隨時重設斷路器，而管理員的設定變更會停止自動檢查；請參閱[斷路器](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-configuration#circuit-breaker)。

提示訊框和工具呼叫訊框共用一個斷路器，兩者的失敗都會計入其中。

每次觸發都會在[活動摘要](https://platform.claude.com/docs/zh-TW/manage-claude/compliance-activity-feed)中記錄為一筆 `inference_hooks_circuit_breaker_tripped` 活動，每次觸發一筆活動。在斷路器觸發期間，不會記錄任何個別請求的 Inference hooks 活動，因此觸發活動是摘要中關於觸發期間的唯一記錄。

### 延遲

強制執行會將您 AI 安全伺服器的往返時間加到您組織中每個受管控請求的「latency」（延遲）上。開啟 **Validate tool calls** 時，產生工具呼叫訊框的回應也會等待其判定，在影子模式下亦然。請讓判定保持快速，並在將伺服器推廣到大型組織之前先進行負載測試。

### 來源 IP 位址

傳送至您 AI 安全伺服器的請求來自 `160.79.106.0/24`，此範圍屬於 Anthropic 公布的[輸出 IP 範圍](https://platform.claude.com/docs/zh-TW/api/ip-addresses)。請將此區塊加入允許清單，而非同一頁面上的輸入範圍，因為輸入範圍並未涵蓋此區塊。

加入允許清單可以縮小您伺服器的暴露面，但無法取代簽章驗證，因為此區塊承載的 Anthropic 輸出流量不僅限於 Inference hooks。

## 向前相容性

協定的擴充不會破壞正確撰寫的伺服器。您的伺服器必須忽略：

* 任一種訊框上的未知頂層欄位。
* `metadata` 中的未知鍵。
* 新的 `source.application` 值。
* 新的 `actor.type` 值。`actor` 是以 `type` 區分的聯集，目前唯一傳送的類別是 `"user"`；未來的類別僅保證 `type` 存在。
* 具有無法辨識之 `type` 的內容區塊。
* `role` 不是 `user` 或 `assistant` 的訊息，後續修訂版本可能會加入此類訊息。

絕不要因為無法辨識的區塊類型或欄位而拒絕請求；請讀取您已知的欄位並略過其餘部分。

未來將會推出其他掛鉤事件類型。新的事件類型是您的伺服器無法透過略過欄位來處理的新增項目：該請求仍然需要判定。當頂層 `type` 是您無法辨識的值時，請回傳允許判定，而非錯誤狀態；錯誤回應屬於 [webhook 失敗](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint#webhook-failures)，而持續的失敗會觸發[斷路器](https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-endpoint#circuit-breaker)。

## 設計您的整合

除了傳輸協定之外，正式環境的 AI 安全伺服器還需要做出幾項設計選擇。

**依據 `webhook-id` 去除重複。** `webhook-id` 標頭在每次傳遞中都是唯一的，其值等於主體的 `request_id`，且連線失敗時的重試會沿用此值，因此可作為冪等性金鑰使用。若您會記錄判定，請以此值作為記錄的鍵。

**記錄判定並關聯拒絕事件。** 請儲存您回傳的每個判定及其 `reference_id`。每次拒絕都會記錄為一筆 `inference_hooks_request_denied` 合規活動，其中包含您伺服器所回傳的 `reference_id`。如此一來，您就能將[活動摘要](https://platform.claude.com/docs/zh-TW/manage-claude/compliance-activity-feed)中的拒絕事件，與您自有系統中的對應記錄關聯起來。

**使用一律允許的伺服器進行封存。** 若要即時擷取對話記錄而不加以管控，請無條件回傳 `{"action": "allow"}`，並在回應之後保存框架。這是輪詢 [Compliance API](https://platform.claude.com/docs/zh-TW/manage-claude/compliance-api) 之外的推送式替代方案。先回應再保存，可讓您的往返時間不會落在使用者的關鍵路徑上。

**為終端使用者撰寫 `deny_reason`。** 當使用者的請求遭到封鎖時，他們看到的就是您回傳的文字，且超過 500 個字元的部分會被截斷。請告訴使用者需要修改什麼，例如應移除哪一類內容，而不是輸出只有您的團隊才能解讀的掃描器代碼。

## 後續步驟

<CardGroup cols={2}>
  <Card title="設定 Inference hooks" href="https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks-configuration">
    啟用 Inference hooks、連接並測試您的端點，以及控制強制執行、失敗處理和推出方式。
  </Card>

  <Card title="Inference hooks 概覽" href="https://platform.claude.com/docs/zh-TW/manage-claude/inference-hooks">
    了解 Inference hooks 是什麼、判定往返如何運作，以及何時使用。
  </Card>
</CardGroup>
