Skip to content

Instantly share code, notes, and snippets.

@JacobFV
Last active January 25, 2026 04:05
Show Gist options
  • Select an option

  • Save JacobFV/2c4a75bc6a835d2c1f6c863cfcbdfa5a to your computer and use it in GitHub Desktop.

Select an option

Save JacobFV/2c4a75bc6a835d2c1f6c863cfcbdfa5a to your computer and use it in GitHub Desktop.
Using Claude Code Programmatically: Complete guide to bypassing the TUI with --print mode for clean JSON output. Build document editors, agents, and automation systems.

Using Claude Code Programmatically: A Complete Guide

TL;DR: Claude Code has a built-in --print --output-format=json mode that bypasses the TUI completely, giving you clean programmatic access to Claude as an async agent runtime.

The Discovery

While building a document editing agent system, I discovered that Claude Code's binary includes a first-class JSON output mode that completely bypasses the terminal UI rendering, making it perfect for programmatic use.

How It Works

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Claude Binary (closed source)    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                  β”‚
β”‚  Interactive mode:               β”‚
β”‚    User Input β†’ TUI Renderer β†’  β”‚
β”‚    Character Buffer β†’ Terminal   β”‚
β”‚                                  β”‚
β”‚  Print mode (--print):           β”‚
β”‚    User Input β†’ Direct JSON β†’   β”‚
β”‚    Clean stdout (NO TUI)         β”‚
β”‚                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The Command

claude --print --output-format=json "your prompt here"

Output:

{
  "type": "result",
  "subtype": "success",
  "result": "The actual response text with proper \\n escaping",
  "session_id": "uuid-here",
  "total_cost_usd": 0.001234,
  "usage": {
    "input_tokens": 10,
    "output_tokens": 50,
    "cache_read_input_tokens": 1000
  },
  "modelUsage": { ... },
  "duration_ms": 2500
}

Why This Matters

βœ… What Works

  • Multi-line content - Properly escaped with \n
  • Special characters - Quotes, backslashes, all handled correctly
  • Unicode - Full emoji and international character support
  • Large documents - Tested with 2000+ character responses
  • Code generation - Preserves formatting perfectly
  • Your Max subscription - Uses your existing authentication
  • No TUI artifacts - No ANSI codes, no control characters

