tools · tweet · 7 min
Claude Agent SDK for Building AI Agents
nader dabit · Jan 13, 2026
If you've used Claude Code, you've seen what an AI agent can actually do: read files, run commands, edit code, figure out the steps to accomplish a task.
And you know it doesn't just help you write code, it takes ownership of problems and works through them the way a thoughtful engineer would.
The Agent SDK is the same engine, yours to point at whatever problem you want, so you can easily build agents of your own.
It's the infrastructure behind Claude Code, exposed as a library. You get the agent loop, the built-in tools, the context management, basically everything you'd otherwise have to build yourself.
This guide walks through building a code review agent from scratch. By the end, you'll have something that can analyze a codebase, find bugs and security issues, and return structured feedback.
More importantly, you'll understand how the SDK works so you can build whatever you actually need.
Our code review agent will:
- Analyze a codebase for bugs and security issues
- Read files and search through code autonomously
- Provide structured, actionable feedback
- Track its progress as it works
Tech Stack: Runtime - Claude Code, SDK - @anthropic-ai/claude-agent-sdk, Language - TypeScript, Model - Claude Opus 4.5
The Agent Loop Problem
If you've built agents with the raw API, you know the pattern: call the model, check if it wants to use a tool, execute the tool, feed the result back, repeat until done. This can get tedious when building anything non-trivial.
The SDK handles that loop:
// Without the SDK: You manage the loop
let response = await client.messages.create({...});
while (response.stop_reason === "tool_use") {
const result = yourToolExecutor(response.tool_use);
response = await client.messages.create({ tool_result: result, ... });
}
// With the SDK: Claude manages it
for await (const message of query({ prompt: "Fix the bug in auth.py" })) {
console.log(message); // Claude reads files, finds bugs, edits code
}
You also get working tools out of the box:
- Read — read any file in the working directory
- Write — create new files
- Edit — make precise edits to existing files
- Bash — run terminal commands
- Glob — find files by pattern
- Grep — search file contents with regex
- WebSearch — search the web
- WebFetch — fetch and parse web pages
You don't have to implement any of this yourself.
Prerequisites
- Node.js 18+ installed
- An Anthropic API key
Step 1: Install Claude Code CLI
The Agent SDK uses Claude Code as its runtime:
npm install -g @anthropic-ai/claude-code
After installing, run claude in your terminal and follow the prompts to authenticate.
Step 2: Create your project
mkdir code-review-agent && cd code-review-agent
npm init -y
npm install @anthropic-ai/claude-agent-sdk
npm install -D typescript @types/node tsx
Step 3: Set your API key
export ANTHROPIC_API_KEY=your-api-key
Create agent.ts:
import { query } from "@anthropic-ai/claude-agent-sdk";
async function main() {
for await (const message of query({
prompt: "What files are in this directory?",
options: {
model: "opus",
allowedTools: ["Glob", "Read"],
maxTurns: 250
}
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if ("text" in block) {
console.log(block.text);
}
}
}
if (message.type === "result") {
console.log("\nDone:", message.subtype);
}
}
}
main();
Run it:
npx tsx agent.ts
Claude will use the Glob tool to list files and tell you what it found.
Understanding Message Types
The query() function returns an async generator that streams messages as Claude works. Here are the key message types:
for await (const message of query({ prompt: "..." })) {
switch (message.type) {
case "system":
// Session initialization info
if (message.subtype === "init") {
console.log("Session ID:", message.session_id);
console.log("Available tools:", message.tools);
}
break;
case "assistant":
// Claude's responses and tool calls
for (const block of message.message.content) {
if ("text" in block) {
console.log("Claude:", block.text);
} else if ("name" in block) {
console.log("Tool call:", block.name);
}
}
break;
case "result":
// Final result
console.log("Status:", message.subtype); // "success" or error type
console.log("Cost:", message.total_cost_usd);
break;
}
}
Building the Code Review Agent
Now let's build something useful. Create review-agent.ts:
import { query } from "@anthropic-ai/claude-agent-sdk";
async function reviewCode(directory: string) {
console.log(`\n🔍 Starting code review for: ${directory}\n`);
for await (const message of query({
prompt: `Review the code in ${directory} for:
1. Bugs and potential crashes
2. Security vulnerabilities
3. Performance issues
4. Code quality improvements
Be specific about file names and line numbers.`,
options: {
model: "opus",
allowedTools: ["Read", "Glob", "Grep"],
permissionMode: "bypassPermissions", // Auto-approve read operations
maxTurns: 250
}
})) {
// Show Claude's analysis as it happens
if (message.type === "assistant") {
for (const block of message.message.content) {
if ("text" in block) {
console.log(block.text);
} else if ("name" in block) {
console.log(`\n📁 Using ${block.name}...`);
}
}
}
// Show completion status
if (message.type === "result") {
if (message.subtype === "success") {
console.log(`\n✅ Review complete! Cost: ${message.total_cost_usd.toFixed(4)}`);
} else {
console.log(`\n❌ Review failed: ${message.subtype}`);
}
}
}
}
// Review the current directory
reviewCode(".");
Testing It Out
Create a file with some intentional issues. Create example.ts:
function processUsers(users: any) {
for (let i = 0; i <= users.length; i++) { // Off-by-one error
console.log(users[i].name.toUpperCase()); // No null check
}
}
function connectToDb(password: string) {
const connectionString = `postgres://admin:${password}@localhost/db`;
console.log("Connecting with:", connectionString); // Logging sensitive data
}
async function fetchData(url) { // Missing type annotation
const response = await fetch(url);
return response.json(); // No error handling
}
Run the review:
npx tsx review-agent.ts
Claude will identify the bugs, security issues, and suggest fixes.
Structured Output with JSON Schema
For programmatic use, you'll want structured data. The SDK supports JSON Schema output:
import { query } from "@anthropic-ai/claude-agent-sdk";
const reviewSchema = {
type: "object",
properties: {
issues: {
type: "array",
items: {
type: "object",
properties: {
severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
category: { type: "string", enum: ["bug", "security", "performance", "style"] },
file: { type: "string" },
line: { type: "number" },
description: { type: "string" },
suggestion: { type: "string" }
},
required: ["severity", "category", "file", "description"]
}
},
summary: { type: "string" },
overallScore: { type: "number" }
},
required: ["issues", "summary", "overallScore"]
};
async function reviewCodeStructured(directory: string) {
for await (const message of query({
prompt: `Review the code in ${directory}. Identify all issues.`,
options: {
model: "opus",
allowedTools: ["Read", "Glob", "Grep"],
permissionMode: "bypassPermissions",
maxTurns: 250,
outputFormat: {
type: "json_schema",
schema: reviewSchema
}
}
})) {
if (message.type === "result" && message.subtype === "success") {
const review = message.structured_output as {
issues: Array<{
severity: string;
category: string;
file: string;
line?: number;
description: string;
suggestion?: string;
}>;
summary: string;
overallScore: number;
};
console.log(`\n📊 Code Review Results\n`);
console.log(`Score: ${review.overallScore}/100`);
console.log(`Summary: ${review.summary}\n`);
for (const issue of review.issues) {
const icon = issue.severity === "critical" ? "🔴" :
issue.severity === "high" ? "🟠" :
issue.severity === "medium" ? "🟡" : "🟢";
console.log(`${icon} [${issue.category.toUpperCase()}] ${issue.file}${issue.line ? `:${issue.line}` : ""}`);
console.log(` ${issue.description}`);
if (issue.suggestion) {
console.log(` 💡 ${issue.suggestion}`);
}
console.log();
}
}
}
}
reviewCodeStructured(".");
Permission Control
By default, the SDK asks for approval before executing tools. You can customize this:
Permission modes
options: {
// Standard mode - prompts for approval
permissionMode: "default",
// Auto-approve file edits
permissionMode: "acceptEdits",
// No prompts (use with caution)
permissionMode: "bypassPermissions"
}
Custom permission handler
For fine-grained control, use canUseTool:
options: {
canUseTool: async (toolName, input) => {
// Allow all read operations
if (["Read", "Glob", "Grep"].includes(toolName)) {
return { behavior: "allow", updatedInput: input };
}
// Block writes to certain files
if (toolName === "Write" && input.file_path?.includes(".env")) {
return { behavior: "deny", message: "Cannot modify .env files" };
}
// Allow everything else
return { behavior: "allow", updatedInput: input };
}
}
(capture appears truncated)