Skip to main content
The rules do not know what an agent SDK is. They wrap functions: a name -> callable map goes in, the same map comes out with every rule around every call, and the agent SDK calls the wrapped one. Whatever framework owns the model loop, the seam is the same — the point where a tool’s implementation is registered.
Two things come with the wrapping and nothing else changes: the door’s personas in, real values out, on every call; and the [tools.*] table — live tools the file declares (a keyed web search, a connector operation) that run.tools() materializes already wrapped, with run.tool_definitions(shape="openai" | "anthropic") / run.toolDefinitions() giving the model their schemas. Below, the seam in each SDK.

OpenAI Agents SDK (Python)

@function_tool reads a function’s signature and docstring for the schema, so decorate a thin typed function that calls the wrapped one:

Vercel AI SDK (TypeScript)

A tool is { description, inputSchema, execute }; wrap execute:

A hand-rolled Anthropic tool loop (Python)

The rules give the model its tool schemas and answer its calls:
A deny surfaces as an exception (HookDenied) from the wrapped call; run.after_error is what the error text the model should see goes through, so catch it and return the message as the tool result.

LangChain (Python)

StructuredTool takes the implementation and a schema separately, which is what a wrapped callable needs:

An MCP server

Wrap the handler bodies the server registers, so every client of the server — a coding agent, an SDK agent — gets the rules without knowing:
This is also the way to give a coding agent the door’s inbound direction (why): the server rewrites each result before the agent reads it.

Claude Agent SDK

The Claude Agent SDK runs Claude Code’s hooks. Name gateway hooks run as the hook command in its hooks option and it behaves as in Claude Code; for an in-process hook, call the run directly from the SDK’s hook callback — run.before_call(tool, input) on PreToolUse, run.after_call(tool, input, result) on PostToolUse, run.end(report) on Stop — and return the callback’s verdict from what those answer.

Your own transport

run.session(session, request_fn=...) (run.session(session, { requestFn })) puts pre_request and post_response around a client with its own auth — an x-api-key, a password grant, a key in the query — so a vendor SDK you did not write still goes through the door. And run.on(event, handler, matcher=...) adds a rule from code ahead of or after the file’s, for the one guard that has no business in a shared file. Everything above is the same file, the same events, the same meters. Which SDK owns the loop is the one thing the rules never need to know.