Skip to content
Install
Back to Guides

Is the Claude Agent SDK in TypeScript Type-Safe?

May 3, 2026
Paula Hingel
Paula Hingel
Is the Claude Agent SDK in TypeScript Type-Safe?

The Claude Agent SDK in TypeScript is type-safe when developers combine exported SDK types, Zod validation, and streaming event narrowing because each layer catches different failure modes before runtime.

The Claude Agent SDK in TypeScript provides compile-time type safety for agent definitions, tool schemas, and streaming responses through two official packages: @anthropic-ai/sdk (REST API client) and @anthropic-ai/claude-agent-sdk (autonomous agent runtime). Both ship with built-in type declarations, require Node 18+, and support Zod-based validation for runtime guarantees that match static types.

TL;DR

TypeScript agents built on the Claude SDK face a gap between API capabilities and type definitions that leads to silent any resolution and runtime failures. This guide covers typed agent configs, Zod tool schemas with strict enforcement, four streaming patterns with typed events, and discriminated union error handling. It also shows where Intent coordinates multiple Claude agents working in parallel across large TypeScript codebases.

Where TypeScript Type Safety Breaks Down in the Claude SDK

TypeScript developers building Claude-powered agents hit a specific frustration: the SDK's type surface evolves faster than its documentation. New tool types arrive without matching type definitions, the agent SDK can resolve types to any when the @anthropic-ai/sdk dependency is missing, and streaming events carry partial JSON that crashes JSON.parse on malformed escape sequences.

These failures show up in production. GitHub issue #864 documents the silent type resolution bug, issue #996 confirms the streaming JSON parse crash, and issue #939 tracks missing tool type definitions.

This guide walks through every layer of type safety available in the Claude Agent SDK for TypeScript: installation, agent definitions, Zod-validated tool schemas, four streaming patterns with typed events, and discriminated union error handling. The final section shows how Intent, the workspace for spec-driven development, coordinates multiple agents working in parallel across TypeScript codebases.

See how Intent's living specs keep parallel Claude agents aligned across cross-service TypeScript refactors.

Build with Intent

Free tier available · VS Code extension · Takes 2 minutes

Installation and Setup: Node 18+, TypeScript Config

The Claude Agent SDK for TypeScript can be installed on its own via the @anthropic-ai/claude-agent-sdk package; Anthropic's official instructions do not require installing @anthropic-ai/sdk together with it. The two packages serve different purposes and aren't interchangeable, so figure out which one you actually need before adding either to your project.

PackagePurposeLicenseEntry Point
@anthropic-ai/sdkREST API wrapper for Claude modelsMITclient.messages.create()
@anthropic-ai/claude-agent-sdkClaude Agent SDK for building agents with built-in tool executionAnthropic Commercial Termsquery()

Install the packages explicitly:

bash
npm install @anthropic-ai/sdk @anthropic-ai/claude-agent-sdk@0.2.123

The agent SDK re-exports aliased types from @anthropic-ai/sdk but does not declare it as a dependency. Without both packages in package.json, types can silently resolve to any, as described in issue #121. Verify both appear in your dependency list:

json
{
"dependencies": {
"@anthropic-ai/sdk": "^0.92.0",
"@anthropic-ai/claude-agent-sdk": "^0.2.123"
}
}

The @anthropic-ai/claude-agent-sdk accepts either Zod 3 (^3.24.1) or Zod 4 (^4.0.0) as a peer dependency, with Zod 4 support added in v0.1.71+ alongside continued Zod 3 compatibility. The SDK repository for @anthropic-ai/sdk includes tsconfig.build.json; consult the repo directly for canonical compiler settings. Both packages ship with built-in type declarations, so no separate @types/ package is needed.

Type-Safe Agent Definitions with the SDK

Type-safe agent definitions in the Claude SDK start with constraining model identifiers to verified string literals, then narrowing parameter types through Anthropic.MessageCreateParams and specialized config interfaces.

Model String Safety

The SDK accepts model strings as string for forward compatibility, but warns at runtime for deprecated models. A const object with literal types provides compile-time enforcement:

