Chapter 47

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:

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:

Rate this chapter
4.8  / 5  (3 ratings)

💬 Comments