❌ What Doesn't Work

  • Session continuation in sync mode - Session ID conflicts when using execSync
  • Streaming in simple mode - Need async wrapper for streaming
  • Direct token access - OAuth tokens are hidden (but you don't need them!)

Implementation

Simple Wrapper (Synchronous)

#!/usr/bin/env node

import { execSync } from 'child_process';

export class ClaudeAgent {
  constructor(options = {}) {
    this.options = {
      model: options.model || 'sonnet',
      dangerouslySkipPermissions: options.dangerouslySkipPermissions ?? true,
      timeout: options.timeout || 30000,
      ...options
    };
  }

  prompt(message, options = {}) {
    const opts = { ...this.options, ...options };
    const args = this._buildArgs(opts);
    const command = `claude ${args.join(' ')} ${JSON.stringify(message)}`;

    try {
      const result = execSync(command, {
        encoding: 'utf-8',
        timeout: opts.timeout,
        maxBuffer: 10 * 1024 * 1024
      });

      const parsed = JSON.parse(result);

      if (parsed.is_error) {
        throw new Error(parsed.result);
      }

      return {
        text: parsed.result,
        sessionId: parsed.session_id,
        usage: parsed.usage,
        cost: parsed.total_cost_usd,
        duration: parsed.duration_ms,
        modelUsage: parsed.modelUsage
      };
    } catch (err) {
      throw new Error(`Claude request failed: ${err.message}`);
    }
  }

  _buildArgs(options) {
    const args = ['--print', '--output-format=json'];
    if (options.model) args.push('--model', options.model);
    if (options.dangerouslySkipPermissions) args.push('--dangerously-skip-permissions');
    if (options.systemPrompt) args.push('--system-prompt', JSON.stringify(options.systemPrompt));
    return args;
  }
}

export default ClaudeAgent;

Usage Example

import ClaudeAgent from './claude-agent.js';

const agent = new ClaudeAgent();

// Simple prompt
const response = agent.prompt('What is 2+2?');
console.log(response.text);      // "2 + 2 = 4"
console.log(response.cost);      // 0.001234 USD
console.log(response.sessionId); // "uuid-here"

// Document generation
const doc = agent.prompt('Write a 500-word essay about AI safety');
console.log(doc.text); // Full essay, clean multi-line text

// Code generation
const code = agent.prompt('Write a Python function to reverse a string');
console.log(code.text); // Properly formatted code

// Use different models
const fastAgent = new ClaudeAgent({ model: 'haiku' });
const quick = fastAgent.prompt('Quick answer: capital of France?');
console.log(quick.text); // "Paris."

Document Editing System Example

class DocumentSystem {
  constructor() {
    this.agent = new ClaudeAgent();
    this.documents = new Map();
  }

  async createDocument(title, requirements) {
    const response = this.agent.prompt(
      `Create a document titled "${title}" with these requirements:\n${requirements}\n\nProvide only the document content.`
    );

    const doc = {
      id: this.generateId(),
      title,
      content: response.text,
      createdAt: new Date(),
      cost: response.cost
    };

    this.documents.set(doc.id, doc);
    return doc;
  }

  async editDocument(docId, instructions) {
    const doc = this.documents.get(docId);
    if (!doc) throw new Error(`Document ${docId} not found`);

    const response = this.agent.prompt(
      `Edit this document according to the instructions.\n\nCurrent content:\n${doc.content}\n\nInstructions: ${instructions}\n\nProvide only the updated content.`
    );

    doc.content = response.text;
    doc.updatedAt = new Date();
    doc.totalCost = (doc.totalCost || doc.cost) + response.cost;

    return doc;
  }

  generateId() {
    return `doc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

// Usage
const system = new DocumentSystem();

const doc = system.createDocument(
  'API Documentation',
  'Create comprehensive REST API docs with auth, endpoints, and examples'
);

console.log('Created:', doc.title);
console.log('Length:', doc.content.length, 'chars');
console.log('Cost:', doc.cost, 'USD');

system.editDocument(doc.id, 'Add a section on rate limiting and error handling');

Testing & Validation

Test 1: Multi-line Content

claude --print --output-format=json "Write a 5-line poem about coding"

Result: βœ… Properly escaped with \n, clean JSON output

Test 2: Special Characters

claude --print --output-format=json 'Write code with "quotes", '\''apostrophes'\'', and backslashes \\'

Result: βœ… All special characters properly escaped in JSON

Test 3: Large Documents

claude --print --output-format=json "Write a 2000-word essay about neural networks"

Result: βœ… 2309 characters, 95 lines, no artifacts

Test 4: Unicode Support

claude --print --output-format=json "Write JSON with emojis πŸš€ and unicode δ½ ε₯½"

Result: βœ… Full Unicode support, no encoding issues

Authentication & Cost

How Authentication Works

  1. You're already logged in via claude (OAuth handled by binary)
  2. The --print mode uses the same authentication
  3. Your Claude Max subscription applies automatically
  4. No token extraction needed - within Anthropic's TOS

Cost Tracking

Every response includes detailed cost information:

{
  text: "Response text here",
  cost: 0.001234,              // Total cost in USD
  usage: {
    input_tokens: 10,
    output_tokens: 50,
    cache_read_input_tokens: 1000
  },
  modelUsage: {
    "claude-sonnet-4-5-20250929": {
      inputTokens: 10,
      outputTokens: 50,
      cacheReadInputTokens: 1000,
      costUSD: 0.001234
    }
  }
}

Available CLI Options

claude --print --output-format=json \
  --model sonnet|opus|haiku \
  --agent agent-name \
  --dangerously-skip-permissions \
  --system-prompt "Custom system prompt" \
  --allowed-tools "Bash,Read,Write" \
  --max-budget-usd 10.00 \
  "Your prompt here"

Use Cases

1. Document Generation & Editing

Build AI-powered document editors, content management systems, or writing assistants.

2. Code Generation Pipelines

Automate code generation, refactoring, or documentation creation.

3. Data Extraction & Analysis

Extract structured data from unstructured text, analyze documents, or generate reports.

4. Chatbots & Agents

Build custom chatbots or agent systems using Claude as the reasoning engine.

5. Batch Processing

Process multiple requests in parallel for bulk operations.

// Parallel processing example
const tasks = [
  'Summarize quantum computing',
  'Explain neural networks',
  'Describe blockchain'
];

const agents = tasks.map(() => new ClaudeAgent({ model: 'haiku' }));

const results = await Promise.all(
  tasks.map((task, i) => agents[i].prompt(task))
);

const totalCost = results.reduce((sum, r) => sum + r.cost, 0);
console.log('Total cost:', totalCost.toFixed(6), 'USD');

Streaming Support (Advanced)

For real-time streaming, use --output-format=stream-json --verbose:

import { spawn } from 'child_process';

async function* streamResponse(prompt) {
  const proc = spawn('claude', [
    '--print',
    '--output-format=stream-json',
    '--verbose',
    '--dangerously-skip-permissions',
    prompt
  ]);

  let buffer = '';
  for await (const chunk of proc.stdout) {
    buffer += chunk.toString();
    const lines = buffer.split('\n');
    buffer = lines.pop() || '';

    for (const line of lines) {
      if (!line.trim()) continue;

      try {
        const event = JSON.parse(line);
        if (event.type === 'assistant') {
          const text = event.message.content
            .filter(c => c.type === 'text')
            .map(c => c.text)
            .join('');
          yield text;
        }
      } catch (e) {
        // Skip malformed JSON
      }
    }
  }
}

// Usage
for await (const chunk of streamResponse('Count to 10')) {
  process.stdout.write(chunk);
}

Important Notes

January 2026 Restrictions

Anthropic blocked extracting OAuth tokens for external API use, but --print mode is still fully supported because:

  • You're using the official Claude Code CLI
  • Not extracting tokens for external use
  • Within Anthropic's Terms of Service
  • Designed for programmatic access

Limitations

  1. No session continuation in simple sync mode - Each call is independent
  2. Subprocess overhead - Each call spawns a process (~1-2s startup)
  3. Requires Claude Code installed - Binary must be available in PATH
  4. Rate limits apply - Same as interactive usage

Best Practices

  1. Use dangerouslySkipPermissions carefully - Only in sandboxed environments
  2. Cache responses - Avoid redundant API calls
  3. Use cheaper models when possible - Haiku for simple tasks
  4. Track costs - Monitor spending across requests
  5. Handle errors gracefully - Wrap in try/catch blocks

Comparison: Print Mode vs Normal Mode

Feature Normal Mode Print Mode
TUI Rendering βœ… Yes ❌ No
JSON Output ❌ No βœ… Yes
Multi-line Clean ❌ Charbuffer βœ… Escaped
Programmatic ❌ Hard βœ… Easy
Streaming βœ… Visual βœ… JSON events
Cost Tracking ❌ No βœ… Detailed
Special Chars ⚠️ TUI artifacts βœ… Proper escaping

Conclusion

Claude Code's --print --output-format=json mode provides clean, programmatic access to Claude without TUI rendering issues. It's:

  • βœ… First-class feature - Built into the binary
  • βœ… Production-ready - Handles all content types
  • βœ… Cost-effective - Uses your Max subscription
  • βœ… Well-designed - Proper JSON escaping
  • βœ… Easy to use - Simple wrapper pattern

This makes Claude Code a powerful async agent runtime for building document editors, automation systems, and AI-powered applications.


Wait, I Actually Realized...

After documenting all of this, I discovered that Anthropic already built an official library for this called the Claude Agent SDK 🀦

The Official Claude Agent SDK

Turns out there's an official TypeScript/Python SDK that wraps Claude Code and provides all the bells and whistles:

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

for await (const message of query({
  prompt: "Find and fix the bug in auth.py",
  options: { allowedTools: ["Read", "Edit", "Bash"] }
})) {
  console.log(message);
}

Features the SDK has that our approach doesn't:

  • βœ… Built-in hooks system (PreToolUse, PostToolUse, etc.)
  • βœ… Subagent spawning and management
  • βœ… Full MCP (Model Context Protocol) integration
  • βœ… Session management with resume/fork
  • βœ… Interactive user approval prompts
  • βœ… Skills and slash commands support
  • βœ… Production-ready error handling
  • βœ… Proper TypeScript types

Under the hood: The SDK still wraps the Claude Code CLI binary (just like our approach), but with a much more polished interface.

So... Why Does This Guide Still Matter?

Good question! Here's when you'd want the lightweight --print approach instead of the official SDK:

Use the Lightweight Approach When:

  1. Prototyping / Quick Scripts

    // One file, no dependencies, done
    const agent = new ClaudeAgent();
    const result = agent.prompt('Quick task');
  2. Minimal Dependencies

    • The SDK is 71.9 MB unpacked
    • Our approach: just child_process (built-in)
  3. Understanding the Foundation

    • Good to know what the SDK wraps
    • Useful for debugging SDK issues
    • Educational value
  4. Custom Lightweight Wrappers

    • Build exactly what you need
    • No extra features you won't use
    • Total control over the interface
  5. Simple Document Editing

    • Don't need hooks or subagents
    • Just want clean text in/out
    • Synchronous operations are fine

Use the Official SDK When:

  1. Production Agents - Full feature set, proper error handling
  2. Complex Workflows - Hooks, subagents, MCP servers
  3. Team Projects - Official support, documentation, updates
  4. Long-running Processes - Better session management
  5. Interactive Apps - User approval prompts, clarifying questions

Comparison Table

Feature Lightweight (--print) Official SDK
Size ~50 lines of code 71.9 MB package
Dependencies None (Node built-ins) npm package
Learning curve 5 minutes 30 minutes
Hooks ❌ No βœ… Yes
Subagents ❌ No βœ… Yes
MCP ❌ No βœ… Yes
Session management ⚠️ Manual βœ… Built-in
Error handling ⚠️ Basic βœ… Production-ready
Streaming ⚠️ Manual βœ… Built-in
Types ⚠️ DIY βœ… Full TypeScript
Best for Quick scripts Production apps

The Bottom Line

Both approaches are valid:

  • This guide shows you the minimal programmatic interface - useful for understanding, prototyping, and lightweight use cases
  • The official SDK is what you should probably use for production applications

Think of it like this:

  • Our approach = curl (direct HTTP access)
  • Official SDK = axios or fetch (polished library)

Both hit the same underlying system, but the library adds convenience, safety, and features.

Next Steps

If you're building something serious, check out:

But if you just want to send a prompt and get text back without installing anything, the --print --output-format=json approach documented here works great! πŸš€

Resources

Credits

Discovered and documented by @JacobFV - January 2026


Found this useful? Star this gist and share with others building AI agents!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment