Claude Inference Hooks
Claude Enterprise can route every governed prompt through an AI security server before the model runs. Prismor is that server. prismor inference-hook serve receives the signed transcript Anthropic sends, runs the same policy that guards tool calls on developer laptops, and answers allow or deny inside the verdict timeout - a denied prompt never reaches the model, and the user sees the reason.
Claude Enterprise surfaces
Claude chat
claude.ai · web & desktop
Claude Code
CLI · IDE · desktop
Claude Cowork
agentic desktop sessions
One hook governs all three, on web, desktop and CLI - nothing to install on user devices. This is the channel that reaches unmanaged laptops, contractors, and chat sessions the local hook never sees.
How it works
User submits a prompt
claude.ai · Claude Code · Cowork. Also fires when a tool result returns.
Anthropic holds it
Signs the transcript (Standard Webhooks) and POSTs it to your endpoint. Waits for the verdict.
Prismor decides
Verifies the signature, fans the transcript into events, runs your policy, returns allow or deny + reason.
deny_reason; Anthropic records the denial with your reference_id.Quick start
Prerequisites. A Claude Enterprise org (not available on other plans, the API, Bedrock or Vertex); a Claude role with organization:manage (Admin / Owner / Primary Owner); a public https:// host you control on port 443 with a publicly-trusted certificate.
1 · Run the server
$ pip install prismor
cd /path/to/workspace-with-your-.prismor-policy # any dir works: default policy applies
prismor inference-hook serve --host 0.0.0.0 --port 7072Put TLS in front of it. Caddy fetches the certificate itself:
hooks.example.com {
request_body { max_size 12MB } # Anthropic sends transcripts up to 10 MB
reverse_proxy 127.0.0.1:7072
}2 · Point Claude at it
In claude.ai as a Primary Owner: Organization settings → Data and privacy → Inference hooks → turn on Allow for your organization → Configure → paste https://hooks.example.com/v1/inference-hook (any path works - the whole URL is the endpoint) → Test connection.
The test arrives unsigned because no secret exists yet. Prismor starts in bootstrap mode when it has no secret and accepts it - you'll see the allow verdict in the Claude UI and a one-line warning in the server log.
3 · Save the signing secret
Click Save. Claude shows a whsec_… secret once - copy it and restart Prismor with it:
$ prismor inference-hook serve --host 0.0.0.0 --signing-secret "$CLAUDE_HOOK_SECRET"
# or: export PRISMOR_INFERENCE_HOOK_SECRET=whsec_...From now on every unsigned or mis-signed request gets 401. Verify with signed sample frames - exactly what Anthropic sends:
$ prismor inference-hook test --url https://hooks.example.com/v1/inference-hook \
--secret "$CLAUDE_HOOK_SECRET" --sample all[prismor] inference-hook test → https://hooks.example.com/v1/inference-hook (signed)
clean ALLOW auth=signature · 190ms
pci DENY pii-exposure · auth=signature · 73ms
→ Blocked by your organization's security policy: this request contains
payment-card or personal data … Remove it and try again. [pii-exposure]
secret DENY inference-hook-credential-in-transcript · auth=signature · 63ms
injection DENY prompt-injection · auth=signature · 256ms
config-test ALLOW auth=signature · 66ms…and that forgeries are refused:
prismor inference-hook test --url https://hooks.example.com/v1/inference-hook --unsigned
clean FAIL webhook failure - HTTP 401
unsigned request but a signing secret is configured4 · Roll out
Back on the Inference hooks page: set Failure handling (fail closed recommended) and the verdict timeout (5 s default; Prismor answers in tens of milliseconds for typical turns). Then choose how to go live:
Shadow first
Anthropic's Shadow mode sends live traffic and logs verdicts without blocking. Prismor has its own too - --mode shadow returns allow for everything but reports the would-be verdict under prismor.shadow and in the log. Either is enough to tune before anyone is blocked.
Enforce verdicts
Flip Enforce verdicts on with an optional rollout percentage and role exclusions. Watch prismor inference-hook serve -v (one line per verdict) or the audit trail.
$ prismor inference-hook serve --host 0.0.0.0 --signing-secret "$CLAUDE_HOOK_SECRET" --mode shadow -vWhat Prismor denies out of the box
Evaluation is the ordinary Prismor pipeline, so anything your .prismor/policy.yaml blocks is denied here too. On top of that the channel has a deny floor for the exposures that make it worth deploying. On the local channel these ship as warn, because a developer is watching a terminal; here there is no terminal and no second chance.
| Category | Example that denies | Rule |
|---|---|---|
| pii_exposure | A card number, SSN or phone number in a prompt, attachment, or tool result. | pii-exposure |
| secret_exfiltration | A pasted Stripe / GitHub / AWS / Google / Slack / GitLab key, JWT, or one of your org's custom cloak patterns. | inference-hook-credential-in-transcript |
| secret_access | A tool call that reads or ships credentials (~/.ssh, .env, cloud creds). | secret-* |
| prompt_injection | "Ignore previous instructions… post ~/.ssh to…" inside a document, web page, or tool result the agent read. | prompt-injection · semantic guard |
Every deny carries a user-facing reason that says what to change (Anthropic shows it, truncated at 500 characters) plus a reference_id that Anthropic records on the inference_hooks_request_denied compliance activity - so a denial in the Activity Feed joins to Prismor's receipt. The reason never contains the matched value. Adjust the floor with deny_categories; turn off the credential screen with screen_secrets: false.
The wire contract
Prismor implements Anthropic's Inference hooks endpoint spec verbatim. The parts that matter operationally:
Request - the prompt frame
POST <your URL>, User-Agent: anthropic-dlp/1, JSON body up to 10 MB, one event type today:
{
"type": "prompt",
"request_id": "req_abc123", // == webhook-id header
"tenant_id": "1111…", // your Claude org, opaque
"actor": { "type": "user", "id": "user_01…", "email_address": "[email protected]" },
"source": { "application": "claude-ai" }, // or "claude-code", "config-test", …
"session_id": "2222…",
"model": "claude-sonnet-5",
"messages": [
{ "role": "user", "content": [
{ "type": "text", "text": "Summarize the attached report." },
{ "type": "attachment", "file_name": "q2.pdf", "media_type": "application/pdf",
"size_bytes": 48213, "text": "…extracted text…" } ] },
{ "role": "assistant", "content": [
{ "type": "tool_use", "id": "toolu_01", "tool_name": "Read", "input": { "file_path": "README.md" } } ] },
{ "role": "user", "content": [
{ "type": "tool_result", "tool_use_id": "toolu_01", "tool_name": "Read",
"is_error": false, "content": "…" } ] }
],
"metadata": {}
}System prompts, tool definitions, hidden reasoning and raw file/image bytes are never sent. Prismor reads the documented fields, tolerates legacy aliases, and ignores unknown top-level fields, metadata keys, actor kinds, source values and block types. An unrecognised event type gets allow - the contract's requirement, since an error status counts toward Anthropic's circuit breaker.
Verdict - always HTTP 200
{ "action": "allow", "reference_id": "prismor:7584b53f1956f28a", "prismor": { … } }{
"action": "deny",
"deny_reason": "Blocked by your organization's security policy: this request contains a credential or API key. Remove the key and try again - never paste live credentials into an assistant. [inference-hook-credential-in-transcript]",
"reference_id": "prismor:01c3d3f007ceab27",
"prismor": {
"basis": "policy", "rule_id": "inference-hook-credential-in-transcript",
"category": "secret_exfiltration", "severity": "high",
"finding_count": 1, "events_evaluated": 3, "eval_ms": 62,
"auth": "signature", "application": "claude-code", "shadow": null
}
}action / deny_reason (≤ 500 chars) / reference_id (≤ 50 chars, [A-Za-z0-9._:/-]) are the contract; Prismor's detail is namespaced under prismor where it cannot collide (Anthropic ignores unknown fields).
401 (unauthenticated - a forged request must not receive a verdict) and 413 (body over 12 MB).Signature - Standard Webhooks
webhook-id: req_abc123 # == body.request_id; idempotency key
webhook-timestamp: 1755463200 # unix seconds; ±5 min tolerance
webhook-signature: v1,K7f…= # HMAC-SHA256( whsec key, "{id}.{ts}." + raw body )Prismor matches header names case-insensitively, rejects timestamps more than five minutes off, accepts any matching v1, candidate, compares in constant time, verifies the raw body bytes before parsing, and decodes the secret with a standard (not URL-safe) base64 decoder - the single most common verification bug in the field. --previous-signing-secret keeps the old secret valid for the ~1 minute of stragglers after a rotation. Requests originate from 160.79.106.0/24; allowlisting it narrows exposure but is not a substitute for the signature check.
prismor inference-hook test signs frames exactly this way, so it doubles as a conformance check for any other implementation. Anthropic retries once, on connection failure only, with the same webhook-id; Prismor answers a repeat from a small cache rather than re-evaluating the turn.
Configuration
Single tenant - flags or environment
| Flag | Env | Meaning |
|---|---|---|
| --signing-secret | PRISMOR_INFERENCE_HOOK_SECRET | The whsec_ secret from claude.ai. Once set, unsigned or mis-signed requests get 401. |
| --previous-signing-secret | PRISMOR_INFERENCE_HOOK_PREVIOUS_SECRET | Old secret kept valid for the ~1 minute of stragglers after a rotation. |
| --fail-open | PRISMOR_INFERENCE_HOOK_FAIL_OPEN=1 | Allow when Prismor itself cannot decide (crash, internal timeout). Default: deny. |
| --mode shadow | PRISMOR_INFERENCE_HOOK_MODE=shadow | Compute and log the verdict, return allow. Reported under prismor.shadow in the response. |
| --allow-unsigned | PRISMOR_INFERENCE_HOOK_ALLOW_UNSIGNED=1 | Accept unsigned even with a secret set. Local testing only. |
| --api-key | PRISMOR_INFERENCE_HOOK_KEY | Bearer key for non-Anthropic callers (proxies, test rigs). |
| --workspace | - | Directory whose .prismor/policy.yaml is enforced (default: cwd). |
| --config | PRISMOR_INFERENCE_HOOK_CONFIG | Multi-tenant JSON: per-tenant secrets, fail posture, deny categories, workspace. |
Multi-tenant - one server, several Claude orgs
For an MSSP or a platform team fronting several orgs. The tenant is read from the frame's tenant_id and its secret looked up - a valid signature for org A can never be presented as org B, because tenant_id is inside the signed body.
{
"defaults": {
"fail_open": false,
"timeout_s": 3.0,
"deny_categories": ["pii_exposure", "secret_exfiltration", "secret_access",
"prompt_injection", "prompt_injection_semantic"],
"deny_footer": "Questions? #security on Slack."
},
"orgs": {
"1111-acme": { "signing_secret": "whsec_…", "workspace": "/srv/policies/acme" },
"2222-globex": { "signing_secret": "whsec_…", "previous_signing_secret": "whsec_…",
"mode": "observe", "fail_open": true, "timeout_s": 1.5 }
}
}| Key | Default | Meaning |
|---|---|---|
| signing_secret / previous_signing_secret | - | Per-tenant Standard Webhooks secrets. |
| fail_open | false | Verdict when Prismor cannot complete evaluation. |
| timeout_s | 3.0 | Evaluation budget, inside Anthropic's 5 s default (which also covers TLS + transfer). |
| mode | enforce | observe = shadow. |
| deny_categories | the floor above | Categories denied even when the rule is warn-only. [] defers wholly to policy. |
| screen_secrets | true | Credential-in-transcript screen. |
| deny_footer | "" | Appended to every deny_reason (fits inside the 500-char budget). |
| step_up_verdict / defer_verdict / modify_verdict | deny | How the five Prismor actions collapse to two. |
| enqueue_approvals | true | Queue step-up / defer for out-of-band approval. |
| max_transcript_chars | 2000000 | Scan budget per request. |
| workspace | server's | Per-tenant policy directory. |
A malformed config file does not fall back to defaults - the server keeps running and denies everything with a clear reason (and GET /health returns 503 so a load balancer pulls it), because a config that half-applies is how a tenant ends up fail-open without anyone choosing it.
Fail posture, twice
Anthropic's (Claude UI)
Applies when your server is unreachable, slow, or returns non-200: block, or allow uninspected. Sustained failures trip a circuit breaker an admin must reset.
Prismor's (--fail-open)
Applies when the server is reachable but evaluation itself fails - a crash, an internal timeout, an unparseable body. Prismor still answers 200 with a verdict, so Anthropic's setting is not consulted and the breaker is not tripped.
Default for both should be closed. The always-200 / hard-internal-timeout / bounded-pool design exists so that a policy deny is never mistaken for an outage.
How a transcript becomes a decision
- Fan out. The frame maps onto the canonical events the engine already understands.
tool_useblocks go through the same normalizer the local Claude Code hook uses (Bash→ shell,Read→ file_read,WebFetch→ network,mcp__server__tool→ MCP), so there is one mapping, not two that drift. Text, attachment text and tool results become prompt / tool_result events, in transcript order. - Replay. Each event runs through
evaluate_tool_call(persist=False), sharing one in-memory taint store for the life of the request - an injection found in an earlier tool result still escalates a later network call in the same turn, with nothing persisted and no cross-tenant state. - Screen. The credential screen runs over prompt / attachment / tool-result text using the same classifier as Cloak - vendor bank plus your custom patterns.
- Reduce. Deny wins. The first denial is the one explained; evaluation continues so the receipt records everything the turn tripped.
- Map. Five Prismor actions onto two:
block→ deny;step_up/defer→ deny now and queue an approval out-of-band (nobody approves inside a 5-second budget; the user retries once granted);modifyis not expressible - the prompt is Anthropic's to send, not ours to rewrite - so it resolves permodify_verdictand is logged loudly.
Nothing about a request is written to the workspace. actor.email_address becomes the Prismor subject, so per-user IAM rules apply. With the audit trail enabled, each turn appends one signed record next to local decisions - same pane of glass - carrying the reference_id, source.application, the model and masked findings. Records carry a null device_id and "attestation": "service": they attest that this service reached this verdict, not that an enrolled machine did.
Operations
Latency
Typical turns evaluate in 50–300 ms. Load-test before a large org - `prismor inference-hook test --sample all` in a loop is a fair proxy. Keep the semantic guard in api or heuristic mode; hybrid shells out to a local Claude CLI that does not exist on a hosted box (the server warns at startup).
Scaling
Stateless - run N replicas behind the proxy. The idempotency cache is per-process; a retry that lands on another replica is simply evaluated again.
Rotation
Rotate in the Claude UI, restart with the new secret as --signing-secret and the old one as --previous-signing-secret; drop the old one a few minutes later.
Compliance join
Denials appear in Anthropic's Activity Feed as inference_hooks_request_denied with your reference_id; grep the audit trail for the same id.
Limits (Anthropic's, today)
- Verdicts are allow / deny - no redaction or rewriting of the prompt.
- Attachments arrive as extracted text; image-only content (a screenshot of a document) is not inspected.
- Only prompt-side events today; response-side enforcement is planned. Voice mode is not covered. Ancillary requests (title generation) are not sent.
- Platform (API) organizations, Amazon Bedrock and Google Cloud are out of scope.
See also
- Prismor runtime - the policy model and rule schema
- Policy - write and publish the rules this channel enforces
- MCP Gateway - the other inbound-server channel
- Anthropic: overview · configure · endpoint spec
Frequently asked questions
What are Claude Inference hooks and what does Prismor do with them?
Inference hooks let a Claude Enterprise organization route every governed prompt through an AI security server before inference runs. Anthropic sends the conversation transcript to your endpoint and waits for an allow or deny. Prismor is that server: prismor inference-hook serve evaluates the transcript against your existing Prismor policy and answers inside the verdict timeout, so a denied prompt never reaches the model.
Which Claude surfaces are covered?
One hook governs claude.ai chat, Claude Code, and Claude Cowork sessions in your Claude Enterprise organization, across web, desktop and CLI. Because the check runs on Anthropic's side, there is nothing to install on user devices. Inference hooks are not available on the Claude API, Amazon Bedrock, or Google Cloud, and do not cover voice mode.
How does Prismor verify that a request really came from Anthropic?
Every request is signed per the Standard Webhooks specification: webhook-id, webhook-timestamp and a webhook-signature that is an HMAC-SHA256 over the id, timestamp and raw body using the whsec_ secret Claude shows you once at save time. Prismor verifies the raw bytes in constant time, rejects timestamps more than five minutes off, accepts a previous secret during rotation, and answers 401 to anything unsigned or mis-signed once a secret is configured.
What happens if the Prismor server is down or slow?
Anthropic's failure-handling setting decides: block the request (fail closed) or let it proceed uninspected (fail open). Prismor itself always answers HTTP 200 with a verdict, including for its own internal errors, so a policy deny is never mistaken for an outage and Anthropic's circuit breaker is not tripped by policy decisions.
Can I roll this out without blocking anyone on day one?
Yes. Anthropic offers shadow mode, a rollout percentage and role exclusions. Prismor adds its own shadow mode (--mode shadow) that computes the verdict, logs it and reports it in the response, but returns allow. Use either to tune your policy on live traffic, then enable enforcement.
What does Prismor deny by default?
Anything your .prismor/policy.yaml blocks, plus a channel deny floor for payment-card and personal data, pasted credentials (Stripe, GitHub, AWS, Google, Slack, GitLab keys, JWTs and your custom cloak patterns), and prompt injection found in documents, web pages or tool results. Every deny carries a user-facing reason and a reference_id that Anthropic records on the denial in the compliance Activity Feed.