Back to all articles
13 MIN READ

Headless & Programmatic Claude Code: SDK & Automation

By Dorian Laurenceau

📅 Last reviewed: April 24, 2026. Updated with April 2026 findings and community feedback.

Headless mode transforms Claude Code from an interactive tool into a programmable automation engine. Build scripts, pipelines, and applications that leverage Claude's capabilities without human intervention.


<!-- manual-insight -->

Headless mode in production: the patterns that don't end in incident reports

Headless Claude Code is where agentic AI starts to feel like infrastructure rather than a dev tool. Which means it's also where the mistakes stop being embarrassing and start being expensive. The architecture patterns that the r/ClaudeAI and r/LangChain communities keep validating:

  • Time and token caps are non-negotiable. Every headless invocation needs --max-turns, a wall-clock timeout, and ideally a token ceiling. The thread everyone eventually reads is the one where a stuck agent spent $400 overnight on a loop. The Claude Code SDK documentation shows the flags; set them on day one.
  • Streaming over batch, JSON lines over ad-hoc output. If you need to observe what the agent is doing, stream events and write JSONL to a log. Parsing natural-language stdout in a pipeline is a support ticket generator. Every production headless setup I've seen converged on structured output eventually.
  • Idempotency belongs in the agent's instructions, not hoped for. Prompts like "if this commit has already been pushed, exit successfully" at the top of your headless prompt prevent the classic "workflow re-ran, created duplicate PR" incident. Claude will respect it; without it, re-runs are a coin flip.

The one-liner test for whether your headless workflow is production-ready: kill the process mid-run and re-run it. If it corrupts state, does duplicate work, or silently fails, it's not ready. Claude Code is agentic; your pipeline needs to be defensive.


Learn AI — From Prompts to Agents

10 Free Interactive Guides120+ Hands-On Exercises100% Free

What is Headless Mode?

Headless mode runs Claude Code without interactive prompts:

Interactive Mode (Default)

$ claude
> How can I help you today?
_

Headless Mode

$ claude --print -p "Analyze this codebase" > report.md
$ echo $?  # Exit code: 0

No interaction needed. Input goes in, output comes out.


Headless CLI Flags

FlagDescription
--print / -pOutput to stdout instead of interactive UI
--prompt / -pSpecify the prompt (combined with --print)
--output-formatOutput format: text, json, stream-json
--dangerously-skip-permissionsSkip all permission prompts
--max-tokensLimit output tokens
--modelSpecify model: sonnet, opus, haiku
--no-colorDisable colored output
--quiet / -qSuppress non-essential output

Basic Headless Usage

Simple Prompt

claude --print -p "Explain this function" < src/utils.ts

Output to File

claude --print -p "Generate API documentation" > docs/api.md

JSON Output

claude --print --output-format json -p "List all TODO comments as JSON array"

Piping Input

cat error.log | claude --print -p "Analyze this error log and suggest fixes"

Multiple Files

claude --print -p "Compare these implementations" < <(cat file1.ts file2.ts)

Automation Patterns

Script Integration

#!/bin/bash
# analyze.sh

# Get changed files
changed=$(git diff --name-only HEAD~1)

# Analyze each file
for file in $changed; do
  echo "Analyzing $file..."
  claude --print --quiet -p "Review $file for issues" > "reviews/${file}.md"
done

echo "Analysis complete"

Error Handling

#!/bin/bash

output=$(claude --print -p "Generate migration" 2>&1)
exit_code=$?

if [ $exit_code -ne 0 ]; then
  echo "Error: $output"
  exit 1
fi

echo "$output" > migration.sql

Conditional Logic

#!/bin/bash

# Ask Claude to categorize
category=$(claude --print --output-format json \
  -p "Categorize this issue. Return JSON: {\"category\": \"bug|feature|docs\"}" \
  < issue.txt | jq -r '.category')

