---
title: Inference hooks 통합 개발하기
url: https://platform.claude.com/docs/ko/manage-claude/inference-hooks-endpoint
description: 서명된 Inference hooks 요청을 수신하고, 이를 검증하며, 허용 또는 거부 판정을 반환하는 AI 보안 서버를 구축합니다.
---

<Note>
  Inference hooks는 베타 단계이며 Claude Enterprise 조직에서 사용할 수 있습니다. 베타 기간 동안 필드 이름, 요청 형태, 헤더가 변경될 수 있습니다.
</Note>

Inference hooks 통합은 AI 보안 서버, 즉 Anthropic이 호출하는 HTTPS 서비스입니다. 관리 대상인 각 요청에 대해, 여러분의 서버는 대화 트랜스크립트를 담은 서명된 `POST`를 수신하고 허용 또는 거부 "verdict"(판정)로 응답합니다. 이 페이지는 해당 서버를 구축하기 위한 프로토콜, 즉 요청 및 판정 스키마, 서명 검증, 운영 계약을 문서화합니다.

Inference hooks를 켜고 여러분의 엔드포인트를 가리키도록 설정하려면 [Inference hooks 구성하기](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-configuration)를 참조하세요. Inference hooks가 무엇이며 언제 사용해야 하는지 알아보려면 [Inference hooks 개요](https://platform.claude.com/docs/ko/manage-claude/inference-hooks)를 참조하세요.

## 첫 번째 판정 왕복 수행하기

가장 작은 동작 가능한 통합은 각 요청을 읽고 이를 허용하는 서버입니다. 다음 서버 중 하나를 실행하고, 공개 `https://` URL로 노출한 다음(예를 들어, 리버스 터널 서비스가 아닌 여러분이 제어하는 호스트에서 TLS 종료 리버스 프록시 뒤에 배치합니다. [요청 수신하기](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-endpoint#receive-a-request) 참조), 관리자에게 [이를 엔드포인트로 설정하고 연결을 테스트](https://platform.claude.com/docs/ko/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):
          # 본문을 모두 읽어 비웁니다. 트랜스크립트는 수 메가바이트일 수 있습니다.
          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) => {
    // 응답하기 전에 본문을 모두 읽어 비우세요. 트랜스크립트는 수 메가바이트에 달할 수 있습니다.
    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) =>
  {
      // 본문을 모두 읽어 비웁니다. 트랜스크립트는 수 메가바이트에 달할 수 있습니다.
      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) {
  		// 연결을 재사용할 수 있도록 본문을 비웁니다. 트랜스크립트는 수 메가바이트일 수 있습니다.
  		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 -> {
          // 본문을 버퍼링하지 않고 비웁니다. 트랜스크립트는 수 메가바이트에 달할 수 있습니다.
          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

  // 본문을 모두 읽어 비웁니다. 트랜스크립트는 수 메가바이트일 수 있습니다.
  file_get_contents('php://input');

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

  ```ruby Ruby
  # webrick은 Ruby 3.4에서 일반 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/ko/manage-claude/inference-hooks-endpoint#verify-the-signature)을 추가하세요.
</Note>

## 요청 수신하기

Anthropic은 관리자가 구성한 URL로 HTTPS `POST`를 전송합니다. 구성된 URL 전체가 엔드포인트입니다. 고정된 경로 접미사가 없으므로 여러분의 서버에 맞는 어떤 경로든 선택하세요.

AI 보안 서버는 Anthropic이 접근할 수 있는 곳에 호스팅하세요. 즉, 공개적으로 라우팅 가능한 호스트(사설, 루프백 및 캐리어급 NAT 대역은 연결 시점에 거부됩니다)의 443 포트에 있는 `https://` URL이어야 하며, 공개 CA 신뢰 저장소로 검증되는 인증서를 사용하고, 리디렉션 없이 응답해야 합니다. 호스트에는 IPv4 주소가 있어야 하며, 호스트에 IPv6 주소가 있더라도 Anthropic은 IPv4 주소를 사용합니다. 호스트가 `localhost`이거나 IPv6 주소인 URL은 거부됩니다. 구성된 URL은 최종 목적지여야 합니다. 리버스 터널 호스트(ngrok 및 유사한 터널 서비스)는 지원되지 않습니다. Anthropic의 네트워크 정책이 이를 차단합니다. 직접 관리하는 도메인에 서버를 호스팅하세요. 관리자가 URL을 설정하고 테스트하는 방법은 [Inference hooks 구성](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-configuration)에서 다룹니다.

모든 요청은 다음의 고정 헤더를 포함하며, 관리자가 구성한 [사용자 지정 요청 헤더](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-configuration)와, 조직에 서명 시크릿이 생긴 이후에는 [서명 검증하기](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-endpoint#verify-the-signature)에 설명된 `webhook-*` 서명 헤더도 함께 포함합니다.

| 헤더                | 값                  |
| ----------------- | ------------------ |
| `Content-Type`    | `application/json` |
| `User-Agent`      | `anthropic-dlp/1`  |
| `Accept-Encoding` | `identity`         |

훅 이벤트는 두 가지이며, 최상위 `type` 필드로 구분됩니다. 프롬프트 프레임은 관리 대상 추론 요청마다 추론이 시작되기 전에 한 번 전송됩니다. 도구 호출 프레임은 **Validate tool calls**가 켜진 조직에서 모델 응답에 도구 호출이 포함될 때, 그중 어느 것도 실행되기 전에 전송됩니다. 어느 경우든 Anthropic은 AI 보안 서버가 응답하거나 판정 타임아웃이 경과할 때까지 기다립니다.

## 프롬프트 프레임

요청 본문은 다음 필드를 가진 JSON 객체입니다.

| 필드           | 타입             | 설명                                                                                                                                                                                                                                                                                                               |
| ------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`       | string         | 훅 이벤트: `"prompt"` 또는 `"tool_call"`([도구 호출 프레임](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-endpoint#the-tool-call-frame) 참조). 향후 다른 이벤트 타입이 도입될 예정이므로 인식되지 않는 값은 적절히 처리하세요([상위 호환성](https://platform.claude.com/docs/ko/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`([Source 값](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-endpoint#source-values) 참조).                                                                                                                                                                               |
| `messages`   | array          | 추론 시점까지의 대화 트랜스크립트. [콘텐츠 블록](https://platform.claude.com/docs/ko/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`(도구 결과는 공개 Messages API 콘텐츠 모델과 일치하게 `user` 역할 아래에 나타납니다)과 `type`으로 구별되는 블록의 `content` 배열을 가집니다.

| 블록 `type`     | 필드                                                                                                                                                                                                                                                                                   |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `text`        | `text`: 텍스트 콘텐츠.                                                                                                                                                                                                                                                                     |
| `tool_use`    | `id`: 일치하는 도구 결과가 참조하는 식별자. `tool_name`: 도구의 이름. `input`: 모델이 도구에 전달한 인수. `tool_info`: 도구 호출 프레임에만 있으며(그 외에는 생략되고 `null`이 되지 않음), 도구를 누가 실행하거나 제공하는지 나타내는 객체입니다. [도구 호출 프레임](https://platform.claude.com/docs/ko/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`일 수 있습니다. 예를 들어 이미지는 `file_name`과 `text`가 `null`로 설정된 `attachment` 블록으로 전달됩니다.

인식할 수 없는 `type`의 블록은 상위 호환 추가 사항입니다. 보장되는 유일한 필드는 `type`입니다. 여러분의 정책은 존재하는 다른 필드를 검사할 수 있지만, 인식할 수 없는 타입 때문에 요청을 거부해서는 안 됩니다.

### 트랜스크립트에 포함되는 내용

트랜스크립트는 추론 시점까지 최종 사용자가 보는 그대로의 대화입니다. 트랜스크립트 텍스트, 도구 호출과 그 결과, 추출된 첨부 파일 텍스트, 이전 턴이 포함됩니다. 시스템 프롬프트, 도구 정의, Anthropic 내부 컨텍스트, Claude의 숨겨진 추론, 원시 파일 바이트는 절대 포함되지 않습니다.

모든 블록이 제외된 턴은 완전히 생략되므로, 엄격한 user와 assistant 교대를 가정하지 마세요.

대화 기록은 잘리지 않은 상태로 전송되므로, 큰 첨부 파일이 포함된 긴 대화는 큰 요청 본문을 생성합니다. 실제로는 모델의 "context window"(컨텍스트 윈도우)로 인해 본문이 약 10 MB 미만으로 유지되지만, 프로토콜은 최대 64 MiB까지 허용합니다. nginx의 `client_max_body_size`(1 MB)와 Express의 `express.json()`(100 kB)을 비롯한 여러 일반적인 기본값은 이보다 훨씬 작으며, 거부된 본문은 웹훅 실패로 간주됩니다. 따라서 **Allow the request** 실패 처리 설정에서는 크기가 초과된 프롬프트가 검사 없이 모델에 도달하게 됩니다.

### Source 값

`source.application`은 닫힌 열거형이 아닌 열린 문자열입니다. 일반적인 값은 `claude-ai`, `claude-code`, `cowork`이며, [연결 테스트](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-configuration)와 자동 서킷 브레이커 [복구 확인](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-endpoint#circuit-breaker)에서는 `config-test`를 사용합니다. 새로운 값이 나타날 수 있으며, 서버는 인식할 수 없는 값 때문에 요청을 거부해서는 안 됩니다.

`source.application`을 신뢰 경계가 아닌 참고용 라우팅 메타데이터로 취급하세요. 보안에 중요한 정책 결정을 이것에만 의존하지 마세요.

## 도구 호출 프레임

Claude가 도구 호출을 생성하면 Anthropic은 이를 나열한 도구 호출 프레임 하나를 보냅니다. claude.ai 자체 도구 중 일부에 대한 호출은 생략될 수 있으며, 도구 호출이 모두 생략된 응답은 도구 호출 프레임을 생성하지 않습니다. [가용성](https://platform.claude.com/docs/ko/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` | 선택    | 도구가 속한 도구 그룹입니다.                                                                |

### 애플리케이션 도구

애플리케이션 도구는 claude.ai 자체 도구처럼 요청을 보내는 Anthropic 애플리케이션이 직접 제공하는 도구입니다.

| 필드             | 존재 여부 | 설명               |
| -------------- | ----- | ---------------- |
| `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`에는 항상 검색 쿼리가 포함되며 다른 필드도 포함될 수 있습니다.

### 서드파티 도구

서드파티 도구는 claude.ai 커넥터나 요청이 `mcp_servers`에 지정한 MCP 서버처럼 Anthropic이 알고 있는 서버에 있는 도구입니다. 이것이 Anthropic이 해당 서버를 검증했다는 의미는 아닙니다.

| 필드                | 존재 여부            | 설명                                                                                                                  |
| ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- |
| `type`            | 항상               | `"third_party"`                                                                                                     |
| `toolset_name`    | 선택               | 요청이 서버에 부여한 이름입니다.                                                                                                  |
| `origin`          | 선택               | 서버 URL의 스킴, 호스트 및 기본값이 아닌 포트입니다. `localhost`를 포함하여 사설 또는 로컬 주소일 수 있습니다. `origin`이 없다는 것은 안전하다는 뜻이 아니라 알 수 없다는 뜻입니다. |
| `verified_origin` | `origin`이 있을 때마다 | Anthropic의 서버가 직접 해당 서버에 연결하는 경우에만 `true`입니다. `false`는 Anthropic이 확인하지 않은 origin으로 취급하세요.                           |

요청이 `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를 호출하는 애플리케이션이 실행하는 도구입니다. bash나 컴퓨터 사용처럼 Anthropic이 정의하지만 호출하는 애플리케이션이 실행하는 도구도 클라이언트 도구이며, `tool_type`에 버전이 지정된 타입을 포함합니다. 요청이 선언하지 않은 도구에 대한 호출도 `"client"`입니다.

로컬 MCP 서버처럼 Claude Code가 사용자의 컴퓨터에서 직접 연결하는 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"
  }
}
```

Anthropic이 정의하고 호출하는 애플리케이션이 실행하는 bash에 대한 `tool_use` 블록:

```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; `[A-Za-z0-9._:/-]`에서 최대 50자 | 이 평가에 대한 여러분 자체의 식별자. 거부의 `inference_hooks_request_denied` [컴플라이언스 활동](https://platform.claude.com/docs/ko/manage-claude/compliance-activity-feed)에 기록되며 최종 사용자에게는 절대 표시되지 않습니다. 불투명하게 유지하세요. 요청 콘텐츠나 개인 데이터를 포함하지 마세요. |

거부는 형식 문제로 인해 절대 폐기되지 않습니다. 크기가 초과된 `deny_reason`은 잘리고, 형식이 잘못된 `reference_id`는 조용히 삭제되며, `action`은 여전히 존중됩니다.

그 반대는 성립하지 않습니다. 파싱 가능한 판정이 있는 HTTP 200 이외의 모든 것은 웹훅 실패이며, 판정 대신 조직의 [실패 처리](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-configuration)가 적용됩니다. 특히:

* 오류 상태로 거부를 신호하지 마세요. 200이 아닌 응답은 거부가 아니라 실패입니다.
* `allow` 또는 `deny` 이외의 모든 `action` 값은 웹훅 실패로 취급됩니다.

Anthropic은 응답 본문을 최대 64 KiB까지 읽으며, 본문은 압축되지 않아야 합니다. 리디렉션은 따르지 않으며, 쿠키는 무시됩니다. 판정 본문의 알 수 없는 필드는 무시되므로, 여기에 문서화된 필드와 함께 더 풍부한 객체를 반환할 수 있습니다.

도구 호출 프레임도 동일한 판정 본문을 사용하며, 이 섹션의 모든 규칙이 그대로 적용됩니다. `allow`는 도구 호출을 실행시키고 응답이 계속되며, `deny`는 모든 도구 호출을 중단시키고 `deny_reason`을 포함하여 거부된 프롬프트와 동일한 오류로 응답을 종료합니다. 첫 번째 도구 호출 전에 전달된 텍스트는 회수되지 않습니다.

## 서명 검증하기

요청은 세 개의 헤더를 사용하여 [Standard Webhooks](https://www.standardwebhooks.com/) 사양에 따라 서명됩니다. Anthropic은 헤더 이름을 소문자로 전송하며, 프록시가 대소문자를 자유롭게 바꿀 수 있으므로 대소문자를 구분하지 않고 조회하세요.

| 헤더                  | 내용                                                                                                                                                          |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `webhook-id`        | 이 전달에 대한 고유 식별자. 본문의 `request_id`와 동일합니다. 멱등성 키로, 그리고 서명된 페이로드의 첫 번째 구성 요소로 사용하세요.                                                                          |
| `webhook-timestamp` | 요청이 서명된 시점의 초 단위 Unix 시간으로, 십진수 문자열입니다. 어느 방향이든 서버 시계와 5분 이상 차이 나는 타임스탬프는 거부하세요.                                                                            |
| `webhook-signature` | 공백으로 구분된 하나 이상의 `v1,<base64>` 값으로, 각각 `{webhook-id}.{webhook-timestamp}.{raw body bytes}`에 대한 HMAC-SHA256입니다. 상수 시간 비교를 사용하여 어느 값이든 여러분의 값과 일치하면 요청을 수락하세요. |

대부분의 검증 버그는 두 가지 세부 사항에서 발생합니다.

* **원시 바이트를 검증하세요.** JSON 파싱이나 재인코딩 전에, 수신한 그대로의 본문에 대해 HMAC을 계산하세요.
* **표준 base64 디코더로 시크릿을 디코딩하세요.** 서명 시크릿은 `whsec_` 접두사 뒤의 값으로, 헤더의 서명과 마찬가지로 표준 base64 알파벳(`+`와 `/`)으로 인코딩되어 있습니다. URL-safe 디코더는 시크릿에 `+`나 `/`가 포함될 때마다 잘못된 키 바이트를 도출하는데, 대부분의 경우가 그렇습니다.

조직에 서명 시크릿이 생기면, 설정 흐름에서 첫 번째 테스트 전에 시크릿을 생성하므로 연결 테스트를 포함하여 Anthropic이 보내는 모든 요청이 서명됩니다. [Inference hooks를 활성화하려면 시크릿이 필요](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-configuration)하므로, 서명 없이 도착하는 요청은 모두 거부하세요. 한 가지 예외가 있습니다. 시크릿이 필수가 되기 전에 Inference hooks를 활성화한 조직은 관리자가 시크릿을 생성할 때까지 서명되지 않은 요청을 계속 전송합니다. 관리자가 시크릿이 존재함을 확인할 때까지만 서명되지 않은 요청을 수락하고, 그 이후에는 거부하세요.

[시크릿 교체](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-configuration#rotate-your-signing-secret)는 즉시 전환되지만, 이전 시크릿으로 서명된 요청이 이후 약 1분 동안, 그리고 이미 전송 중인 것들이 여전히 도착할 수 있습니다. 이러한 지연 도착 요청이 거부되지 않도록 전환 기간 동안 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()
      )

      # 바이트로 비교: compare_digest는 str에 비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은 헤더 이름을 소문자로 보내지만 프록시가 자유롭게
      /// 대소문자를 바꿀 수 있으므로 대소문자를 구분하지 않고 비교합니다.
      /// </summary>
      public static bool Verify(string secret, IReadOnlyDictionary<string, string> headers, byte[] body)
      {
          // 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에서 번들 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 보안 서버가 응답한 후에는 교환이 절대 재시도되지 않습니다.

### 웹훅 실패

타임아웃, 200이 아닌 상태(리디렉션 포함), 파싱할 수 없거나 크기가 초과된 응답 본문, 도달할 수 없는 엔드포인트는 모두 웹훅 실패입니다. 웹훅 실패는 절대 거부가 되지 않습니다. 대신 조직의 [실패 처리](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-configuration) 설정이 영향받은 요청을 차단할지 검사 없이 진행할지 결정합니다.

### 서킷 브레이커

AI 보안 서버에 기인하는 지속적인 웹훅 실패는 시행을 중지하는 "circuit breaker"(서킷 브레이커)를 작동시킵니다. Anthropic은 여러분의 서버에 연락하는 것을 중지하고, 모든 요청에 실패 처리가 적용됩니다.

작동 후 10분이 지나면 Anthropic은 서버가 복구되었는지 확인합니다. 최대 약 1분에 한 번씩, **Test connection**이 보내는 것과 동일한 합성 테스트 요청(`source.application`이 `config-test`)을 서버에 전송하며, 이 요청은 다른 요청과 마찬가지로 서명되고 사용자 콘텐츠를 포함하지 않습니다. 평소처럼 응답하세요. 허용이든 거부든 유효한 판정이 반환되면 브레이커가 재설정되고 적용이 재개됩니다. 웹훅 실패가 발생하면 브레이커는 작동 상태로 유지되고 확인이 계속됩니다. 관리자는 언제든지 브레이커를 재설정할 수도 있으며, 관리자가 구성을 변경하면 자동 확인이 중단됩니다. [서킷 브레이커](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-configuration#circuit-breaker)를 참조하세요.

프롬프트 프레임과 도구 호출 프레임은 하나의 서킷 브레이커를 공유하며, 어느 쪽의 실패든 이에 합산됩니다.

각 작동은 [Activity Feed](https://platform.claude.com/docs/ko/manage-claude/compliance-activity-feed)에 `inference_hooks_circuit_breaker_tripped` 활동으로 기록되며, 작동당 하나의 활동이 기록됩니다. 브레이커가 작동된 동안에는 요청별 Inference hooks 활동이 기록되지 않으므로, 작동 활동이 작동된 기간에 대한 피드의 유일한 기록입니다.

### 지연 시간

적용은 조직 내 모든 관리 대상 요청의 "latency"(지연 시간)에 AI 보안 서버의 왕복 시간을 추가합니다. **Validate tool calls**가 켜져 있으면, 도구 호출 프레임을 생성하는 응답도 섀도 모드를 포함하여 해당 판정을 기다립니다. 판정을 빠르게 유지하고, 대규모 조직에 배포하기 전에 서버의 부하 테스트를 수행하세요.

### 소스 IP 주소

AI 보안 서버로의 요청은 Anthropic이 게시한 [아웃바운드 IP 범위](https://platform.claude.com/docs/ko/api/ip-addresses)의 일부인 `160.79.106.0/24`에서 발신됩니다. 같은 페이지의 인바운드 범위가 아닌 해당 블록을 허용 목록에 추가하세요. 인바운드 범위는 이를 포함하지 않습니다. 허용 목록은 서버의 노출을 줄이지만 서명 검증을 대체하지는 않습니다. 해당 블록은 Inference hooks 외의 Anthropic 이그레스 트래픽도 전달합니다.

## 상위 호환성

프로토콜은 올바르게 작성된 서버를 깨뜨리지 않고 성장합니다. 여러분의 서버는 다음을 무시해야 합니다.

* 두 프레임 모두의 알 수 없는 최상위 필드.
* `metadata`의 알 수 없는 키.
* 새로운 `source.application` 값.
* 새로운 `actor.type` 값. `actor`는 `type`으로 구별되는 유니온이며, `"user"`가 현재 전송되는 유일한 종류입니다. 향후 종류는 `type`이 존재한다는 것만 보장합니다.
* 인식할 수 없는 `type`의 콘텐츠 블록.
* 이후 개정에서 추가될 수 있는, `user` 또는 `assistant` 이외의 `role`을 가진 메시지.

인식할 수 없는 블록 타입이나 필드 때문에 요청을 절대 거부하지 마세요. 아는 필드를 읽고 나머지는 건너뛰세요.

향후 다른 훅 이벤트 타입이 도입될 예정입니다. 새로운 이벤트 타입은 필드를 건너뛰는 것으로 서버가 처리할 수 없는 추가 사항입니다. 요청에는 여전히 판정이 필요합니다. 최상위 `type`이 인식할 수 없는 값일 때는 오류 상태가 아닌 허용 판정을 반환하세요. 오류 응답은 [웹훅 실패](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-endpoint#webhook-failures)이며, 지속적인 실패는 [서킷 브레이커](https://platform.claude.com/docs/ko/manage-claude/inference-hooks-endpoint#circuit-breaker)를 작동시킵니다.

## 통합 설계하기

프로덕션 AI 보안 서버는 와이어 프로토콜 외에도 몇 가지 설계 선택을 합니다.

**`webhook-id`로 중복 제거하세요.** `webhook-id` 헤더는 전달마다 고유하고 본문의 `request_id`와 동일하며, 연결 실패 재시도는 이를 재사용하므로 멱등성 키로 작동합니다. 판정을 기록한다면 이를 키로 사용하세요.

**판정을 기록하고 거부를 조인하세요.** 반환하는 각 판정을 `reference_id`와 함께 저장하세요. 모든 거부는 서버가 반환한 `reference_id`를 포함하는 `inference_hooks_request_denied` 컴플라이언스 활동으로 기록되므로, [Activity Feed](https://platform.claude.com/docs/ko/manage-claude/compliance-activity-feed)의 거부를 여러분 자체 시스템의 일치하는 기록과 조인할 수 있습니다.

**항상 허용하는 서버로 아카이브하세요.** 트랜스크립트를 감시하지 않고 실시간으로 캡처하려면 무조건 `{"action": "allow"}`를 반환하고 응답 후 프레임을 영속화하세요. 이는 [Compliance API](https://platform.claude.com/docs/ko/manage-claude/compliance-api)를 폴링하는 것에 대한 푸시 기반 대안이며, 영속화하기 전에 응답하면 왕복 시간이 사용자의 임계 경로에서 벗어납니다.

**최종 사용자를 위해 `deny_reason`을 작성하세요.** 반환하는 텍스트는 요청이 차단될 때 사용자가 보는 것으로, 500자에서 잘립니다. 여러분의 팀만 해석할 수 있는 스캐너 코드를 내보내는 대신, 어떤 종류의 콘텐츠를 제거해야 하는지와 같이 무엇을 변경해야 하는지 알려주세요.

## 다음 단계

<CardGroup cols={2}>
  <Card title="Inference hooks 구성하기" href="https://platform.claude.com/docs/ko/manage-claude/inference-hooks-configuration">
    Inference hooks를 활성화하고, 엔드포인트를 연결 및 테스트하며, 시행, 실패 처리, 배포를 제어합니다.
  </Card>

  <Card title="Inference hooks 개요" href="https://platform.claude.com/docs/ko/manage-claude/inference-hooks">
    Inference hooks가 무엇인지, 판정 왕복이 어떻게 작동하는지, 언제 사용해야 하는지 알아봅니다.
  </Card>
</CardGroup>
