Integrations

Anthropic SDK

Route the official Anthropic Python and Node SDKs through Sentisec by changing one argument — and what your code sees when a tool call is blocked.

The official anthropic SDKs take a base_url argument. Point it at your workspace and every message your agent sends — and every tool call the model comes back with — goes through Sentisec first.

The swap

import anthropic

client = anthropic.Anthropic(
    base_url="https://api.sentisec.ch/ws/<your-workspace-id>",
    api_key="sk_sentisec_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
)

message = client.messages.create(
    model="claude-sonnet-4",
    max_tokens=512,
    messages=[{"role": "user", "content": "Reconcile the quarterly ledger."}],
)
print(message.content)
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({
  baseURL: 'https://api.sentisec.ch/ws/<your-workspace-id>',
  apiKey: 'sk_sentisec_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
});

const message = await client.messages.create({
  model: 'claude-sonnet-4',
  max_tokens: 512,
  messages: [{ role: 'user', content: 'Reconcile the quarterly ledger.' }],
});
console.log(message.content);

The base URL has no /v1 suffix here — the SDK appends /v1/messages itself, so adding your own produces /v1/v1/messages and a 404. This is the mirror image of the OpenAI recipe, where the /v1 does belong in the base URL. The Vercel AI SDK's @ai-sdk/anthropic, which Mastra uses, is not this SDK: its base URL ends in /v1. See Vercel AI SDK and Mastra.

The API key is your Sentisec key. Sentisec holds the upstream Anthropic credential, so your provider key never ships in your application config. The workspace id in the URL is what carries your policy; why it matters.

What happens when a call is blocked

The request succeeds with 200. No exception is raised and no error status is returned. By default the blocked tool_use block becomes a text block at the same position:

[Sentisec blocked tool_use: <why the call was stopped>]

Other tool_use blocks in the message that only read are released. A call that changes something is not sent when another call in the message is held or refused, and it carries its own short explanation (see calls proposed together). stop_reason stays tool_use while an allowed call remains and is end_turn otherwise. A stream carries the same text in place of the call's frames. Your loop needs no special case: with no call left to run, the message is the final answer, and the explanation reaches your user.

If your workspace turns on the rename opt-in (how), the block instead stays a tool_use, keeping its id, with its name and input replaced:

ToolUseBlock(type='tool_use', id='toolu_01...',
             name='sentisec_tool_call_blocked',
             input={'error': 'tool_call_blocked', 'reason': '...'})

The SDK's tool runner (supported versions) answers it as an unknown tool, Sentisec replaces that answer with its ruling, and the model re-plans. If you dispatch tool calls yourself, check the name first:

for block in message.content:
    if block.type != "tool_use":
        continue
    if block.name == "sentisec_tool_call_blocked":
        continue  # return block.input["reason"] as its tool_result; never retry it
    dispatch(block)

Streaming

client.messages.stream() works normally, including on a blocked call:

with client.messages.stream(
    model="claude-sonnet-4",
    max_tokens=512,
    messages=messages,
    tools=tools,
) as stream:
    for text in stream.text_stream:
        print(text, end="")
    final = stream.get_final_message()

get_final_message() returns a completed message carrying the explanation and a standard stop reason. It does not raise.

Verdicts

Every request produces a verdict on the dashboard — NOMINAL when nothing was unusual, escalating to HIJACK when a tool call is traced back to content the agent was never supposed to act on. The intervention (PROCEED, SANITIZE, HALT) records what actually happened to the call.

New workspaces start in a monitor-only posture, so you can watch what a policy would do before it does it.

See also

Protected docs