Docs · Claude Agent SDK
Claude Agent SDK on Selan
The Agent SDK is Claude Code as a library — the same loop and the same built-in tools, driven from your code instead of a terminal. Pointing it at Selan is two environment variables. For running the CLI itself, start at the docs.
Install
The SDK ships for TypeScript and Python. It needs Node 18 or newer even in the Python case, because it runs Claude Code underneath:
npm install @anthropic-ai/claude-agent-sdk
pip install claude-agent-sdk
This is not @anthropic-ai/sdk. That one sends a request
and returns an answer, and it is covered on the API page. The
Agent SDK runs a whole session: it reads files, runs commands and keeps going until
the work is done. Both reach the gateway the same way, so the setup below applies to
either.
The two variables
A base URL carrying the /anthropic segment, which names the format the
SDK speaks, and a token:
export ANTHROPIC_BASE_URL=https://gw.selan.ai/anthropic
export ANTHROPIC_AUTH_TOKEN=selanct_...
Mint the token in Selan under Settings → CLI tokens. It spends as
itself, under an address like nightly-triage@tokens.selan.ai, so what the
agent costs shows up as the agent in Usage rather than on whoever set it up.
ANTHROPIC_API_KEY works here too. The SDK turns either one into the
Authorization: Bearer header the gateway reads, which is why both are
accepted — unlike a raw HTTP call, where only the bearer form is.
The SDK starts a subprocess, so it needs the variables handed to it.
Exporting them in your shell covers the simple case. Inside a service, a test runner
or CI, pass them through the env option and spread the current
environment rather than replacing it — drop PATH and the subprocess
cannot start at all.
const env = {
...process.env,
ANTHROPIC_BASE_URL: "https://gw.selan.ai/anthropic",
ANTHROPIC_AUTH_TOKEN: process.env.SELAN_TOKEN,
}
Your first agent
A whole session in one call. query returns an async iterator of the
messages the session produces, and the result message is the end of it:
import { query } from "@anthropic-ai/claude-agent-sdk"
const env = {
...process.env,
ANTHROPIC_BASE_URL: "https://gw.selan.ai/anthropic",
ANTHROPIC_AUTH_TOKEN: process.env.SELAN_TOKEN,
}
for await (const message of query({
prompt: "Find every TODO under src/ and write them to TODOS.md",
options: {
env,
cwd: "/tmp/scratch",
model: "claude-opus-5",
permissionMode: "bypassPermissions",
},
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "tool_use") console.log("tool:", block.name)
}
}
if (message.type === "result") console.log(message.result)
}
Python, the same session:
import os, anyio
from claude_agent_sdk import query, ClaudeAgentOptions
env = {
**os.environ,
"ANTHROPIC_BASE_URL": "https://gw.selan.ai/anthropic",
"ANTHROPIC_AUTH_TOKEN": os.environ["SELAN_TOKEN"],
}
async def main():
async for message in query(
prompt="Find every TODO under src/ and write them to TODOS.md",
options=ClaudeAgentOptions(
env=env,
cwd="/tmp/scratch",
model="claude-opus-5",
permission_mode="bypassPermissions",
),
):
if message.type == "result":
print(message.result)
anyio.run(main)
cwdis the directory the session may work in. Give it a scratch directory rather than the process's own.permissionModedecides what happens when a tool asks.bypassPermissionsnever asks, which is the only workable setting when nobody is watching, and the reasoncwdmatters.modeltakes an Anthropic id or one of your company's, below.
Choosing what it may do
By default the session has Claude Code's whole tool set: reading and writing files,
running shell commands, searching the web. allowedTools narrows that to
a list you name:
options: {
env,
cwd: "/tmp/scratch",
model: "claude-opus-5",
permissionMode: "bypassPermissions",
allowedTools: ["Read", "Grep", "Glob"],
}
That session can read the tree and nothing else. An empty array leaves it with no tools at all, which turns the SDK into a plain one-turn call.
Narrowing is worth doing when you can name the tools the task needs. An agent handed
Bash can reach anything the process can, so a restriction expressed
anywhere above it is one the agent can walk around.
Selan does not change what a session may do locally. The gateway decides which credential is spent and on what. Which files a session touches is the SDK's business and yours — run unattended work in a container or a checkout you can throw away.
Connecting your own tools
Tools of your own go in over MCP. The SDK can start an in-process server, so a tool is a function and there is no second process to run:
import { createSdkMcpServer, tool, query } from "@anthropic-ai/claude-agent-sdk"
import { z } from "zod"
const server = createSdkMcpServer({
name: "billing",
tools: [
tool(
"invoice_total",
"Returns the total on an invoice, in cents.",
{ invoiceId: z.string() },
async ({ invoiceId }) => ({
content: [{ type: "text", text: String(await lookupTotal(invoiceId)) }],
})
),
],
})
for await (const message of query({
prompt: "What is the total on invoice INV-4021?",
options: { env, mcpServers: { billing: server }, permissionMode: "bypassPermissions" },
})) {
if (message.type === "result") console.log(message.result)
}
The model sees that tool as mcp__billing__invoice_total: the server name
and the tool name, joined. Name it in that full form if you are also narrowing the
built-in set with allowedTools.
An external MCP server is the same option with a different value — a command to spawn over stdio, or a URL. The SDK's MCP documentation covers both.
Tool definitions are prompt, and prompt is metered. Every tool you mount is sent on every turn of the session, so a large catalog costs on each one. The gateway records what your tool definitions weigh alongside the rest of the request.
Naming a model
Anthropic's own ids work unchanged — claude-opus-5,
claude-sonnet-5. To run a session on a model your company connected
through another provider, use the id Selan lists for it:
curl https://gw.selan.ai/anthropic/v1/models \
-H "Authorization: Bearer $SELAN_TOKEN"
Every row is a model this token may name, already in the form the SDK expects. Which credential serves it is decided per request, so the id is all your code needs to know.
What your company sees
The same as every other Selan client. Each turn is metered against the token that made it, the company-wide per-user spend limit applies, and the session appears in Logs under its own address.
One session is many turns rather than one request, so an agent that fails to terminate keeps spending until something stops it. Set a budget on the token before you leave one running unattended.