إنتقل إلى المحتوى الرئيسي

Inference Hooks: inline DLP for Claude Enterprise

متقدّم
What you'll learn
  • What Inference Hooks actually are — an HTTPS POST from Anthropic to a server you run, not a WebSocket and not an on-device agent
  • The prompt frame schema — exactly what your AI security server sees (and what it never sees: system prompts, hidden reasoning, raw bytes)
  • The verdict JSON: allow, deny with deny_reason, and why there is deliberately no redact action today
  • The signing model — Standard Webhooks HMAC-SHA256, the two verification bugs that catch every first integration, and the whsec_ secret format
  • The three operational levers that decide whether users get blocked or the model gets uninspected traffic: verdict timeout, failure handling, and the circuit breaker
  • A rollout playbook that doesn't blow up on day one — shadow mode → percentage rollout → role exclusions → enforce, in that order

Announced August 5, 2026, Inference Hooks are Anthropic's first-party answer to the question every security team asks after they roll out a Claude Enterprise seat: how do I stop a prompt with regulated data from ever reaching the model? The answer, until now, was a corporate proxy that intercepted TLS traffic to claude.ai — brittle, incomplete, and blind to the Claude Code CLI. Inference Hooks moves the enforcement point inside Anthropic's perimeter: for every governed prompt, Anthropic pauses inference, POSTs the transcript to a server your organization operates, and waits for an allow or deny before the model sees anything.

The one paragraph version

Your organization stands up an HTTPS endpoint. Anthropic sends it every governed prompt as a signed POST (Standard Webhooks HMAC-SHA256). Your server returns {"action": "allow"} and inference proceeds, or {"action": "deny", "deny_reason": "..."} and the user sees the reason and never hits the model. The endpoint covers Claude Enterprise chat, Claude Code, and Cowork with one configuration. If your server times out or 500s, your failure handling setting decides whether the request blocks or proceeds uninspected. Roll it out gradually with shadow mode + percentage rollout + role exclusions before you flip Enforce verdicts on.

Inference Hooks vs the Compliance API

Both exist for the same audience — Claude Enterprise security, legal, and compliance teams — but they operate at opposite ends of the request lifecycle.

Inference HooksCompliance API
WhenInline, before inference runsAfter the fact
What it doesAllows or denies each governed request in real timeRetrieves activity, chats, files, projects, users for audit and export
DirectionAnthropic → your serverYou → Anthropic
Use it toStop a leakProve what happened

Most enterprises will run both. Hooks are the tripwire; the Compliance API is the audit log.

How the verdict round trip works

Guided walkthrough1 of 5
  1. That's claude.ai chat, Claude Code (web, desktop, CLI), or Claude Cowork. Ancillary requests like conversation-title generation are NOT sent. Voice mode is out of scope for the beta.

The whole point of running on Anthropic's servers, not on user devices, is uniformity: one config, one server, and every governed request on every surface is inspected the same way. There's nothing to install on employee laptops, and there's no per-app integration to keep in sync.

The prompt frame

Every request is a JSON body with these top-level fields:

FieldTypeDescription
typestringAlways "prompt" today. New event types will appear — return allow on unrecognized values so you don't trip the circuit breaker.
request_idstringOpaque per-inference-call identifier. Equals the webhook-id header — use it as your idempotency key.
tenant_idstring | nullOpaque identifier for the org.
actorobjectDiscriminated on type ("user" is the only value today). Carries a tagged id stable across a user's requests and email_address when available. Both fields can be null.
sourceobject{"application": "..."}. Known values: claude-ai, claude-code, config-test (used by the admin "Test connection" button). Open enum — new values will appear.
session_idstring | nullOpaque conversation identifier. Don't parse it. Best-effort for Claude Code.
modelstring | nullPublic model identifier for this request when available.
messagesarrayThe conversation transcript up to the point of inference — see Content blocks.
metadataobjectReserved extension map. Empty today. Tolerate keys you don't know.

A minimal live request looks like this:

{
"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-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": {}
}

Content blocks

Each messages[].content[] entry has a type and matches the public Messages API content model. Tool results appear under the user role.

Block typeFields
texttext
tool_useid, tool_name, input
tool_resultcontent (text, joined with newlines; binary parts are placeholder markers), is_error, tool_name, tool_use_id
attachmentfile_name, media_type, size_bytes, text (extracted text, transcript, or link metadata)

What the transcript never contains

This is the part that trips privacy reviews.

  • No system prompts. Anthropic's, yours (via projects/skills), or the model's constitution — none of it is sent.
  • No hidden reasoning. Claude's extended-thinking chain is not part of the transcript your server sees.
  • No tool definitions. Only the calls and their results.
  • No raw bytes. Files and images are represented by metadata and extracted text. Image-only content (a screenshot of a document) will not be inspected.
  • No Anthropic-internal context or trust boundaries.

The transcript is the conversation as the end user sees it, plus tool traces. A block or turn whose contents are all excluded is dropped entirely, so don't assume strict user/assistant alternation when you parse.

One size gotcha

Transcripts are sent untruncated up to a 10 MB ceiling. Common defaults are much smaller — nginx client_max_body_size is 1 MB, Express express.json() is 100 kB, most PaaS reverse proxies cap at a few MB. A body your server rejects is a webhook failure, which under Allow the request failure handling means the oversized prompt reaches the model uninspected. Raise your body limits before you enforce.

The verdict schema

Respond with HTTP 200 for both outcomes. The action field discriminates.

Allow:

{ "action": "allow" }

Deny:

{
"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"
}
FieldType & limitSemantics
action"allow" or "deny"; requiredallow lets inference proceed. deny rejects the request.
deny_reasonstring or null; at most 500 chars, longer values truncatedShown to the end user, appended to the standing message your admin configured. Write it for the user — tell them what to change, not what your scanner rule was called.
reference_idstring or null; at most 50 chars from [A-Za-z0-9._:/-]Your own identifier for this evaluation. Recorded on the denial's inference_hooks_request_denied Activity Feed entry, never shown to the end user. Keep it opaque — no request content, no personal data.

Why there is no redact action

The verdict is deliberately binary. Anthropic could have added {"action": "redact", "rewritten_prompt": "..."} and let your DLP server sanitize the transcript in flight — but that would mean Anthropic ships whatever your box returns to the model, on your organization's authority. The design keeps that trust boundary sharp: your server evaluates content, it doesn't author it. If you need redaction, do it in the client before the user hits send.

A deny is never discarded over formatting

An oversized deny_reason is truncated; a malformed reference_id is silently dropped; the action is still honored. The reverse doesn't hold: anything other than HTTP 200 with a parseable verdict is a webhook failure, not a deny. If you signal blocks with HTTP 403 your denies quietly turn into fail-open allows (or blocks, depending on failure handling) and every one of them counts against the circuit breaker.

The smallest working server

Allow-all AI security server in 12 lines of Python

# Run with: python server.py — expose on an https:// URL your admin configures.
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()

Put this behind a TLS-terminating reverse proxy on port 443, configure it as your endpoint, hit Test connection in the admin console — you'll see the allow verdict. This is exactly the shape of an archival-only integration: return allow unconditionally and persist the frame after responding, as a push alternative to polling the Compliance API. It is not a shape you should enforce with — it accepts every request, including unsigned ones. Add signature verification before you flip Enforce verdicts on.

Signature verification

Signing follows the Standard Webhooks spec. Three headers, lowercase as Anthropic sends them but case-insensitive on lookup (proxies re-case).

HeaderContents
webhook-idUnique per delivery. Equals the body's request_id. Use as your idempotency key.
webhook-timestampUnix time in seconds, as a decimal string. Reject if more than 5 minutes off your clock in either direction — that's the replay window.
webhook-signatureSpace-separated v1,<base64> values. Each is an HMAC-SHA256 over the byte string {webhook-id}.{webhook-timestamp}.{raw body bytes}. Accept the request if any value matches yours — use constant-time comparison.

