Skip to content

Claude Agent SDK in TypeScript: A Practical Tutorial

Claude Agent SDK in TypeScript: A Practical Guide

Build a working AI agent with the Claude Agent SDK in TypeScript: install it, stream messages, add a custom Zod tool, and pick a safe permission mode.

· · 7 min read
Multiple monitors displaying colorful lines of code in a neon-lit programming workspace

Quick Take

The Claude Agent SDK gives you Claude Code's agent loop as a TypeScript library, not just a CLI. This guide installs it, streams its first response, adds a custom tool with a Zod schema, and covers the permission mode you actually want in production.

I broke my first Claude Agent SDK script by leaving permissionMode on its default and handing the agent a repo with a half-finished migration script sitting in it. It ran npm run migrate without asking, because I'd set allowedTools too wide. Nothing was lost, it was a throwaway branch, but it's the fastest way to learn that this SDK gives you a real agent, not a chatbot wrapper.

Quick take: The Claude Agent SDK (@anthropic-ai/claude-agent-sdk) exposes Claude Code's actual agent loop as a TypeScript library: multi-turn reasoning, built-in tools, and session state, callable from query() instead of a CLI. Add your own tools with tool() and a Zod schema, and keep allowedTools explicit until you trust what you've built.

What Is the Claude Agent SDK, and How Is It Different From the Claude API?

The plain Claude API gives you one thing: a request goes in, a response comes back. Multi-step reasoning, tool calls, and file access are all things you build yourself on top of it. The Claude Agent SDK is a different layer entirely. It's the same agent loop that powers Claude Code, packaged as an npm library you call from your own Node process.

Install it with npm install @anthropic-ai/claude-agent-sdk. That single package gives you query(), the built-in tool set (Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch), permission hooks, and session resume, all without shelling out to a CLI binary. If you've used Claude Code from the terminal, this is that same engine, just embedded in your own TypeScript code instead of a chat interface.

How Do You Get an Anthropic API Key?

You need a key before any of the code below will run. Here's the fast path:

  1. Go to console.anthropic.com and sign in, or create an account if you don't have one yet.
  2. Open Settings -> API Keys in the left sidebar.
  3. Click Create Key, give it a name (something like agent-sdk-tutorial so you remember what it's for), and confirm.
  4. Copy the key immediately. It starts with sk-ant- and Anthropic only shows the full value once.
  5. Add billing under Settings -> Billing if the workspace doesn't already have credit. Agent SDK calls bill the same as regular Claude API usage, per token, not a flat fee.
  6. Store the key as an environment variable, never in source code: export ANTHROPIC_API_KEY=sk-ant-... in your shell, or a .env file that's in .gitignore.

Treat the key like a password. Anyone who has it can run up your bill, so keep it out of client-side JavaScript, screenshots, and public repos. Two habits are worth adopting from the first key you create. Scope a separate key per project rather than reusing one everywhere, because revoking a leaked key is instant and painless when only one service depends on it. And set a spend limit on the workspace under Billing before you start iterating, since an agent loop that retries a failing tool call can burn through tokens far faster than a chat session ever will. The Anthropic documentation recommends rotating keys on a schedule too, which sounds like security theatre until the first time a key ends up in a committed .env file and you need to know exactly which one to kill.

How Do You Install and Run Your First Agent?

Set your API key, install the package, and call query(). It returns an async generator, so you stream messages with a plain for await loop:

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "List the TypeScript files in this project and summarize what each one does.",
  options: {
    cwd: process.cwd(),
    allowedTools: ["Read", "Glob"],
    permissionMode: "default",
  },
})) {
  if (message.type === "assistant") {
    console.log(message.message);
  }
  if (message.type === "result") {
    console.log("done:", message);
  }
}

Two options matter more than the rest on your first run. allowedTools is a hard allowlist, anything not on it gets rejected before it executes, not just flagged. cwd scopes the built-in file tools to that directory, so Read and Glob can't wander outside your project by accident. Leave both unset and the agent inherits broader defaults than you probably want for a first test. The third option, permissionMode, is the one to leave alone until you understand the other two, default prompts before anything writes, which is exactly the behaviour you want while you're still learning what the agent decides to do on its own. Expect the first run on a mid-sized repo to take somewhere between 15 and 40 seconds, most of it spent reading files rather than generating text.

How Do You Give the Agent a Custom Tool?

Built-in tools cover file access and shell commands. For anything domain-specific, an internal API, a database query, a linter you wrote, you define your own with tool() and a Zod schema:

import { tool, createSdkMcpServer, query } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";

const lintCheck = tool(
  "lint_check",
  "Run the project's ESLint config against a file path and return violations",
  { filePath: z.string() },
  async ({ filePath }) => {
    const violations = await runEslint(filePath); // your own function
    return {
      content: [{ type: "text", text: JSON.stringify(violations) }],
    };
  },
);

const devTools = createSdkMcpServer({
  name: "dev-tools",
  tools: [lintCheck],
});

