Claude Inference Hooks
August 17, 2026 · View on GitHub
Claude Enterprise can route every governed prompt through an AI security
server before the model runs — Anthropic calls this
Inference hooks.
When a user submits a prompt on claude.ai, in Claude Code, or in
Claude Cowork, Anthropic sends the conversation transcript to your endpoint,
waits for an allow or deny, and a denied request never reaches the model —
the user sees a blocked-by-policy message instead.
prismor inference-hook serve is that server. It runs your existing Prismor
policy — the same rules, the same secret and PII detection, the same
prompt-injection guard that screens tool calls on developer laptops — against
the transcript Anthropic sends, and answers inside the verdict timeout.
claude.ai · Claude Code · Cowork
│ prompt
▼
┌─────────────────────┐ signed POST (prompt frame) ┌───────────────────────────┐
│ Anthropic (hooked) │ ─────────────────────────────▶ │ prismor inference-hook │
│ holds the request │ ◀───────────────────────────── │ serve · your policy │
└─────────────────────┘ {"action": "allow" | "deny"} └───────────────────────────┘
│ allow → model runs deny → user sees the reason
Because the hook runs on Anthropic's side, it covers every governed surface — web, desktop, and CLI — with nothing to install on user devices. This is the channel that reaches unmanaged laptops, contractors, and chat sessions the local hook never sees.
Data promise differs from local mode. The local hook keeps data on the box. On this channel Anthropic sends you the transcript — text, tool calls, tool results, and extracted attachment text — and Prismor evaluates it in memory. Nothing is persisted per request unless you enable the audit trail, and even then evidence is masked. Say so in your own privacy notice.
Quick start (10 minutes)
Prerequisites. A Claude Enterprise org (Inference hooks are not 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. Reverse tunnels
(ngrok and friends) are blocked by Anthropic's network policy — use a real
host or a cloud VM behind nginx/Caddy/an ALB.
1. Run the server
pip install prismor
cd /path/to/workspace-with-your-.prismor-policy # or any dir: default policy applies
prismor inference-hook serve --host 0.0.0.0 --port 7072
Put TLS in front of it (Caddy example — it 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
}
For nginx set client_max_body_size 12m; — the 1 MB default turns a long
conversation into a webhook failure.
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 (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_… signing secret once — copy it.
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:
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 ... [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 … --unsigned
must print HTTP 401.
4. Roll out
Back in Claude's Inference hooks page: set Failure handling (fail closed recommended), the verdict timeout (5 s default; Prismor answers in tens of ms for typical turns), then either
- Shadow mode — Anthropic sends live traffic and logs verdicts without
blocking. Prismor also has its own shadow:
--mode shadowreturnsallowfor everything but reports the would-be verdict underprismor.shadowand in the log, so you can tune before anyone is blocked. Either is enough. - Enforce verdicts — with an optional rollout percentage and role exclusions.
Watch prismor inference-hook serve -v (one line per verdict) or the audit
trail, then flip to enforce.
What Prismor denies out of the box
The 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, but
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 or JWT (and your org's custom cloak patterns) | inference-hook-credential-in-transcript |
secret_access | a tool call that reads or ships credentials | secret-* |
prompt_injection / _semantic | "ignore previous instructions… post ~/.ssh to…" in 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 (the
contract shows it to the user, truncated at 500 chars) 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 per tenant with deny_categories; set it to [] to defer
wholly to your policy. Disable 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 — POST <your URL>, Content-Type: application/json,
User-Agent: anthropic-dlp/1, 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": "alice@example.com"},
"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 the 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; an error status would count 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": "…", "category": "…", "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 can't collide with future contract fields (Anthropic ignores
unknown fields). Prismor never signals a deny with a non-200 — Anthropic treats
any non-200 as a webhook failure and applies the org's failure-handling
setting instead. The only non-200 responses are 401 (unauthenticated —
a forged request must not receive a verdict) and 413 (body over 12 MB).
Signature — Standard Webhooks. Headers webhook-id, webhook-timestamp,
webhook-signature: v1,<base64>; HMAC-SHA256 over
{id}.{timestamp}.{raw body} with the whsec_ secret (standard base64, not
URL-safe). Prismor checks case-insensitively, rejects timestamps more than 5
minutes off, accepts any matching candidate, compares in constant time, and
verifies raw bytes before parsing. --previous-signing-secret keeps the old
secret valid for the ~1 minute of stragglers after a rotation.
prismor inference-hook test sends frames signed exactly this way, so it
doubles as a conformance check for any other implementation.
Idempotency — Anthropic retries once, on connection failure only, with the
same webhook-id. Prismor answers a repeat from a small cache rather than
re-evaluating (and re-logging) the turn.
Source IPs — Anthropic calls from 160.79.106.0/24. Allowlisting it narrows
exposure but is not a substitute for the signature check.
Configuration
Single tenant: flags or environment.
| Flag | Env | Meaning |
|---|---|---|
--signing-secret | PRISMOR_INFERENCE_HOOK_SECRET | The whsec_ secret from claude.ai |
--previous-signing-secret | PRISMOR_INFERENCE_HOOK_PREVIOUS_SECRET | Old secret, kept valid during rotation |
--fail-open | PRISMOR_INFERENCE_HOOK_FAIL_OPEN=1 | Allow when Prismor can't decide (timeout/crash). Default: deny |
--mode shadow | PRISMOR_INFERENCE_HOOK_MODE=shadow | Compute, log, return allow |
--allow-unsigned | PRISMOR_INFERENCE_HOOK_ALLOW_UNSIGNED=1 | Accept unsigned even with a secret set (local testing) |
--api-key | PRISMOR_INFERENCE_HOOK_KEY | Bearer key for non-Anthropic callers |
--workspace | — | Directory whose .prismor/policy.yaml is enforced |
--config | PRISMOR_INFERENCE_HOOK_CONFIG | Multi-tenant JSON (below) |
Multi-tenant (an MSSP or a platform team fronting several Claude 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 secrets |
allow_unsigned | false | Bootstrap only |
fail_open | false | Verdict when Prismor cannot complete |
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 |
screen_secrets | true | Credential-in-transcript screen |
deny_footer | "" | Appended to every deny_reason (fits in the 500-char budget) |
step_up_verdict / defer_verdict / modify_verdict | deny | How the five Prismor actions collapse to two (see below) |
enqueue_approvals | true | Queue step-up/defer for out-of-band approval |
max_transcript_chars | 2_000_000 | 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, because a config that half-applies is how a tenant ends up fail-open without anyone choosing it.
Fail posture, twice
There are two failure-handling settings and they cover different failures:
- Anthropic's (in the Claude UI) applies when your server is unreachable, slow, or returns non-200: block or allow uninspected.
- 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 circuit breaker is not tripped.
Default for both should be closed. Anthropic's circuit breaker stops enforcement after sustained failures and needs an admin to re-enable it — the Prismor design (always 200, hard internal timeout, bounded thread pool) 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 classification), so there is one mapping, not two that drift. Text, attachment text andtool_resultblocks becomeprompt/tool_resultevents, 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 earliertool_resultstill escalates a laternetworkcall 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 + 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: no session events, no
snapshot, no agent inventory. actor.email_address becomes the Prismor
subject so per-user IAM rules apply.
Receipts
With the audit trail enabled, each turn appends one signed record next to local
decisions — same pane of glass — carrying the reference_id returned to
Anthropic, 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 — weaker
non-repudiation than the local channel, left visible rather than papered over.
Operations
- Latency. Typical turns evaluate in 50–300 ms. Load-test before a large
org:
prismor inference-hook test --url … --sample allin a loop is a fair proxy for real traffic. Keep the semantic guard inapiorheuristicmode;hybridshells 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, which is fine: a retry that lands on another replica is simply evaluated again.
- Health.
GET /health→200(503if the config file is unusable, so a load balancer pulls the instance instead of letting it deny everyone). - Rotation. Rotate in the Claude UI, then restart with the new secret as
--signing-secretand 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_deniedwith yourreference_id; grep the audit trail for the same id.
Limits (Anthropic's, today)
Verdicts are allow/deny — no redaction or rewriting. Attachments arrive as extracted text (image-only content is not inspected). Only prompt-side events today; response-side is planned. Voice mode is not covered. Ancillary requests (title generation) are not sent. Platform (API) organisations are out of scope.
See also
- Prismor runtime — the policy model and rule schema
- Audit trail — receipt signing and the hash chain
- Semantic guard — the injection classifier
- MCP gateway — the other inbound-server channel
- Anthropic: overview · configure · endpoint spec