The two bugs that catch every first integration

Watch out
  • Verify raw bytes, NOT re-encoded JSON. Compute the HMAC over the body exactly as received, before any parsing or reserialization. A json.loads() → json.dumps() round trip changes whitespace and dies here.
  • Decode the secret with a STANDARD base64 decoder, not a URL-safe one. The signing secret is the value after the whsec_ prefix, encoded with the standard alphabet (+ and /). A URL-safe decoder derives the wrong key bytes whenever the secret contains + or /, which is most of the time — and the failure is a silent constant-time mismatch.

Reference Python implementation (compressed from Anthropic's docs):

import base64, hashlib, hmac, time

TOLERANCE_SECONDS = 300

def verify(secret: str, headers: dict[str, str], body: bytes) -> bool:
h = {k.lower(): v for k, v in headers.items()}
try:
msg_id, ts, sigs = h["webhook-id"], h["webhook-timestamp"], h["webhook-signature"]
except KeyError:
return False # unsigned, not from Anthropic
try:
signed_at = int(ts)
except ValueError:
return False
if abs(time.time() - signed_at) > TOLERANCE_SECONDS:
return False # replayed, or clocks disagree
try:
key = base64.b64decode(secret.removeprefix("whsec_"), validate=True)
except ValueError:
return False # misconfigured secret
payload = f"{msg_id}.{ts}.".encode() + body
expected = b"v1," + base64.b64encode(hmac.new(key, payload, hashlib.sha256).digest())
return any(hmac.compare_digest(expected, s.encode()) for s in sigs.split())

Secret rotation

Rotation is an immediate cutover on the admin side, but requests signed with the previous secret can still arrive for about a minute afterward, plus anything already in flight. Have your server accept signatures from both the old and new secret during the rotation window so those stragglers aren't rejected as unsigned.

One-time exception

A connection test sent before your organization's first save arrives unsigned, because the signing secret doesn't exist yet. Accept unsigned requests until your admin confirms the secret exists, then reject them.

Operational semantics

Timeout

Your admin sets a verdict timeout between 1 and 10,000 ms, defaulting to 5,000 ms. That budget covers the entire round trip: connection, TLS handshake, request body upload, response body download.

Retry

Anthropic retries exactly once, after a 100 ms delay, and only when the connection attempt fails. Not on 500s. Not on timeouts. Not on parse errors. Once your server has responded — with anything — the exchange is done. The retry shares the same timeout budget and carries the same webhook-id and signature, so it's safe to key deduplication on webhook-id.

Failure handling

Everything else that isn't a clean 200-with-verdict is a webhook failure: timeouts, non-200 statuses (redirects included), unparseable or oversized response bodies, unreachable endpoints. On failure, your organization's setting decides:

  • Block the request. Safe default for high-regulation environments. If your DLP server is down, users are blocked. Availability of Claude becomes availability of your scanner.
  • Allow the request. Users keep working while your server recovers. Prompts flow uninspected during the outage — an accepted tradeoff for many orgs, but plan how you'll reconcile the gap in your audit trail.

Circuit breaker

Sustained webhook failures attributable to your AI security server trip a circuit breaker that stops enforcement: Anthropic stops calling your server, and failure handling applies to every request. Recovery isn't automatic — fix the server, then have your admin toggle Enforce verdicts back on. In practice this means: an unknown top-level type should return {"action": "allow"}, not an HTTP 500 — a future new event type would otherwise trip you into circuit-breaker territory on rollout day.

Latency

Every governed request in your organization pays your AI security server's round-trip in added latency. Load-test before you roll out to a large org; a 4-second scanner is invisible on a chat prompt but a nightmare on Claude Code tool loops that fire many requests in a row.

Source IP allowlisting

Requests originate from 160.79.106.0/24, part of Anthropic's published outbound IP ranges. Allowlist that block, not the inbound ranges on the same page — different lists. And allowlisting is not a substitute for signature verification: the block carries Anthropic egress beyond Inference Hooks.

The rollout playbook

Guided walkthrough1 of 4
  1. The first thing you turn on. Your server evaluates every request but no deny is enforced. You get to tune your rules against a week of real traffic before a single user is blocked.

Anthropic's docs put it plainly: blocking employees on day one is how DLP programs die. Shadow mode exists for a reason.

Design your integration

Pro tip
  • Deduplicate on webhook-id. It's unique per delivery and matches request_id in the body. A connection-failure retry reuses it, so it's a clean idempotency key.
  • Store every verdict with its reference_id. Anthropic records reference_id on the Activity Feed entry for each denial, so you can join denials back to the exact scan decision in your own system.
  • For always-allow archival integrations, RESPOND first, then persist. Answering before the write keeps your round trip out of the user's critical path — your storage system is not on the hot path.
  • Write deny_reason for the person, not the SIEM. 'Remove the credit card numbers from your prompt and resubmit' beats 'PCI_REGEX_2A tripped, reference 4471'. Users will act on the first.

Coverage matrix

Surface / accessInspected by Inference Hooks?
claude.ai (web, desktop, mobile)✅ Yes
Claude Code (web, desktop, CLI)✅ Yes (session_id is best-effort, client-asserted)
Claude Cowork✅ Yes
Voice mode❌ Not in the beta
Conversation title generation, other ancillary❌ Not sent
System prompts, tool definitions❌ Never sent
Raw file / image bytes❌ Never sent (extracted text is)
Image-only content (e.g. screenshot of doc)❌ Not inspected
Claude Platform API keys (developer access)❌ Out of scope of Inference Hooks (Platform orgs, not Enterprise)
Amazon Bedrock / Google Cloud deployments❌ Not available on those planes

Common mistakes

Watch out
  • Signalling a block with HTTP 403. That's a webhook failure, not a deny — your policy verdict gets thrown away and failure handling takes over.
  • Returning any action other than 'allow' or 'deny'. Same story: webhook failure. If you're tempted to add a third state, do it in your own audit log, not in the verdict.
  • Small default body limits (Express 100 kB, nginx 1 MB). A 3 MB transcript with a big PDF's extracted text will 413 at your reverse proxy. Raise limits to accommodate the 10 MB ceiling.
  • URL-safe base64 decode of the whsec_ secret. Silent constant-time mismatch on every request until you notice all your requests are 'unsigned'.
  • Reserialising the body before HMAC. Verify the raw bytes exactly as received. json.loads + json.dumps changes whitespace and breaks the signature.
  • Rejecting unknown top-level `type` with 500. Trips the circuit breaker on the day Anthropic ships a new event type. Return `allow` on unknown types.
  • Rejecting `source.application` values you don't recognise. It's an open enum. New values will appear and old integrations must not brick on them.
  • Assuming user/assistant alternation. Turns whose blocks are all excluded are dropped from `messages`. Parse defensively.

When to use Inference Hooks vs a client-side proxy

Some orgs still run TLS-intercepting proxies for coverage of everything their employees do online. Inference Hooks aren't a proxy replacement — they're a Claude-specific enforcement point that sits inside Anthropic's perimeter and sees a richer, structured view of the conversation than a proxy that only sees encrypted bytes on the wire.

  • Use Inference Hooks when you want structured access to what the model will actually see (tool calls, attachments, transcript), uniform coverage across chat + Code + Cowork, and no per-device install.
  • Keep your network DLP for everything else on the box: file uploads to non-Claude services, browser traffic outside claude.ai, email attachments. The two don't overlap.
  • Add the Compliance API for after-the-fact audit and export.

Quiz

Check yourself

0/3
  1. Anthropic contacts your AI security server how?
  2. Your DLP scanner detects a policy violation. Which response is correct?
  3. Your AI security server is down for a rolling deploy and returns 500s for 90 seconds. What happens to user prompts during that window?

Next