Shipping a Claude Agent SDK agent to customers
The Claude Agent SDK gives you Anthropic's agent loop, tools, permission modes and hooks. What you add when the agent serves customers across accounts.
Keep the Claude Agent SDK. It gives you Anthropic's own agent loop, a file and shell tool set, permission modes, hooks and subagents, and it is the shortest path from an idea to an agent that does real work. The part it leaves to you is the layer a customer-facing agent needs: tenant identity, per-account configuration, cost attributed to an account, and surfaces beyond a terminal.
This page is written for a team that has already chosen the SDK, has it working against internal use, and now has to put it in front of paying accounts. Nothing below asks you to move the loop.
What the Claude Agent SDK does well
The loop is the product. Anthropic ships the same harness that drives Claude Code, so the turn structure, the tool-result handling and the context compaction behavior are maintained by the people who train the model against them. A hand-rolled loop over the Messages API reaches a first tool call quickly and then costs months in edge cases: partial tool results, interleaved thinking, a tool that returns two megabytes of JSON, a turn that has to summarize itself to keep going.
The tool set is opinionated in a useful way. Read, Write and Edit treat a filesystem as the agent's working memory, Glob and Grep search it, Bash runs commands, WebFetch and WebSearch reach outward, and ToolSearch lets the model find a tool instead of carrying every schema in context. Subagent dispatch is itself a tool, so a parent can only delegate when you put Agent in the allowed list, and code that watches for a dispatch has to match both Agent and the Task name the session's init message still reports. You narrow the set per run with allowedTools and disallowedTools rather than by writing wrappers.
Permissions are a first-class option. permissionMode selects the posture for a run, with the values default, acceptEdits, plan, dontAsk, bypassPermissions and auto, and a canUseTool callback resolves any call the mode and the allow rules did not, returning { behavior: 'allow' } with optionally rewritten input or { behavior: 'deny' } with a message that can interrupt the run. A call the mode or allowedTools already approved skips that callback, so a gate that has to see every call belongs in a PreToolUse hook, whose decision can be allow, deny, ask or defer.
Hooks cover the rest of the lifecycle. PreToolUse and PostToolUse bracket tool execution, PostToolUseFailure fires when one errors, and further events cover prompt submission, subagent start and stop, permission requests and compaction, which is where audit logging and redaction belong. Each event takes a list of matchers, so a hook can target Write|Edit and leave read-only tools alone.
Subagents are defined in an agents map with their own description, prompt, tool list and model, so a research step runs on a cheaper model with read-only tools while the parent keeps write access. MCP servers attach over stdio or HTTP, and the TypeScript SDK can define one in-process with createSdkMcpServer and a tool helper, so a tool runs inside your own program with no transport hop.
That is a serious agent runtime, and none of the work below replaces any of it.
The seam
The SDK is an opinionated harness where Anthropic owns the loop and you own the process it runs in. Both SDKs bundle a native Claude Code binary and run the loop in a subprocess, so a web tier serving many conversations at once is managing one child process and one working directory per live conversation. Every assumption points at one operator: one machine, one working directory, one set of credentials, one person reading the transcript. Four things change when the agent stops being internal.
Identity does not exist below your call site. A query takes a prompt and options. There is no tenant on the session, no end user on a tool call, and no way for a hook to ask which account it is serving except by reading a variable your own code put in scope. Isolation is whatever the surrounding process arranged, which usually means a per-tenant working directory and a hope that no tool escapes it. Bash can reach anything the process can reach, including the other tenant directories beside it.
Per-account configuration becomes application code. A customer-facing agent varies by account: a different system prompt on a premium tier, a tool enabled only for accounts that connected their CRM, a cheaper model on the free plan. In the SDK those are fields on an options object, so "which configuration does account 4471 get" is a function in your codebase, versioned with your deploys. Changing one account's prompt means a release.
Cost arrives per result, not per account. The terminating result message carries total_cost_usd, token usage, a modelUsage breakdown that covers subagents, num_turns and the session_id. That is enough to log a number, and maxBudgetUsd ends a query once the client-side cost estimate reaches a dollar figure you set. Aggregating by account, separating cached from uncached tokens across a month, and holding one account to a budget that spans more than a single query are yours to build.
A terminal is not a surface. Sessions are JSONL transcript files on local disk under ~/.claude/projects/, keyed by the encoded working directory. There is no hosted session store, so a deployment across several hosts or serverless invocations needs a session-store adapter that mirrors transcripts into a backend you operate. On top of that sit the things a customer actually touches: a chat widget in your product, a Slack app in their workspace, an endpoint their backend calls, a nightly run that reports back. Each needs its own auth model and its own idea of who the end user is.
None of that is a criticism of the SDK. It describes where its scope ends, and the same four items end every agent framework's scope, which is the argument made at length in the agent framework comparison.
How to close it
The work divides cleanly. Keep the loop and the tool semantics in the SDK, and put identity, configuration, telemetry and surfaces in a layer around it.
Inject tenant context above the loop, enforce it below
Resolve the account at your HTTP edge, before any SDK call, and carry it in a typed context object rather than a global. Then use it in four places.
- Workspace. Set the working directory per tenant, created fresh per run, and never share one between accounts. Anything the file tools write is visible to the next turn in that directory, and the transcript path is derived from it.
- Tool pool. Compute
allowedToolsfrom the account's entitlements instead of a constant. An account without a connected CRM should not see the CRM tool at all, since a tool the model can see is a tool it will try. - Permission callback. Put the authorization check in
canUseToolor aPreToolUsehook, where the tool name and the resolved arguments are both available. A prompt instruction telling the model to stay in its lane is a suggestion. A callback that denies aBashcommand touching a path outside the tenant workspace is a boundary. - Credentials. Resolve API keys per account at the point of the tool call, and keep them out of the system prompt and out of the tool schema. A key that reaches the model can be echoed back in an assistant message.
One rule holds all four together: the model never receives the tenant id in a form it can act on. It sees data already filtered for that account, and every call it makes is rechecked against the context your edge resolved. The concrete list of what to check is in the tenant isolation checklist.
Instrument the loop as spans, not log lines
The SDK emits no OpenTelemetry of its own, and a JSONL transcript is a debugging aid for one run on one disk. What production needs is a queryable trace per run, correlated with the account it served. Wrap each query in a span, emit a child span per tool call carrying arguments and result, and attach the account and end user as attributes on the root span so children inherit them. Record total_cost_usd and the usage fields off the result message onto the same span, so behavior and spend sit in one object rather than two dashboards.
Then export the spans somewhere durable. That is an OTLP exporter and an endpoint:
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.runtype.com/v1/otel
OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer rt_YOUR_API_KEY,x-runtype-agent-id=agent_YOUR_AGENT_ID
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OpenTelemetry appends /v1/traces to that base endpoint. Set the protocol explicitly if your language's exporter defaults to gRPC. The step-by-step version, including which attributes carry tenant and end-user identity and what a useful tool-call span looks like, is in instrumenting an agent you built elsewhere.
Keep in the SDK
Leave the agent loop, context compaction, the file and shell tools, the permission modes, the hook events and subagent dispatch where they are. They are maintained against the model, and reimplementing them buys nothing. Move only the parts that are about your customers rather than about the agent: who is asking, what they are allowed to reach, what it cost them, and how they reach it at all.
Where Runtype fits
Runtype is where a Claude Agent SDK deployment gets its tenants, its surfaces and its spend record, and Anthropic's harness keeps the loop. The change that buys the most is registering the agent: give the process an endpoint that speaks Runtype's unified stream or A2A, create an external agent pointing at it, and Runtype calls your loop. The same agent can then be embedded in the open-source Persona chat widget, added to a product as a capability reached through web chat, Slack, a REST API, SMS, iMessage, MCP and A2A, and put on a schedule. Query options, hooks and canUseTool stay as they are, and tool calls and cost are recorded per run (bring your own agent).
Sending traces is the smaller first step and composes with that. The spans you wrap around each query, exported over OTLP to https://api.runtype.com/v1/otel, become runs carrying the trace tree, structured logs, tool calls with arguments and results, token usage and a display-only cost estimate. A run whose spans carry the GenAI content attributes has a transcript and can be captured as an eval case, which is how a prompt change gets measured. Anthropic still bills you for the tokens, and exactly one instrumentation should point at the endpoint, because two doubles the count.
The third way in points tools at your loop instead. An MCP surface exposes a product's flows, agents, records, skills and tools as MCP tools, which the SDK attaches over HTTP like any other MCP server, so the loop stays the orchestrator. The fourth moves one capability into a native flow or hosted agent, once a suite harvested from real runs can prove parity; a rebuilt capability is versioned, published and gated by runtype eval run in CI, which returns a non-zero exit code on a regression.
The identity model is the piece the SDK leaves entirely to your calling code. Each resource declares a tenancy strategy of internal, tenant-isolated or end-user-isolated with an assurance floor of asserted or verified, evaluated before execution, so a request whose identity scope does not clear the floor is rejected before it runs. Every trace and every cost figure is filed under the tenant and end user the run served, and long-term memory, when enabled, is keyed per agent, tenant or end user (end-user identity).
Per-account governance sits in the same layer. Approval gates pause a tool call for a human with a timeout that defaults to 5 minutes, maxToolCalls bounds a turn (default 10, maximum 100), loopConfig.maxTurns bounds a run, and an optional loopConfig.maxCost caps the accumulated spend of a run in US dollars, checked between turns. Code the agent writes runs in a Linux sandbox with configurable network access, and the runtime can be self-hosted on your own infrastructure when the data cannot leave.
Frequently asked questions
- Does the Claude Agent SDK support multi-tenancy?
- Not as a concept. A query takes options, and one of those options is a working directory, so isolation is whatever your process arranges before the call. There is no tenant field on a session, no per-tenant store, and no rule that a tool cannot read another account's data. You supply that in the calling code and enforce it in the permission callback or a PreToolUse hook.
- Can I keep the Claude Agent SDK and still get per-customer cost reporting?
- Yes. The result message that ends a query carries `total_cost_usd` and token usage, so you can record it against whichever account the request belonged to. What you build around that is the aggregation, the retention and the per-account policy. The SDK's own `maxBudgetUsd` ends a single query at a dollar estimate, and tracking what one account spent across many queries is yours.
- Do I have to rewrite the agent to run it somewhere else?
- No, and rewriting is usually the wrong move if the loop already behaves. The cheaper path is to leave the loop alone and export OpenTelemetry spans to a platform that stores traces, cost and evals, then move individual capabilities only when a specific surface or governance requirement makes it worth doing.
- Where does the Claude Agent SDK store conversation history?
- In JSONL transcript files on the local disk, under `~/.claude/projects/` keyed by the encoded working directory (or under `CLAUDE_CONFIG_DIR` when that is set). There is no hosted session store. A deployment spread across several hosts or serverless invocations has to implement a session-store adapter that mirrors transcripts to a backend you run.