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.
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.
ββββββββββββββββββββββββββββββββββββ
β Claude Binary (closed source) β
ββββββββββββββββββββββββββββββββββββ€
β β
β Interactive mode: β
β User Input β TUI Renderer β β
β Character Buffer β Terminal β
β β
β Print mode (--print): β
β User Input β Direct JSON β β
β Clean stdout (NO TUI) β
β β
ββββββββββββββββββββββββββββββββββββ
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
}- 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
- 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!)
#!/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;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."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');claude --print --output-format=json "Write a 5-line poem about coding"Result: β
Properly escaped with \n, clean JSON output
claude --print --output-format=json 'Write code with "quotes", '\''apostrophes'\'', and backslashes \\'Result: β All special characters properly escaped in JSON
claude --print --output-format=json "Write a 2000-word essay about neural networks"Result: β 2309 characters, 95 lines, no artifacts
claude --print --output-format=json "Write JSON with emojis π and unicode δ½ ε₯½"Result: β Full Unicode support, no encoding issues
- You're already logged in via
claude(OAuth handled by binary) - The
--printmode uses the same authentication - Your Claude Max subscription applies automatically
- No token extraction needed - within Anthropic's TOS
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
}
}
}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"Build AI-powered document editors, content management systems, or writing assistants.
Automate code generation, refactoring, or documentation creation.
Extract structured data from unstructured text, analyze documents, or generate reports.
Build custom chatbots or agent systems using Claude as the reasoning engine.
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');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);
}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
- No session continuation in simple sync mode - Each call is independent
- Subprocess overhead - Each call spawns a process (~1-2s startup)
- Requires Claude Code installed - Binary must be available in PATH
- Rate limits apply - Same as interactive usage
- Use
dangerouslySkipPermissionscarefully - Only in sandboxed environments - Cache responses - Avoid redundant API calls
- Use cheaper models when possible - Haiku for simple tasks
- Track costs - Monitor spending across requests
- Handle errors gracefully - Wrap in try/catch blocks
| 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 | β Proper escaping |
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.
After documenting all of this, I discovered that Anthropic already built an official library for this called the 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.
Good question! Here's when you'd want the lightweight --print approach instead of the official SDK:
-
Prototyping / Quick Scripts
// One file, no dependencies, done const agent = new ClaudeAgent(); const result = agent.prompt('Quick task');
-
Minimal Dependencies
- The SDK is 71.9 MB unpacked
- Our approach: just
child_process(built-in)
-
Understanding the Foundation
- Good to know what the SDK wraps
- Useful for debugging SDK issues
- Educational value
-
Custom Lightweight Wrappers
- Build exactly what you need
- No extra features you won't use
- Total control over the interface
-
Simple Document Editing
- Don't need hooks or subagents
- Just want clean text in/out
- Synchronous operations are fine
- Production Agents - Full feature set, proper error handling
- Complex Workflows - Hooks, subagents, MCP servers
- Team Projects - Official support, documentation, updates
- Long-running Processes - Better session management
- Interactive Apps - User approval prompts, clarifying questions
| 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 | β Built-in | |
| Error handling | β Production-ready | |
| Streaming | β Built-in | |
| Types | β Full TypeScript | |
| Best for | Quick scripts | Production apps |
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 =
axiosorfetch(polished library)
Both hit the same underlying system, but the library adds convenience, safety, and features.
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! π
- Claude Code Documentation
- Claude API Documentation
- Model Context Protocol (MCP)
- Claude Agent SDK Overview
Discovered and documented by @JacobFV - January 2026
Found this useful? Star this gist and share with others building AI agents!