Claude Code SDK Mode: --print, JSON Output, Programmatic Invocation and Automation Integration
Chapter 47: Claude Code SDK: Programmatically Driving AI Programming Agents
47.1 What the SDK Is For: Embedding AI in Your Application
Claude Code itself is a command-line tool, but Anthropic provides the @anthropic-ai/claude-code npm package, which lets you call Claude Code's full capabilities programmatically from your own Node.js or TypeScript applications.
This unlocks an entirely new class of use cases: embedding an AI programming agent into existing tools, platforms, and automation systems.
With the SDK you can:
- Provide an "AI code assistant" feature in a web application where users upload code and the AI analyzes and returns improvement suggestions
- Build batch code processing pipelines (for example, bulk-adding type annotations to a legacy codebase)
- Create custom AI programming workflows integrated into your internal development tools
- Build a GUI frontend for Claude Code
Compared to calling the Anthropic Messages API directly, the core advantage of the Claude Code SDK is that it has built-in, complete tool-calling capability (read file, write file, run command) and agent orchestration logic. You do not need to build this infrastructure from scratch.
47.2 Installation and Basic Setup
# Install the SDK
npm install @anthropic-ai/claude-code
# Or with pnpm
pnpm add @anthropic-ai/claude-code
The SDK requires Node.js 18+ and the Claude Code CLI to be installed on the system (the SDK calls the CLI under the hood):
# Ensure the Claude Code CLI is installed
npm install -g @anthropic-ai/claude-code
# Verify the installation
claude --version
Set up the API key:
# Option 1: environment variable (recommended)
export ANTHROPIC_API_KEY="your-api-key-here"
# Option 2: set in code (not recommended for production)
process.env.ANTHROPIC_API_KEY = "your-api-key-here";
47.3 The ClaudeCode Class: Core API
The heart of the SDK is the ClaudeCode class, which provides the primary interface for interacting with Claude Code:
import { ClaudeCode } from '@anthropic-ai/claude-code';
const claude = new ClaudeCode({
// Optional configuration
apiKey: process.env.ANTHROPIC_API_KEY, // defaults to environment variable
model: 'claude-opus-4-5', // model to use
maxTurns: 10, // maximum conversation turns
cwd: '/path/to/project', // working directory
});
Basic Usage: The query Method
The most basic usage is the query method, which sends a prompt and returns the complete response:
import { ClaudeCode } from '@anthropic-ai/claude-code';
async function analyzeCode() {
const claude = new ClaudeCode({
cwd: '/path/to/my-project',
});
const result = await claude.query(
'Please analyze src/utils/date.ts for potential bugs and improvement opportunities.'
);
console.log(result.response);
console.log(`Used ${result.usage.inputTokens} input tokens`);
console.log(`Used ${result.usage.outputTokens} output tokens`);
}
analyzeCode().catch(console.error);
Streaming Responses: The stream Method
For long-running tasks, use the stream method for real-time streaming output:
import { ClaudeCode } from '@anthropic-ai/claude-code';
async function streamedQuery() {
const claude = new ClaudeCode({
cwd: '/path/to/project',
});
const stream = await claude.stream(
'Refactor all files in src/api/ to use a consistent error-handling approach.'
);
for await (const event of stream) {
switch (event.type) {
case 'text':
// Claude's text output
process.stdout.write(event.content);
break;
case 'tool_use':
// Claude is using a tool (read/write file, etc.)
console.log(`\n[Tool call] ${event.tool}: ${event.input}`);
break;
case 'tool_result':
// Tool execution result
console.log(`[Tool result] ${event.result.substring(0, 100)}...`);
break;
case 'done':
console.log('\n\nTask complete');
console.log(`Total tokens used: ${event.usage.totalTokens}`);
break;
}
}
}
47.4 Complete Example: Bulk Code Migration Tool
Here is a complete real-world application: bulk-migrating JavaScript files to TypeScript.
// scripts/migrate-to-typescript.ts
import { ClaudeCode } from '@anthropic-ai/claude-code';
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';
interface MigrationResult {
file: string;
success: boolean;
tokensUsed: number;
error?: string;
}
async function migrateFile(
claude: ClaudeCode,
filePath: string
): Promise<MigrationResult> {
console.log(`\nMigrating: ${filePath}`);
try {
const result = await claude.query(`
Migrate the following JavaScript file to TypeScript:
File path: ${filePath}
Requirements:
1. Read the file contents
2. Add TypeScript type annotations (inferred + explicit)
3. Rename the file to .ts (keep .js as a backup)
4. Ensure there are no TypeScript compilation errors
5. Do not modify any logic
`);
return {
file: filePath,
success: true,
tokensUsed: result.usage.totalTokens,
};
} catch (error) {
return {
file: filePath,
success: false,
tokensUsed: 0,
error: error instanceof Error ? error.message : String(error),
};
}
}
async function batchMigrate(projectDir: string, targetFiles: string[]) {
console.log(`\nStarting batch migration of ${targetFiles.length} files...`);
const claude = new ClaudeCode({
cwd: projectDir,
maxTurns: 15, // migrating a single file may take multiple turns
});
const results: MigrationResult[] = [];
let totalTokens = 0;
for (let i = 0; i < targetFiles.length; i++) {
const file = targetFiles[i];
console.log(`\nProgress: ${i + 1}/${targetFiles.length}`);
const result = await migrateFile(claude, file);
results.push(result);
totalTokens += result.tokensUsed;
if (result.success) {
console.log(`✓ Successfully migrated: ${file}`);
} else {
console.log(`✗ Migration failed: ${file}`);
console.log(` Reason: ${result.error}`);
}
// Avoid rate limits: wait 1 second between files
if (i < targetFiles.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
console.log('\n========== Migration Report ==========');
console.log(`Successful: ${successful}/${targetFiles.length}`);
console.log(`Failed: ${failed}/${targetFiles.length}`);
console.log(`Total tokens used: ${totalTokens.toLocaleString()}`);
console.log(`Estimated cost: $${(totalTokens * 0.000003).toFixed(4)}`);
if (failed > 0) {
console.log('\nFailed files:');
results
.filter(r => !r.success)
.forEach(r => console.log(` - ${r.file}: ${r.error}`));
}
const report = {
timestamp: new Date().toISOString(),
projectDir,
results,
summary: { successful, failed, totalTokens },
};
fs.writeFileSync('migration-report.json', JSON.stringify(report, null, 2));
console.log('\nDetailed report written to migration-report.json');
}
async function main() {
const projectDir = process.argv[2] || process.cwd();
const jsFiles = fs
.readdirSync(path.join(projectDir, 'src'), { recursive: true })
.filter((f): f is string => typeof f === 'string')
.filter(f => f.endsWith('.js') && !f.includes('.test.'))
.map(f => `src/${f}`);
if (jsFiles.length === 0) {
console.log('No .js files found to migrate.');
return;
}
console.log(`Found ${jsFiles.length} files:`);
jsFiles.forEach(f => console.log(` - ${f}`));
const estimatedTokens = jsFiles.length * 2000;
const estimatedCost = (estimatedTokens * 0.000003).toFixed(4);
console.log(`\nEstimated token usage: ~${estimatedTokens.toLocaleString()}`);
console.log(`Estimated cost: ~$${estimatedCost}`);
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise<string>(resolve =>
rl.question('\nProceed? (y/n) ', resolve)
);
rl.close();
if (answer.toLowerCase() !== 'y') {
console.log('Cancelled.');
return;
}
await batchMigrate(projectDir, jsFiles);
}
main().catch(console.error);
47.5 Advanced Features: Session Management and Context Reuse
The SDK supports multi-turn conversations with persistent context:
import { ClaudeCode } from '@anthropic-ai/claude-code';
async function multiTurnSession() {
const claude = new ClaudeCode({ cwd: '/path/to/project' });
// Turn 1: Analyze the codebase
console.log('Turn 1: Analyzing codebase...');
const analysis = await claude.query(
'Analyze the overall architecture of the src/ directory and identify the main modules and their dependencies.'
);
console.log('Architecture analysis complete.');
// Turn 2: Propose improvements (inherits context from turn 1)
console.log('\nTurn 2: Proposing improvements...');
const improvements = await claude.query(
'Based on your analysis, suggest the 3 most important architectural improvements and estimate the effort for each.'
);
console.log(improvements.response);
// Turn 3: Implement the first improvement
console.log('\nTurn 3: Implementing the improvement...');
const implementation = await claude.query(
'Begin implementing the first improvement. Write the tests first, then the implementation.'
);
}
Managing Session IDs
To resume a session across processes, save and restore the session ID:
import { ClaudeCode } from '@anthropic-ai/claude-code';
import * as fs from 'fs';
const SESSION_FILE = '.claude-session-id';
async function continueSession() {
const claude = new ClaudeCode({ cwd: process.cwd() });
let sessionId: string | undefined;
if (fs.existsSync(SESSION_FILE)) {
sessionId = fs.readFileSync(SESSION_FILE, 'utf8').trim();
console.log(`Resuming session: ${sessionId}`);
}
const result = await claude.query(
'Continue the refactoring task we were working on.',
{ sessionId } // pass session ID to restore context
);
if (result.sessionId) {
fs.writeFileSync(SESSION_FILE, result.sessionId);
}
console.log(result.response);
}
47.6 Tool Permission Control
In production applications, you may need to restrict which tools Claude can use:
import { ClaudeCode } from '@anthropic-ai/claude-code';
// Read-only mode: allow file reading but no modifications
const readOnlyClaude = new ClaudeCode({
cwd: '/path/to/project',
allowedTools: ['Read', 'Grep', 'Glob'],
});
// Analysis mode: allow reads and read-only shell commands
const analysisClaude = new ClaudeCode({
cwd: '/path/to/project',
allowedTools: ['Read', 'Grep', 'Glob', 'Bash'],
bashAllowedCommands: ['npm test', 'tsc --noEmit', 'eslint'],
});
// Full permissions (default — no allowedTools means all tools are available)
const fullClaude = new ClaudeCode({
cwd: '/path/to/project',
});
47.7 Error Handling and Retry Strategy
import { ClaudeCode, ClaudeCodeError, RateLimitError } from '@anthropic-ai/claude-code';
async function robustQuery(
claude: ClaudeCode,
prompt: string,
maxRetries = 3
): Promise<string> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await claude.query(prompt);
return result.response;
} catch (error) {
if (error instanceof RateLimitError) {
// Rate limited: wait and retry with exponential backoff
const waitTime = Math.pow(2, attempt) * 1000;
console.log(`Rate limited. Waiting ${waitTime / 1000}s before retry (${attempt}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
if (error instanceof ClaudeCodeError) {
console.error(`Claude Code error: ${error.message}`);
if (error.code === 'CONTEXT_TOO_LONG') {
console.log('Context too long; consider compacting or reducing input.');
}
throw error; // non-rate-limit errors: rethrow immediately
}
throw error; // unknown errors: rethrow
}
}
throw new Error(`Failed after ${maxRetries} retries`);
}
47.8 Building a Web Service: Claude Code API Server
You can wrap the Claude Code SDK into an HTTP API, providing AI programming capabilities to web frontends:
// server.ts
import express from 'express';
import { ClaudeCode } from '@anthropic-ai/claude-code';
import * as path from 'path';
const app = express();
app.use(express.json());
// POST /analyze — analyze code
app.post('/analyze', async (req, res) => {
const { projectPath, prompt } = req.body;
if (!projectPath || !prompt) {
return res.status(400).json({ error: 'projectPath and prompt are required' });
}
// Security check: ensure path is within the allowed base
const allowedBasePath = '/workspace/projects';
const resolvedPath = path.resolve(projectPath);
if (!resolvedPath.startsWith(allowedBasePath)) {
return res.status(403).json({ error: 'Access denied' });
}
try {
const claude = new ClaudeCode({
cwd: resolvedPath,
allowedTools: ['Read', 'Grep', 'Glob'], // read-only mode
});
const result = await claude.query(prompt);
res.json({
response: result.response,
usage: result.usage,
});
} catch (error) {
console.error('Analysis error:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Unknown error',
});
}
});
// POST /stream — streaming analysis
app.post('/stream', async (req, res) => {
const { projectPath, prompt } = req.body;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
const claude = new ClaudeCode({ cwd: projectPath });
const stream = await claude.stream(prompt);
for await (const event of stream) {
res.write(`data: ${JSON.stringify(event)}\n\n`);
}
res.write('data: [DONE]\n\n');
res.end();
} catch (error) {
res.write(`data: ${JSON.stringify({ type: 'error', message: String(error) })}\n\n`);
res.end();
}
});
app.listen(3000, () => {
console.log('Claude Code API Server running on port 3000');
});
47.9 Choosing Between the SDK and Direct API Calls
| Scenario | Recommended Approach |
|---|---|
| Need filesystem operations (read/write code) | ClaudeCode SDK |
| Only need text generation (chat, summarization) | Anthropic Messages API |
| Need to run shell commands | ClaudeCode SDK |
| Need fine-grained control over tool calls | Implement Tool Use API manually |
| Need multi-agent coordination | ClaudeCode SDK (uses the Agent tool) |
| Simple one-off text Q&A | Anthropic Messages API (lighter weight) |
Summary
The Claude Code SDK extends AI programming agent capabilities from a command-line tool to a programmable interface.
Key takeaways:
- The
@anthropic-ai/claude-codenpm package provides theClaudeCodeclass for programmatic access in Node.js/TypeScript applications - The
querymethod suits single-round queries;streamsuits long-running tasks with real-time output - The SDK has built-in tool-calling (read file, write file, run commands) and agent orchestration; no need to build these from scratch
- Use
allowedToolsto restrict Claude's available tools for permission control - Session IDs allow conversation context to persist across requests
- Ideal for building batch code-processing pipelines, web-based AI coding assistants, and internal automation tools