for await (const message of query({
  prompt: "Check src/index.ts for lint violations and explain the worst one.",
  options: {
    mcpServers: { "dev-tools": devTools },
    allowedTools: ["mcp__dev-tools__lint_check"],
  },
})) {
  console.log(message);
}

The Zod schema does double duty here. It validates the arguments Claude sends before your handler ever runs, and TypeScript infers the handler's argument types straight from it, so filePath is a string with zero manual typing. The description string is the only thing the model sees when deciding whether to call this tool, so write it like documentation, not a variable name. Note the naming convention on the allowlist too: in-process tools get addressed as mcp__<server>__<tool>, and getting that string wrong is the single most common reason a custom tool silently never fires. The agent just won't have it available, and nothing in the output says why. When I first wired up 3 internal tools this way, two of them sat unused for an hour because I'd written the server name with a hyphen in one place and an underscore in the other.

What Permission Modes Should You Use in Production?

Four modes exist: default (each unlisted tool asks for permission, allowlisted tools run freely), plan (the agent proposes actions without executing anything), acceptEdits (file edits auto-approve, everything else still asks), and bypassPermissions (nothing asks, ever). The gap between default and bypassPermissions is the whole safety story here.

For a CI script or a scheduled job, don't reach for bypassPermissions just because there's no human around to click "approve." Set allowedTools to the exact list the job needs and leave permissionMode on default, that combination gives you the same unattended behavior with a hard boundary instead of an honor system. Reserve bypassPermissions for sandboxed environments where a wrong file write genuinely can't matter, a throwaway container, not a real repo.

canUseTool is the escape hatch when an allowlist alone isn't precise enough. It's an async callback you pass in options that inspects the actual tool call, so you can allow Bash in general but deny any command string containing rm -rf, something a static allowlist can't express.

Where Does the SDK Fit Next to Your Other TypeScript Agent Tooling?

If you've already built MCP servers for Claude Code or Cursor, nothing here replaces them, mcpServers in the query() options accepts the same external MCP servers over stdio or HTTP, alongside your in-process tool() definitions. The full mechanics of that protocol, including resources and the newer authorization flow, are in the MCP guide. And if you're comparing this against a framework-first approach instead of Anthropic's own SDK, Mastra and the Vercel AI SDK covers that other path, provider-agnostic, more scaffolding, less tied to Claude specifically.

The honest tradeoff: the Claude Agent SDK gets you Claude Code's actual agent loop with almost no glue code, at the cost of being Anthropic-specific. If your product is committed to Claude anyway, that's not a real cost. If you need to swap models later, budget the time to rebuild this layer on something provider-agnostic. For a rough sense of scale, the working example in this guide is under 40 lines of TypeScript across 2 files, and the equivalent framework setup runs closer to 3 files plus a config module before the first agent call happens.

Frequently Asked Questions

What is the Claude Agent SDK, and how is it different from the Claude API?
The Claude API's Messages endpoint gives you one request, one response. You handle the loop, the tool-calling, and the file access yourself. The Claude Agent SDK wraps that same model access in the actual agent loop Claude Code runs: multi-turn reasoning, built-in tools (Read, Write, Edit, Bash, Glob, Grep, WebSearch), permission handling, and session management, all exposed as a TypeScript library instead of a CLI-only tool. Install it with npm install @anthropic-ai/claude-agent-sdk and you're calling query() in your own Node process, not shelling out to a binary.
Do I need an Anthropic API key to use the Claude Agent SDK?
Yes, the SDK authenticates the same way the Claude API does, through your Anthropic API key (or a Claude Code subscription session if you're running it locally with the CLI already authenticated). Set ANTHROPIC_API_KEY in your environment before calling query(), the same variable the plain Anthropic TypeScript SDK reads. Requests bill against your Claude API usage, so a long-running agent with a high maxTurns value can run up real cost if you don't cap it.
How do I stop the agent from running arbitrary Bash commands?
Set allowedTools to an explicit list instead of leaving it open, and keep permissionMode on 'default' rather than 'bypassPermissions'. With allowedTools: ['Read', 'Grep'] the agent physically cannot invoke Bash or Write, the SDK rejects the tool call before it runs. If you need Bash for a specific task, scope it with a canUseTool callback that inspects the command string and denies anything outside an allowlist, rather than trusting the model's judgment alone.
Can I give the Claude Agent SDK my own custom tools?
Yes, that's most of the value over the plain CLI. The tool() helper takes a name, a description, a Zod schema for the input, and an async handler that returns { content: [...] }. Wrap one or more with createSdkMcpServer() and pass it into the mcpServers option on query(). The agent sees your tool exactly like a built-in one, the model decides when to call it based on your description string, so a vague description produces vague tool-calling behavior, and a specific one produces reliable results.
Is the Claude Agent SDK the same thing as MCP?
No, they're complementary. MCP (Model Context Protocol) is the open standard for exposing tools and resources to any AI client, Claude Code, Cursor, Claude Desktop, all speak it. The Claude Agent SDK is Anthropic's own library for running Claude's agent loop from TypeScript code, and it happens to have first-class MCP client support built in, so an agent built with the SDK can call both your in-process tool() functions and any external MCP server over stdio or HTTP in the same conversation.