typescript
import Anthropic from '@anthropic-ai/sdk';
const CLAUDE_MODELS = {
OPUS_4_7: 'claude-opus-4-7',
SONNET_4_6: 'claude-sonnet-4-6',
HAIKU_4_5: 'claude-haiku-4-5',
OPUS_4_6: 'claude-opus-4-6',
} as const;
type ClaudeModel = (typeof CLAUDE_MODELS)[keyof typeof CLAUDE_MODELS];
const model: ClaudeModel = CLAUDE_MODELS.OPUS_4_7; // ✅
// const bad: ClaudeModel = 'claude-gpt-4'; // ❌ TS2322

Typed Agent Configurations

Anthropic.MessageCreateParams, Anthropic.Message, and Anthropic.Tool are real exported types from src/resources/messages/messages.ts. Specialized agent configs use TypeScript narrowing to restrict model and token combinations:

typescript
interface AgentConfig {
systemPrompt: string;
model: ClaudeModel;
maxTokens: number;
tools?: Anthropic.Tool[];
}
interface ResearchAgentConfig extends AgentConfig {
model: typeof CLAUDE_MODELS.OPUS_4_7 | typeof CLAUDE_MODELS.OPUS_4_6;
maxTokens: 8192 | 16384;
}
const researchAgent: ResearchAgentConfig = {
systemPrompt: 'You are a research specialist.',
model: 'claude-opus-4-7',
maxTokens: 8192,
};

Generic Typed Agent with Structured Output

The client.messages.parse() method combined with zodOutputFormat() from @anthropic-ai/sdk/helpers/zod infers the return type directly from a Zod schema, per the official example. The example below targets Zod 3; on Zod 4, replace zodOutputFormat(schema) with z.toJSONSchema(schema) directly, since zod-to-json-schema (which zodOutputFormat wraps) has documented incompatibilities with Zod 4 schemas:

typescript
import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod';
import { z, ZodType, infer as zodInfer } from 'zod';
async function typedAgent<TSchema extends ZodType>(
prompt: string,
schema: TSchema,
config: {
model: ClaudeModel;
systemPrompt: string;
maxTokens: number;
}
): Promise<zodInfer<TSchema> | null> {
const message = await client.messages.parse({
model: config.model,
max_tokens: config.maxTokens,
system: config.systemPrompt,
messages: [{ role: 'user', content: prompt }],
output_config: {
format: zodOutputFormat(schema),
},
});
return message.parsed_output ?? null;
}

TypeScript infers the full return shape from the Zod schema at compile time, so no separate interface definition is needed.

Known Type Safety Gaps

The adaptive thinking mode is a valid API value but does not appear in TypeScript type definitions yet, so a type assertion is required:

typescript
const params = {
model: 'claude-opus-4-7',
max_tokens: 16000,
thinking: { type: 'adaptive' },
} as unknown as Anthropic.MessageCreateParamsNonStreaming;

The same applies to newer tool types like tool_search_tool_regex_20251119, per issue #939.

Tool Schemas and Zod Validation Patterns

Tool schemas in the Claude API require JSON Schema format in the input_schema field, not Zod objects directly. Zod provides compile-time type inference through z.infer<> and runtime validation through parse() and safeParse(). The SDK also ships a betaZodTool helper that builds a runnable tool definition from a Zod schema and is designed to be passed into anthropic.beta.messages.toolRunner() for automatic execution.

Defining Schemas with Inferred Types

typescript
import { z } from 'zod';
const GetWeatherInputSchema = z.object({
location: z.string().describe('The city and state, e.g. San Francisco, CA'),
unit: z.enum(['celsius', 'fahrenheit']),
});
type GetWeatherInput = z.infer<typeof GetWeatherInputSchema>;
// => { location: string; unit: "celsius" | "fahrenheit" }

Converting Schemas to Claude Tool Definitions

Use zod-to-json-schema for the conversion. The default jsonSchema7 (draft-07) target produces output that the standard Messages API accepts directly, and adding strict: true guarantees Claude's tool calls always match the schema, per Anthropic's strict tool use docs:

typescript
import { zodToJsonSchema } from 'zod-to-json-schema';
function makeClaudeTool<T extends z.ZodObject<any>>(config: {
name: string;
description: string;
schema: T;
handler: (input: z.infer<T>) => Promise<string>;
}) {
return {
definition: {
name: config.name,
description: config.description,
input_schema: zodToJsonSchema(config.schema),
},
handler: async (rawInput: unknown): Promise<string> => {
const validated = config.schema.parse(rawInput);
return config.handler(validated);
},
};
}

This pattern targets the standard Messages API (client.messages.create()), which accepts draft-07 output. Tool schemas for Claude Code or MCP servers require JSON Schema draft-2020-12, so use Zod 4's native z.toJSONSchema(schema, { target: 'draft-2020-12' }) in those contexts instead of zod-to-json-schema.

The betaZodTool Helper

The SDK ships a built-in helper, documented in the official helpers reference, that combines schema conversion, type inference, and runtime validation in one call (added in v0.63.0+, per SDK discussion #817). betaZodTool is imported as a standalone function from @anthropic-ai/sdk/helpers, takes the Zod schema in an inputSchema field, and is then passed into anthropic.beta.messages.toolRunner():

typescript
import Anthropic from '@anthropic-ai/sdk';
// Also importable from '@anthropic-ai/sdk/helpers/beta/zod' (the path used in the SDK's example files)
import { betaZodTool } from '@anthropic-ai/sdk/helpers';
import { z } from 'zod';
const anthropic = new Anthropic();
const weatherTool = betaZodTool({
name: 'get_weather',
description: 'Get the current weather in a given location',
inputSchema: z.object({
location: z.string(),
unit: z.enum(['celsius', 'fahrenheit']).default('fahrenheit'),
}),
run: (input) => {
// input is fully typed from the Zod schema
return `The weather in ${input.location} is foggy and 60°F`;
},
});
const finalMessage = await anthropic.beta.messages.toolRunner({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages: [{ role: 'user', content: 'What is the weather in San Francisco?' }],
tools: [weatherTool],
});

The toolRunner handles the tool-use loop end to end: it calls messages.create, executes the matching run function with validated input, sends the result back to Claude, and repeats until the model returns a final message.

Type Safety by Layer

The mechanisms above each enforce safety at a different stage of the request lifecycle. The table below summarizes where each one catches errors:

MechanismEnforcement Point
z.infer<typeof Schema>Compile time: TypeScript errors on invalid field access
schema.parse(input)Runtime: throws ZodError with field-level issues array
schema.safeParse(input)Runtime: returns { success, error } without throwing
strict: true on a tool definitionModel output: schema conformance guaranteed by Claude, except in refusal cases
betaZodTool + toolRunnerCompile time + runtime: type inference from inputSchema, automatic invocation, validated input passed to run

Once multiple typed tools start running across a large TypeScript project, coordinating them becomes the harder problem. Intent's coordinator agent decomposes work into tasks and delegates to specialist agents in parallel waves so work proceeds in sequence without conflicts, with a living spec acting as the shared coordination layer for every Claude Code instance involved.

See how Intent coordinates parallel Claude agents around a shared, evolving spec.

Build with Intent

Free tier available · VS Code extension · Takes 2 minutes

ci-pipeline
···
$ cat build.log | auggie --print --quiet \
"Summarize the failure"
Build failed due to missing dependency 'lodash'
in src/utils/helpers.ts:42
Fix: npm install lodash @types/lodash

Streaming Responses: Handling Partial Outputs

Streaming responses from Claude-related streaming APIs are commonly represented with typed events such as message_start, content_block_start, content_block_delta, content_block_stop, message_delta, and message_stop, though the official TypeScript SDK example exposes higher-level .on('contentBlock', ...) and .on('message', ...) handlers rather than documenting that exact event sequence. The SDK provides four distinct patterns for consuming these events.

Pattern 1: Event Emitter with .on()

The client.messages.stream() method returns a MessageStream that accumulates the full message and exposes typed event callbacks:

typescript
const stream = client.messages.stream({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello' }],
});
stream.on('text', (textDelta: string, textSnapshot: string) => {
process.stdout.write(textDelta);
});
const finalMessage = await stream.finalMessage();

Pattern 2: Raw Async Iterator (Low-Memory)

Setting stream: true on client.messages.create() returns a Stream<RawMessageStreamEvent> that does not accumulate a message object, using less memory per the SDK helpers docs:

typescript
const stream = await client.messages.create({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello' }],
stream: true,
});
for await (const event of stream) {
if (
event.type === 'content_block_delta' &&
event.delta.type === 'text_delta'
) {
process.stdout.write(event.delta.text);
}
}

Partial Tool Call Accumulation

Tool arguments arrive as input_json_delta events that contain incomplete JSON fragments. Example code in Anthropic's fine-grained tool streaming docs shows how to accumulate input_json_delta fragments and parse them on content_block_stop, demonstrated as an implementation pattern rather than an explicit recommendation:

typescript
const toolAccumulators = new Map<number, { id: string; name: string; partialJson: string }>();
for await (const event of stream) {
if (event.type === 'content_block_start' && event.content_block.type === 'tool_use') {
toolAccumulators.set(event.index, {
id: event.content_block.id,
name: event.content_block.name,
partialJson: '',
});
}
if (event.type === 'content_block_delta' && event.delta.type === 'input_json_delta') {
const acc = toolAccumulators.get(event.index);
if (acc) acc.partialJson += event.delta.partial_json;
}
if (event.type === 'content_block_stop') {
const acc = toolAccumulators.get(event.index);
if (acc) {
try {
const toolInput = JSON.parse(acc.partialJson);
console.log(`Tool "${acc.name}" input:`, toolInput);
} catch (e) {
console.error('Failed to parse tool input JSON:', e);
}
}
}
}

Wrap every JSON.parse in try/catch within streaming pipelines. Issue #996 documents crashes on invalid escape sequences in the streaming path.

Type Guards for Stream Events

The SDK exposes streaming events that TypeScript can handle in a type-safe way:

typescript
import type { ContentBlockDeltaEvent, TextDelta, InputJSONDelta } from '@anthropic-ai/sdk/resources/messages';
function isTextDelta(delta: ContentBlockDeltaEvent['delta']): delta is TextDelta {
return delta.type === 'text_delta';
}
function isInputJsonDelta(delta: ContentBlockDeltaEvent['delta']): delta is InputJSONDelta {
return delta.type === 'input_json_delta';
}

Error Handling with Discriminated Unions

Error handling in Claude SDK TypeScript agents combines SDK instanceof checks for API-level errors with discriminated union types for application-level error routing. Some error classes extend Anthropic.APIError, while others, such as AbortError, extend the built-in Error class. The SDK automatically retries 429 and 5xx responses with exponential backoff (default max_retries=2), and max_retries is a configuration option that controls this behavior.

SDK Error Class Hierarchy

The official SDK exposes API error classes mapped to HTTP status codes. The exact mappings and retryability behavior described below could not be fully verified from official Anthropic documentation or source code, so confirm against the version you're running:

HTTP StatusTypeScript ClassRetryable
400Anthropic.BadRequestErrorNo
401Anthropic.AuthenticationErrorNo
403Anthropic.PermissionDeniedErrorNo
404Anthropic.NotFoundErrorNo
429Anthropic.RateLimitErrorYes
500Anthropic.InternalServerErrorYes
529Anthropic.OverloadedErrorYes

instanceof checks must run most-specific to least-specific. RateLimitError must precede APIError, or the base class absorbs all subclasses.

Application-Level Discriminated Unions

A Result<T, E> type with a literal kind discriminant enables exhaustive switch statements that produce compile-time errors when new variants are added:

Open source
augmentcode/auggie205
Star on GitHub
typescript
type Result<T, E = ToolError> =
| { kind: 'success'; data: T }
| { kind: 'error'; error: E };
type ToolError =
| { kind: 'validation'; field: string; message: string }
| { kind: 'execution'; toolName: string; message: string }
| { kind: 'api'; statusCode: number; retryable: boolean; message: string };
function assertNever(x: never): never {
throw new Error(`Unhandled discriminant: ${JSON.stringify(x)}`);
}
function handleToolError(error: ToolError): string {
switch (error.kind) {
case 'validation':
return `Validation failed on '${error.field}': ${error.message}`;
case 'execution':
return `Tool '${error.toolName}' failed: ${error.message}`;
case 'api':
return `API error ${error.statusCode}: ${error.message}`;
default:
return assertNever(error);
}
}

Adding a new variant to ToolError without a matching case produces a TypeScript error at the assertNever call, per TypeScript's narrowing documentation. The compiler then flags every error path that lacks a handler.

Surfacing Errors to Claude

When a tool call fails, return is_error: true on the ToolResultBlockParam. Claude uses this flag to reason about the failure and adjust its next action:

typescript
function toolResultToContent(
toolUseId: string,
result: Result<unknown, ToolError>
): Anthropic.ToolResultBlockParam {
if (result.kind === 'success') {
return { type: 'tool_result', tool_use_id: toolUseId, content: JSON.stringify(result.data) };
}
return {
type: 'tool_result',
tool_use_id: toolUseId,
is_error: true,
content: handleToolError(result.error),
};
}

Scaling TypeScript Agents with Intent

Individual Claude agents built with the patterns above work well for single-task execution. Production TypeScript codebases with hundreds of files and cross-service dependencies require coordination across multiple agents working simultaneously, where each agent holds a subset of context.

Intent is the workspace where that coordination happens. It accepts Claude Code (built on @anthropic-ai/claude-agent-sdk) as a delegated agent runtime through a BYOA (Bring Your Own Agent) model, so existing Anthropic subscriptions cover model usage while Intent handles orchestration on an Augment Code account.

How Intent Coordinates TypeScript Agents

Intent uses a three-role architecture, with each role mapped to a separate agent and a shared living spec:

  • Coordinator: Analyzes the codebase, drafts a living spec, decomposes tasks into a dependency-ordered DAG, then delegates to specialist agents in parallel batches
  • Implementor agents: Execute subtasks simultaneously in isolated git worktrees so parallel execution avoids filesystem conflicts
  • Verifier: Reads the spec and validates that implementations match before handoff

Intent ships with six built-in specialist personas (Investigate, Implement, Verify, Critique, Debug, Code Review) and supports custom agents defined as Markdown files with YAML front matter in .augment/agents/. Every agent is backed by the Context Engine, which is also exposed as an MCP server for third-party agent access.

Living Specs and CI/CD Integration

Living specs auto-update as agents complete work, so every agent reads the current state of the implementation rather than a stale PRD. When requirements change mid-flight, updates propagate to all active agents.

The Auggie CLI (npm install -g @augmentcode/auggie) enables headless execution in CI/CD pipelines. A GitHub Actions workflow can call Auggie to run spec-driven workflows alongside existing test stages.

Coordinate Multi-Agent TypeScript Work with a Shared Spec

The Claude Agent SDK gives TypeScript developers type safety through Zod-validated tool schemas, typed agent configs, and discriminated union error handling. The friction shows up when a single agent's context runs out before the task does. Cross-service TypeScript refactors, parallel implementation across isolated worktrees, and spec-driven coordination call for an orchestration layer that keeps multiple Claude agents aligned on shared, evolving requirements.

Intent's living specs and coordinator/implementor/verifier architecture keep independent Claude agents aligned as the plan changes, with each agent starting from the architectural context the codebase already contains.

See how Intent's living specs keep parallel agents aligned as TypeScript requirements evolve.

Build with Intent

Free tier available · VS Code extension · Takes 2 minutes

FAQ

Written by

Paula Hingel

Paula Hingel

Paula writes about the patterns that make AI coding agents actually work — spec-driven development, multi-agent orchestration, and the context engineering layer most teams skip. Her guides draw on real build examples and focus on what changes when you move from a single AI assistant to a full agentic codebase.

Get Started

Give your codebase the agents it deserves

Install Augment to get started. Works with codebases of any size, from side projects to enterprise monorepos.