case $category in
  bug)
    echo "Routing to bug triage..."
    ;;
  feature)
    echo "Adding to feature backlog..."
    ;;
  docs)
    echo "Assigning to documentation team..."
    ;;
esac

The Anthropic SDK

For deeper integration, use the official SDK:

Installation

npm install @anthropic-ai/sdk

Basic Usage

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

async function main() {
  const message = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 1024,
    messages: [
      {
        role: "user",
        content: "Explain the concept of dependency injection"
      }
    ]
  });
  
  console.log(message.content[0].text);
}

main();

With System Prompt

const message = await client.messages.create({
  model: "claude-sonnet-4-20250514",
  max_tokens: 1024,
  system: "You are a senior software engineer. Provide detailed, practical advice.",
  messages: [
    { role: "user", content: "How should I structure a microservices project?" }
  ]
});

Streaming Responses

const stream = await client.messages.stream({
  model: "claude-sonnet-4-20250514",
  max_tokens: 1024,
  messages: [
    { role: "user", content: "Write a comprehensive testing guide" }
  ]
});

for await (const chunk of stream) {
  if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
    process.stdout.write(chunk.delta.text);
  }
}

Multi-Turn Conversations

const conversation = [
  { role: "user", content: "I'm building a REST API in Node.js" },
  { role: "assistant", content: "Great! What framework are you using?" },
  { role: "user", content: "Express. How should I structure my routes?" }
];

const response = await client.messages.create({
  model: "claude-sonnet-4-20250514",
  max_tokens: 1024,
  messages: conversation
});

Tool Use with SDK

Claude can use tools programmatically:

Define Tools

const tools = [
  {
    name: "get_weather",
    description: "Get current weather for a location",
    input_schema: {
      type: "object",
      properties: {
        location: {
          type: "string",
          description: "City name"
        }
      },
      required: ["location"]
    }
  },
  {
    name: "search_code",
    description: "Search codebase for patterns",
    input_schema: {
      type: "object",
      properties: {
        query: { type: "string" },
        file_pattern: { type: "string" }
      },
      required: ["query"]
    }
  }
];

Execute Tools

async function runWithTools(prompt: string) {
  let messages = [{ role: "user", content: prompt }];
  
  while (true) {
    const response = await client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 1024,
      tools,
      messages
    });
    
    // Check if Claude wants to use a tool
    const toolUse = response.content.find(block => block.type === "tool_use");
    
    if (!toolUse) {
      // No more tools, return final response
      return response.content.find(block => block.type === "text")?.text;
    }
    
    // Execute the tool
    const result = await executeToolCall(toolUse.name, toolUse.input);
    
    // Add tool result to conversation
    messages.push({
      role: "assistant",
      content: response.content
    });
    messages.push({
      role: "user",
      content: [{
        type: "tool_result",
        tool_use_id: toolUse.id,
        content: result
      }]
    });
  }
}

async function executeToolCall(name: string, input: any): Promise<string> {
  switch (name) {
    case "get_weather":
      return JSON.stringify({ temp: 72, condition: "sunny" });
    case "search_code":
      return `Found 5 matches for "${input.query}"`;
    default:
      return "Tool not found";
  }
}

Batch Processing

Process multiple items efficiently:

Sequential Processing

import { readdir, readFile, writeFile } from "fs/promises";

async function processFiles(directory: string) {
  const files = await readdir(directory);
  
  for (const file of files) {
    if (!file.endsWith(".ts")) continue;
    
    const content = await readFile(`${directory}/${file}`, "utf-8");
    
    const response = await client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 2048,
      messages: [{
        role: "user",
        content: `Generate JSDoc comments for this file:\n\n${content}`
      }]
    });
    
    const documented = response.content[0].text;
    await writeFile(`${directory}/${file}`, documented);
    
    console.log(`Processed: ${file}`);
  }
}

Parallel Processing (with rate limiting)

import pLimit from "p-limit";

