> ## Documentation Index
> Fetch the complete documentation index at: https://docs.surfacearea.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Hooks with any agent SDK

> Attach gateway-hooks.toml to the tools of whichever agent SDK you build on — OpenAI Agents SDK, Vercel AI SDK, a hand-rolled Anthropic tool loop, LangChain, an MCP server, the Claude Agent SDK — by wrapping the functions once.

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.

```python theme={null}
from gatewaysdk.hooks import HookRun

run = HookRun.from_config("gateway-hooks.toml")
run.seed(entities)                                  # the task's people, before the agent starts
guarded = run.wrap({"lookup_email": lookup_email, "web_search": web_search})
# ... register guarded["lookup_email"] wherever your SDK takes a tool implementation ...
verdict = run.end(report)                           # run_end: the outgoing scan, the findings
```

```ts theme={null}
import { HookRun } from "@withgateway/sdk/hooks";

const run = await HookRun.fromConfig("gateway-hooks.toml");
run.seed(entities);
const guarded = run.wrapTools({ lookup_email: lookupEmail, web_search: webSearch });
const verdict = await run.end(report);
```

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:

```python theme={null}
from agents import Agent, Runner, function_tool

guarded = run.wrap({"lookup_email": _lookup_email})

@function_tool
def lookup_email(email: str) -> dict:
    """Breach records for one email address."""
    return guarded["lookup_email"](email=email)

agent = Agent(name="investigator", instructions=run.alias.rewrite_instruction(task), tools=[lookup_email])
result = Runner.run_sync(agent, "Start with the subject's email.")
verdict = run.end(result.final_output)
```

## Vercel AI SDK (TypeScript)

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

```ts theme={null}
import { generateText, tool } from "ai";
import { z } from "zod";

const impls = run.wrapTools({
  lookup_email: async ({ email }: { email: string }) => lookupEmail(email),
});

const result = await generateText({
  model,
  system: run.alias.rewriteInstruction(task),
  tools: {
    lookup_email: tool({
      description: "Breach records for one email address.",
      inputSchema: z.object({ email: z.string() }),
      execute: impls.lookup_email,
    }),
  },
  prompt: "Start with the subject's email.",
});
const verdict = await run.end(result.text);
```

## A hand-rolled Anthropic tool loop (Python)

The rules give the model its tool schemas and answer its calls:

```python theme={null}
import anthropic

client = anthropic.Anthropic()
tools = {**run.tools(), **run.wrap({"lookup_email": lookup_email})}
definitions = run.tool_definitions(shape="anthropic") + [LOOKUP_EMAIL_DEFINITION]

messages = [{"role": "user", "content": run.alias.rewrite_instruction(task)}]
while True:
    turn = client.messages.create(model=MODEL, tools=definitions, messages=messages, max_tokens=8000)
    messages.append({"role": "assistant", "content": turn.content})
    if turn.stop_reason != "tool_use":
        break
    results = []
    for block in turn.content:
        if block.type == "tool_use":
            answer = tools[block.name](**block.input)          # the rules ran around this
            results.append({"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(answer)})
    messages.append({"role": "user", "content": results})
verdict = run.end(turn.content[-1].text)
```

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:

```python theme={null}
from langchain_core.tools import StructuredTool
from pydantic import BaseModel

class LookupEmail(BaseModel):
    email: str

guarded = run.wrap({"lookup_email": lookup_email})
tool = StructuredTool.from_function(
    func=lambda email: guarded["lookup_email"](email=email),
    name="lookup_email", description="Breach records for one email address.", args_schema=LookupEmail,
)
```

## 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:

```python theme={null}
from mcp.server.fastmcp import FastMCP

server = FastMCP("vendor")
guarded = run.wrap({"lookup_email": _lookup_email})

@server.tool()
def lookup_email(email: str) -> dict:
    """Breach records for one email address."""
    return guarded["lookup_email"](email=email)
```

This is also the way to give a coding agent the door's inbound direction
([why](/hooks/coding-agents)): 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](/hooks/coding-agents); 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.