Anthropic Claude Agent SDK¶
agent_airlock.integrations.anthropic_claude_agent_sdk is the canonical
adapter for the Anthropic Claude Agent SDK. It guards the
tools you serve from an in-process SDK MCP server, and re-exports the
auto-memory and task-budget helpers so callers can find them without
learning the internal module layout.
Install¶
The extra pins claude-agent-sdk>=0.1.58. The SDK is not imported at
module load — wrapping an object that comes from the SDK without the
extra installed raises a clear ClaudeAgentSDKMissingError
with the install hint (never an opaque ImportError from somewhere deep
in the call stack).
Quickstart¶
from claude_agent_sdk import ClaudeAgentOptions, create_sdk_mcp_server, tool
from agent_airlock.integrations.anthropic_claude_agent_sdk import wrap_tools
from agent_airlock.policy import SecurityPolicy
@tool("greet", "Greet a user", {"name": str})
async def greet(args):
return {"content": [{"type": "text", "text": f"Hello, {args['name']}!"}]}
policy = SecurityPolicy(rate_limits={"*": "100/hour"})
server = create_sdk_mcp_server("tools", tools=wrap_tools([greet], policy=policy))
options = ClaudeAgentOptions(
mcp_servers={"tools": server},
allowed_tools=["mcp__tools__greet"],
)
Wrap the tools before building the server, which keeps its own
references. wrap_tools returns guarded copies and leaves the originals
as they were. On every call:
- a key the tool's
input_schemadoes not declare is a ghost argument, stripped before the handler sees it (refused instead when the process starts withAIRLOCK_UNKNOWN_ARGS=block) - each declared argument is strictly validated, against the type the SDK advertises to the model
- the policy applies under the tool's name, the one the model calls
- what the handler returns is masked for PII and secrets
- a refusal reaches the model as an error result (
is_error: True) carrying Airlock's error and fix hints
What the adapter does¶
wrap_tools(tools, policy=...)fronts eachSdkMcpTool's handler with a proxy whose keyword parameters come frominput_schema, in the three forms the SDK accepts: a{"name": type}map (every key required), aTypedDict(its required keys) or a JSON schema (itsrequiredlist). The handler's singleargsdict is spread into those parameters, so every gate that reads arguments sees each one.AnthropicClaudeAgentSDKAdapter().wrap_agent(obj, policy=...)walks any object'stoolsattribute (dict or list) and replaces each entry: anSdkMcpToolwith a guarded copy, an object'sforwardor a callable with an Airlock-wrapped shim that carries the tool's name and signature and is async when the tool is.- Re-exports helpers from the existing
claude_*.pymodules so callers find them in one place. The adapter applies none of them itself: AutoMemoryAccessPolicy+guarded_read/guarded_write— per-tenant scope, byte quota, redaction-on-write. Call them yourself; the adapter wraps no memory access. A tenant id has to name one directory under/memory/: an empty or path-like id raisesAutoMemoryTenantIdErrorat construction.build_task_budget_headers+build_output_config— the Messages API task budget,output_config={"task_budget": {"type": "tokens", "total": N, "remaining": M}}with thetask-budgets-2026-03-13beta (totalat least 20,000). The adapter'stask_budget_request_kit(remaining=...)returns both, for a request you build withclient.beta.messages.create. Through the SDK itself, passClaudeAgentOptions(task_budget={"total": N}), which sends the header and the field for you.- Pins a
SUPPORTED_SDK_VERSIONStuple recording the SDK versions the adapter was checked against. Wrapping an SDK object on any other version emits aUserWarningbut still wraps.
AnthropicClaudeAgentSDKAdapter(managed_audit=..., auto_memory_policy=...)
accepted two settings that nothing ever read: no Managed Agents request
passes through this adapter, and it wraps no memory access. Setting either
now emits a FutureWarning; both go in v1.0.0. For Claude Managed Agents
sessions, see the Managed Agents approval gate.
Honest scope¶
- Each argument is checked against the type the SDK advertises and hands
the handler, not the Python annotation. The SDK sends a type it does not
map (a
datetime, aLiteral) as a string, so Airlock checks a string; afloatfield accepts an integer and passes it on unconverted. Nested structure (list items, object properties, enums) is left to the SDK's own JSON-schema check, which runs before the handler is called. - Airlock is stricter than that check in one place: an
integerfield refuses3.0, which JSON Schema admits. - A key outside a JSON schema's
propertiesis a ghost argument unless the schema setsadditionalPropertiesto something other thanfalse. JSON Schema, and the SDK, admit it by default. - Every key has to be usable as a Python parameter name. A tool whose
schema declares
fromorfile-pathis refused when you wrap it, with anAirlockErrornaming the keys, rather than guarded partially. - A
typing_extensions.TypedDictschema is enforced even though the SDK, on Python 3.11 and later, does not recognise one and advertises the tool with no properties. The model then learns the keys from Airlock's fix hints. - An exception raised by a handler reaches the model as Airlock's generic "Unexpected error" result, not as the exception's own message.
ClaudeAgentOptions.toolsholds the names of Claude Code's built-in tools, which Claude Code runs itself. Airlock cannot wrap those, andwrap_agentraises anAirlockErrorpointing towrap_toolsif it is handed one.- Stub objects (any object with a
toolsattribute, or a tool of theSdkMcpToolshape) never trigger the SDK import, which is what the test suite relies on. - Verified end to end on
claude-agent-sdk0.2.160: guarded tools served bycreate_sdk_mcp_serverand called through an MCP client session.