const limit = pLimit(5); // Max 5 concurrent requests

async function processFilesParallel(files: string[]) {
  const tasks = files.map(file =>
    limit(async () => {
      const content = await readFile(file, "utf-8");
      
      const response = await client.messages.create({
        model: "claude-sonnet-4-20250514",
        max_tokens: 1024,
        messages: [{
          role: "user",
          content: `Analyze: ${content}`
        }]
      });
      
      return { file, analysis: response.content[0].text };
    })
  );
  
  return Promise.all(tasks);
}

Batch API (for large workloads)

// For very large batches, use the Batch API
const batch = await client.batches.create({
  requests: files.map((file, i) => ({
    custom_id: `file-${i}`,
    params: {
      model: "claude-sonnet-4-20250514",
      max_tokens: 1024,
      messages: [{ role: "user", content: `Analyze: ${file}` }]
    }
  }))
});

// Poll for completion
while (batch.status !== "completed") {
  await new Promise(r => setTimeout(r, 60000));
  batch = await client.batches.retrieve(batch.id);
}

// Get results
const results = await client.batches.results(batch.id);

Building Pipelines

Code Analysis Pipeline

interface AnalysisResult {
  file: string;
  issues: Issue[];
  complexity: number;
  suggestions: string[];
}

async function analyzeCodebase(directory: string): Promise<AnalysisResult[]> {
  const files = await glob(`${directory}/**/*.ts`);
  const results: AnalysisResult[] = [];
  
  for (const file of files) {
    const content = await readFile(file, "utf-8");
    
    const response = await client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 2048,
      messages: [{
        role: "user",
        content: `Analyze this TypeScript file and return JSON:
          {
            "issues": [{"type": "string", "line": number, "message": "string"}],
            "complexity": number (1-10),
            "suggestions": ["string"]
          }
          
          File: ${file}
          \`\`\`typescript
          ${content}
          \`\`\``
      }]
    });
    
    const analysis = JSON.parse(response.content[0].text);
    results.push({ file, ...analysis });
  }
  
  return results;
}

// Use the pipeline
const analysis = await analyzeCodebase("./src");
const highComplexity = analysis.filter(r => r.complexity > 7);
console.log(`Files needing refactoring: ${highComplexity.length}`);

Documentation Pipeline

async function generateDocs(sourceDir: string, outputDir: string) {
  // Step 1: Analyze structure
  const structure = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 2048,
    messages: [{
      role: "user",
      content: `Analyze ${sourceDir} and create a documentation outline`
    }]
  });
  
  // Step 2: Generate README
  const readme = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 4096,
    messages: [{
      role: "user",
      content: `Generate README.md based on: ${structure.content[0].text}`
    }]
  });
  
  await writeFile(`${outputDir}/README.md`, readme.content[0].text);
  
  // Step 3: Generate API reference
  const apiFiles = await glob(`${sourceDir}/api/**/*.ts`);
  
  for (const file of apiFiles) {
    const content = await readFile(file, "utf-8");
    
    const doc = await client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 2048,
      messages: [{
        role: "user",
        content: `Generate API documentation for:\n${content}`
      }]
    });
    
    const docPath = file.replace(sourceDir, outputDir).replace(".ts", ".md");
    await writeFile(docPath, doc.content[0].text);
  }
}

Error Handling & Retries

Robust API Calls

import { setTimeout } from "timers/promises";

async function callWithRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> {
  let lastError: Error;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      
      // Don't retry on validation errors
      if (error.status === 400) throw error;
      
      // Exponential backoff
      const delay = baseDelay * Math.pow(2, attempt);
      console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`);
      await setTimeout(delay);
    }
  }
  
  throw lastError;
}

// Usage
const response = await callWithRetry(() =>
  client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 1024,
    messages: [{ role: "user", content: prompt }]
  })
);

Rate Limit Handling

import Bottleneck from "bottleneck";

const limiter = new Bottleneck({
  reservoir: 100,      // 100 requests
  reservoirRefreshAmount: 100,
  reservoirRefreshInterval: 60 * 1000, // per minute
  maxConcurrent: 5
});

async function rateLimitedCall(prompt: string) {
  return limiter.schedule(() =>
    client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 1024,
      messages: [{ role: "user", content: prompt }]
    })
  );
}

Production Patterns

Configuration Management

// config.ts
interface Config {
  model: string;
  maxTokens: number;
  temperature: number;
  systemPrompt: string;
}

const configs: Record<string, Config> = {
  analysis: {
    model: "claude-sonnet-4-20250514",
    maxTokens: 2048,
    temperature: 0,
    systemPrompt: "You are a code analysis expert. Be thorough and precise."
  },
  creative: {
    model: "claude-sonnet-4-20250514",
    maxTokens: 4096,
    temperature: 0.7,
    systemPrompt: "You are a creative technical writer."
  },
  fast: {
    model: "claude-3-5-haiku-20241022",
    maxTokens: 512,
    temperature: 0,
    systemPrompt: "Be concise."
  }
};

async function query(prompt: string, configName: keyof typeof configs) {
  const config = configs[configName];
  
  return client.messages.create({
    model: config.model,
    max_tokens: config.maxTokens,
    temperature: config.temperature,
    system: config.systemPrompt,
    messages: [{ role: "user", content: prompt }]
  });
}

Logging & Monitoring

import { createLogger, transports, format } from "winston";

const logger = createLogger({
  level: "info",
  format: format.combine(
    format.timestamp(),
    format.json()
  ),
  transports: [
    new transports.File({ filename: "claude.log" })
  ]
});

async function trackedQuery(prompt: string, metadata: Record<string, any>) {
  const startTime = Date.now();
  
  try {
    const response = await client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 1024,
      messages: [{ role: "user", content: prompt }]
    });
    
    logger.info("API call successful", {
      ...metadata,
      duration: Date.now() - startTime,
      inputTokens: response.usage.input_tokens,
      outputTokens: response.usage.output_tokens
    });
    
    return response;
  } catch (error) {
    logger.error("API call failed", {
      ...metadata,
      duration: Date.now() - startTime,
      error: error.message
    });
    throw error;
  }
}

Caching

import { createHash } from "crypto";
import { Redis } from "ioredis";

const redis = new Redis();

function hashPrompt(prompt: string, model: string): string {
  return createHash("sha256")
    .update(`${model}:${prompt}`)
    .digest("hex");
}

async function cachedQuery(prompt: string, ttl = 3600) {
  const cacheKey = hashPrompt(prompt, "claude-sonnet-4-20250514");
  
  // Check cache
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // Make API call
  const response = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 1024,
    messages: [{ role: "user", content: prompt }]
  });
  
  // Cache result
  await redis.setex(cacheKey, ttl, JSON.stringify(response));
  
  return response;
}

Use Cases

Automated Code Review Service

// review-service.ts
import express from "express";
import { Anthropic } from "@anthropic-ai/sdk";

const app = express();
const client = new Anthropic();

app.post("/review", async (req, res) => {
  const { code, language, rules } = req.body;
  
  const response = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 2048,
    system: `You are a code reviewer. Review ${language} code following these rules: ${rules}`,
    messages: [{
      role: "user",
      content: `Review this code:\n\`\`\`${language}\n${code}\n\`\`\``
    }]
  });
  
  res.json({
    review: response.content[0].text,
    tokens: response.usage
  });
});

app.listen(3000);

Documentation Generator

// doc-generator.ts
async function generateModuleDocs(modulePath: string) {
  const files = await glob(`${modulePath}/**/*.ts`);
  const docs: string[] = [];
  
  for (const file of files) {
    const content = await readFile(file, "utf-8");
    
    const response = await client.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 2048,
      messages: [{
        role: "user",
        content: `Generate markdown documentation for:\n${content}`
      }]
    });
    
    docs.push(`## ${file}\n\n${response.content[0].text}`);
  }
  
  return docs.join("\n\n---\n\n");
}

Test Generator

// test-generator.ts
async function generateTests(sourceFile: string, testFramework = "jest") {
  const source = await readFile(sourceFile, "utf-8");
  
  const response = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 4096,
    system: `Generate comprehensive ${testFramework} tests. Include edge cases and error scenarios.`,
    messages: [{
      role: "user",
      content: `Generate tests for:\n\`\`\`typescript\n${source}\n\`\`\``
    }]
  });
  
  const testFile = sourceFile.replace(".ts", ".test.ts");
  await writeFile(testFile, response.content[0].text);
  
  return testFile;
}

Integration with Claude Code Features

Using Skills Programmatically

// Load skill definitions
const skills = await loadSkills(".claude/skills");

async function runSkill(skillName: string, input: any) {
  const skill = skills[skillName];
  
  return client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 4096,
    system: skill.systemPrompt,
    messages: [{
      role: "user",
      content: `Execute skill "${skillName}" with:\n${JSON.stringify(input)}`
    }]
  });
}

See Agent Skills in Claude Code: Extend Claude's Capabilities.

Programmatic MCP

import { MCPClient } from "@modelcontextprotocol/sdk";

const mcp = new MCPClient();
await mcp.connect("github", "https://api.github.com/mcp/");

// Use MCP tools in SDK calls
const tools = await mcp.listTools();

See Model Context Protocol (MCP) for Claude Code: Complete Guide.


Key Takeaways

  1. --print for scripting: Essential flag for headless automation.

  2. SDK for complex workflows: Full control with the Anthropic SDK.

  3. Handle errors gracefully: Implement retries and rate limiting.

  4. Cache when possible: Reduce costs and latency.

  5. Monitor in production: Log usage, errors, and performance.


Build Production AI Systems

Headless Claude Code is the foundation for production AI systems. Learn to build reliable, scalable AI workflows.

In our Module 6, Autonomous Agents, you'll learn:

  • Production AI architecture
  • Reliability patterns
  • Scaling strategies
  • Monitoring and observability

Explore Module 6: Autonomous Agents

GO DEEPER — FREE GUIDE

Module 6 — AI Agents & ReAct

Create autonomous agents that reason and take actions.

D

Dorian Laurenceau

Full-Stack Developer & Learning Designer

Full-stack web developer and learning designer. I spent 4 years as a freelance full-stack developer and 4 years teaching React, JavaScript, HTML/CSS and WordPress to adult learners. Today I design learning paths in web development and AI, grounded in learning science. I founded learn-prompting.fr to make AI practical and accessible, and built the Bluff app to gamify political transparency.

Prompt EngineeringLLMsFull-Stack DevelopmentLearning DesignReact
Published: January 30, 2026Updated: April 24, 2026
Newsletter

Weekly AI Insights

Tools, techniques & news — curated for AI practitioners. Free, no spam.

Free, no spam. Unsubscribe anytime.

FAQ

What is headless mode in Claude Code?+

Headless mode runs Claude Code without interactive terminal UI, enabling scripting, CI/CD integration, and automated pipelines. Use 'claude --print' or SDK for programmatic access.

How do I use Claude Code in scripts?+

Use 'claude -p "your prompt" --print' for simple scripting. For complex automation, use the TypeScript/Python SDK with streaming responses and tool execution control.

Can Claude Code be used in CI/CD pipelines?+

Yes. Claude Code integrates with GitHub Actions, GitLab CI, Jenkins via headless mode. Common uses: automated code review, PR descriptions, test generation, and documentation.

What's the difference between Claude Code SDK and API?+

SDK wraps Claude Code's agent capabilities (file access, tools, memory). Direct API is raw model access. Use SDK for coding tasks; API for custom integrations.