Capítulo 21: Referencia Rápida y Cookbook

Por: Artiko
claudeagent-sdkreferenciacookbookcheatsheetrecetas

Capítulo 21: Referencia Rápida y Cookbook

Este capítulo es una referencia, no un tutorial. Está organizado para búsqueda rápida y copia de código. Si necesitas contexto o explicaciones profundas, los capítulos anteriores cubren cada tema en detalle.


1. Cheat Sheet: ClaudeCodeOptions

Opciones Completas

OpciónTipoDefaultDescripción
max_turnsint | NoneNoneMáximo de turnos del agente. None = ilimitado
system_promptstr | NoneNoneInstrucciones del sistema adicionales
cwdstr | Path | NoneCWD actualDirectorio de trabajo del agente
allowed_toolslist[str] | NoneNone (todos)Lista blanca de herramientas
disallowed_toolslist[str] | NoneNoneLista negra de herramientas
permission_modePermissionMode | None"default"Modo de permisos de herramientas
modelstr | NoneNone (modelo default)Modelo de Claude a usar
session_idstr | NoneNoneID para continuar sesión previa
mcp_serversdict[str, MCPServer] | NoneNoneServidores MCP adicionales
output_formatstr | NoneNoneFormato de salida ("json", "text")
verboseboolFalseLogging verbose del CLI
executable_pathstr | NoneNonePath al binario claude
argslist[str] | NoneNoneArgumentos extra al CLI

Valores de permission_mode

ValorDescripciónCuándo Usar
"default"Claude pregunta confirmación para operaciones destructivasUso general
"acceptEdits"Auto-acepta ediciones de archivos, pregunta restoCI/CD con archivos conocidos
"bypassPermissions"Auto-acepta todo sin preguntarEntornos sandbox, testing
"plan"Solo planifica, no ejecuta herramientasRevisión antes de ejecutar

Python — opciones más usadas

from claude_code_sdk import query, ClaudeCodeOptions
from pathlib import Path

opts = ClaudeCodeOptions(
    max_turns=10,
    system_prompt="Eres un asistente de código experto en Python.",
    cwd=Path("/mi/proyecto"),
    allowed_tools=["Read", "Write", "Bash"],
    permission_mode="acceptEdits",
    model="claude-opus-4-5",
)

async for msg in query(prompt="Analiza el proyecto", options=opts):
    pass

TypeScript — opciones más usadas

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

const opts: ClaudeCodeOptions = {
  maxTurns: 10,
  systemPrompt: "Eres un asistente de código experto en TypeScript.",
  cwd: "/mi/proyecto",
  allowedTools: ["Read", "Write", "Bash"],
  permissionMode: "acceptEdits",
  model: "claude-opus-4-5",
};

for await (const msg of query({ prompt: "Analiza el proyecto", options: opts })) {
  // procesar mensajes
}

Nombres Python vs TypeScript

PythonTypeScript
max_turnsmaxTurns
system_promptsystemPrompt
cwdcwd
allowed_toolsallowedTools
disallowed_toolsdisallowedTools
permission_modepermissionMode
session_idsessionId
mcp_serversmcpServers
output_formatoutputFormat
executable_pathexecutablePath

2. Cheat Sheet: Tipos de Mensajes

Tabla de Tipos

TipoCuándo AparecePropiedades Clave
AssistantMessageClaude genera texto o usa herramientascontent: list[ContentBlock]
UserMessageInput del usuario (raro en SDK)content: list[ContentBlock]
ResultMessageAl final del stream, siempresubtype, cost_usd, session_id, total_cost_usd
SystemMessageMensajes del sistema del CLIsubtype, data

Subtipos de ResultMessage

subtypeSignifica
"success"El agente completó correctamente
"error_during_execution"Ocurrió un error durante la ejecución
"max_turns_reached"Se alcanzó el límite max_turns

Content Blocks dentro de AssistantMessage

Tipo de BlockPropiedadesCuándo
TextBlocktext: strClaude genera texto
ToolUseBlockid, name, inputClaude llama una herramienta

Snippets por tipo

Python — filtrar solo texto de AssistantMessage:

from claude_code_sdk import query, AssistantMessage, TextBlock

async for msg in query(prompt="Hola", options=opts):
    if isinstance(msg, AssistantMessage):
        for block in msg.content:
            if isinstance(block, TextBlock):
                print(block.text, end="", flush=True)

TypeScript — filtrar solo texto:

import { query, AssistantMessage, TextBlock } from "@anthropic-ai/claude-code-sdk";

for await (const msg of query({ prompt: "Hola", options: opts })) {
  if (msg.type === "assistant") {
    for (const block of msg.content) {
      if (block.type === "text") {
        process.stdout.write(block.text);
      }
    }
  }
}

Python — capturar ResultMessage:

from claude_code_sdk import query, ResultMessage

result = None
async for msg in query(prompt="...", options=opts):
    if isinstance(msg, ResultMessage):
        result = msg

if result:
    print(f"Estado: {result.subtype}")
    print(f"Costo: ${result.cost_usd:.4f}")
    print(f"Costo total sesión: ${result.total_cost_usd:.4f}")
    print(f"Session ID: {result.session_id}")

TypeScript — capturar ResultMessage:

import { query, ResultMessage } from "@anthropic-ai/claude-code-sdk";

let result: ResultMessage | null = null;
for await (const msg of query({ prompt: "...", options: opts })) {
  if (msg.type === "result") {
    result = msg;
  }
}

if (result) {
  console.log(`Estado: ${result.subtype}`);
  console.log(`Costo: $${result.costUsd?.toFixed(4)}`);
  console.log(`Session ID: ${result.sessionId}`);
}

Python — capturar uso de herramientas:

from claude_code_sdk import query, AssistantMessage, ToolUseBlock

async for msg in query(prompt="...", options=opts):
    if isinstance(msg, AssistantMessage):
        for block in msg.content:
            if isinstance(block, ToolUseBlock):
                print(f"Herramienta: {block.name}")
                print(f"Input: {block.input}")

TypeScript — capturar uso de herramientas:

for await (const msg of query({ prompt: "...", options: opts })) {
  if (msg.type === "assistant") {
    for (const block of msg.content) {
      if (block.type === "tool_use") {
        console.log(`Herramienta: ${block.name}`);
        console.log(`Input: ${JSON.stringify(block.input)}`);
      }
    }
  }
}

3. Cheat Sheet: Herramientas

Herramientas Nativas del CLI

HerramientaDescripciónParámetros Clavepermission_mode Requerido
ReadLee archivosfile_pathdefault
WriteCrea/sobreescribe archivosfile_path, contentacceptEdits o bypassPermissions
EditEdita secciones de archivosfile_path, old_string, new_stringacceptEdits o bypassPermissions
MultiEditMúltiples ediciones en un archivofile_path, edits: listacceptEdits o bypassPermissions
BashEjecuta comandos de shellcommand, timeout?default (pregunta por destructivos)
GlobBusca archivos por patrónpattern, path?default
GrepBusca contenido en archivospattern, path?, glob?default
LSLista directoriopathdefault
TaskLanza sub-agentedescription, promptdefault
TodoWriteEscribe lista de tareastodos: listdefault
TodoReadLee lista de tareas actualdefault
WebFetchDescarga URLurl, prompt?default
WebSearchBúsqueda webquerydefault

Cómo Restringir Herramientas

Python:

# Solo lectura
opts = ClaudeCodeOptions(
    allowed_tools=["Read", "Glob", "Grep", "LS"],
    permission_mode="default",
)

# Sin herramientas web
opts = ClaudeCodeOptions(
    disallowed_tools=["WebFetch", "WebSearch"],
)

# Solo Bash (para scripts)
opts = ClaudeCodeOptions(
    allowed_tools=["Bash"],
    permission_mode="bypassPermissions",
)

TypeScript:

// Solo lectura
const opts: ClaudeCodeOptions = {
  allowedTools: ["Read", "Glob", "Grep", "LS"],
  permissionMode: "default",
};

// Sin herramientas web
const opts: ClaudeCodeOptions = {
  disallowedTools: ["WebFetch", "WebSearch"],
};

4. Cheat Sheet: Hooks

Tipos de Eventos de Hook

EventoCuándo se DisparaRecibePuede Retornar
PreToolUseAntes de cada llamada a herramientatool_name, tool_input, session_id{"action": "block", "reason": "..."} para bloquear, {"action": "approve"} para aprobar
PostToolUseDespués de cada llamada a herramientatool_name, tool_input, tool_outputIgnorado (solo observación)
NotificationCuando Claude envía notificaciónmessage, session_idIgnorado
StopCuando el agente terminasession_id, resultIgnorado
SubagentStopCuando un sub-agente terminasession_id, resultIgnorado

Configuración de Hooks (settings.json)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python /hooks/auditar.py"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python /hooks/notificar.py"
          }
        ]
      }
    ]
  }
}

Snippets de Hooks Mínimos

Python — hook PreToolUse (bloquear archivos .env):

#!/usr/bin/env python3
import json, sys

data = json.loads(sys.stdin.read())
tool = data.get("tool_name", "")
inp = data.get("tool_input", {})

if tool in ("Write", "Edit") and ".env" in inp.get("file_path", ""):
    print(json.dumps({"action": "block", "reason": "No se permiten modificaciones a .env"}))
    sys.exit(0)

print(json.dumps({"action": "approve"}))

Python — hook PostToolUse (log de auditoría):

#!/usr/bin/env python3
import json, sys
from datetime import datetime

data = json.loads(sys.stdin.read())
with open("/var/log/agente-audit.log", "a") as f:
    f.write(json.dumps({
        "ts": datetime.utcnow().isoformat(),
        "tool": data.get("tool_name"),
        "input": data.get("tool_input"),
    }) + "\n")

Python — hook Stop (notificación):

#!/usr/bin/env python3
import json, sys, subprocess

data = json.loads(sys.stdin.read())
result = data.get("result", {})
subtype = result.get("subtype", "unknown")
subprocess.run(["notify-send", f"Agente terminó: {subtype}"])

5. Cookbook: Recetas de Uso Frecuente

Receta 1: Hello World Mínimo

Cuándo usar: Primer test de que el SDK funciona.

Python:

import asyncio
from claude_code_sdk import query, ClaudeCodeOptions

async def main():
    async for msg in query(
        prompt="Di 'hola mundo' en español",
        options=ClaudeCodeOptions(max_turns=1),
    ):
        print(msg)

asyncio.run(main())

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

async function main() {
  for await (const msg of query({
    prompt: "Di 'hola mundo' en español",
    options: { maxTurns: 1 },
  })) {
    console.log(msg);
  }
}

main();

Receta 2: Extraer Solo el Resultado Final

Cuándo usar: Solo necesitas el texto de respuesta de Claude, sin metadata.

Python:

import asyncio
from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage, TextBlock

async def get_text(prompt: str) -> str:
    partes = []
    async for msg in query(
        prompt=prompt,
        options=ClaudeCodeOptions(max_turns=1),
    ):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    partes.append(block.text)
    return "".join(partes)

resultado = asyncio.run(get_text("¿Cuánto es 2+2?"))
print(resultado)

TypeScript:

import { query, ClaudeCodeOptions, AssistantMessage } from "@anthropic-ai/claude-code-sdk";

async function getText(prompt: string): Promise<string> {
  const parts: string[] = [];
  for await (const msg of query({ prompt, options: { maxTurns: 1 } })) {
    if (msg.type === "assistant") {
      for (const block of msg.content) {
        if (block.type === "text") {
          parts.push(block.text);
        }
      }
    }
  }
  return parts.join("");
}

const resultado = await getText("¿Cuánto es 2+2?");
console.log(resultado);

Receta 3: Streaming de Texto en Tiempo Real

Cuándo usar: Mostrar la respuesta mientras llega, sin esperar al final.

Python:

import asyncio
import sys
from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage, TextBlock

async def stream_respuesta(prompt: str):
    async for msg in query(
        prompt=prompt,
        options=ClaudeCodeOptions(max_turns=1),
    ):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    sys.stdout.write(block.text)
                    sys.stdout.flush()
    print()  # newline final

asyncio.run(stream_respuesta("Explica qué es un closure en 3 párrafos"))

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

async function streamRespuesta(prompt: string) {
  for await (const msg of query({ prompt, options: { maxTurns: 1 } })) {
    if (msg.type === "assistant") {
      for (const block of msg.content) {
        if (block.type === "text") {
          process.stdout.write(block.text);
        }
      }
    }
  }
  process.stdout.write("\n");
}

await streamRespuesta("Explica qué es un closure en 3 párrafos");

Receta 4: Agente que Analiza un Directorio

Cuándo usar: Obtener resumen o análisis de un proyecto existente.

Python:

import asyncio
from pathlib import Path
from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage, TextBlock

async def analizar_directorio(path: str) -> str:
    resultado = []
    opts = ClaudeCodeOptions(
        cwd=Path(path),
        allowed_tools=["Read", "Glob", "Grep", "LS"],
        max_turns=5,
        system_prompt="Analiza el proyecto y produce un resumen conciso.",
    )
    async for msg in query(
        prompt="Lista los archivos principales y resume la arquitectura del proyecto.",
        options=opts,
    ):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    resultado.append(block.text)
    return "".join(resultado)

print(asyncio.run(analizar_directorio("/mi/proyecto")))

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

async function analizarDirectorio(path: string): Promise<string> {
  const parts: string[] = [];
  const opts: ClaudeCodeOptions = {
    cwd: path,
    allowedTools: ["Read", "Glob", "Grep", "LS"],
    maxTurns: 5,
    systemPrompt: "Analiza el proyecto y produce un resumen conciso.",
  };
  for await (const msg of query({
    prompt: "Lista los archivos principales y resume la arquitectura.",
    options: opts,
  })) {
    if (msg.type === "assistant") {
      for (const block of msg.content) {
        if (block.type === "text") parts.push(block.text);
      }
    }
  }
  return parts.join("");
}

Receta 5: Agente que Crea un Archivo

Cuándo usar: Generar código o contenido y guardarlo en disco.

Python:

import asyncio
from pathlib import Path
from claude_code_sdk import query, ClaudeCodeOptions, ResultMessage

async def crear_archivo(descripcion: str, ruta_destino: str):
    opts = ClaudeCodeOptions(
        cwd=Path(ruta_destino).parent,
        allowed_tools=["Write"],
        permission_mode="bypassPermissions",
        max_turns=3,
    )
    async for msg in query(
        prompt=f"Crea el archivo {ruta_destino} con el siguiente contenido: {descripcion}",
        options=opts,
    ):
        if isinstance(msg, ResultMessage):
            print(f"Estado: {msg.subtype}")

asyncio.run(crear_archivo("función Python que ordena una lista", "/tmp/ordenar.py"))

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";
import path from "path";

async function crearArchivo(descripcion: string, rutaDestino: string) {
  const opts: ClaudeCodeOptions = {
    cwd: path.dirname(rutaDestino),
    allowedTools: ["Write"],
    permissionMode: "bypassPermissions",
    maxTurns: 3,
  };
  for await (const msg of query({
    prompt: `Crea el archivo ${rutaDestino} con: ${descripcion}`,
    options: opts,
  })) {
    if (msg.type === "result") {
      console.log(`Estado: ${msg.subtype}`);
    }
  }
}

Receta 6: Agente que Ejecuta Tests

Cuándo usar: Correr la suite de tests y reportar resultado.

Python:

import asyncio
from pathlib import Path
from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage, TextBlock

async def ejecutar_tests(proyecto: str) -> dict:
    salida = []
    opts = ClaudeCodeOptions(
        cwd=Path(proyecto),
        allowed_tools=["Bash"],
        permission_mode="bypassPermissions",
        max_turns=3,
        system_prompt="Ejecuta los tests y reporta: total, pasados, fallados.",
    )
    async for msg in query(
        prompt="Ejecuta los tests del proyecto y dame el resumen.",
        options=opts,
    ):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    salida.append(block.text)
    return {"salida": "".join(salida)}

resultado = asyncio.run(ejecutar_tests("/mi/proyecto"))
print(resultado["salida"])

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

async function ejecutarTests(proyecto: string): Promise<string> {
  const salida: string[] = [];
  const opts: ClaudeCodeOptions = {
    cwd: proyecto,
    allowedTools: ["Bash"],
    permissionMode: "bypassPermissions",
    maxTurns: 3,
    systemPrompt: "Ejecuta los tests y reporta: total, pasados, fallados.",
  };
  for await (const msg of query({
    prompt: "Ejecuta los tests del proyecto y dame el resumen.",
    options: opts,
  })) {
    if (msg.type === "assistant") {
      for (const block of msg.content) {
        if (block.type === "text") salida.push(block.text);
      }
    }
  }
  return salida.join("");
}

Receta 7: Agente con Sistema de Logging (Hooks)

Cuándo usar: Auditar todas las acciones del agente para debugging o compliance.

Python — hook de auditoría (/hooks/audit.py):

#!/usr/bin/env python3
import json, sys
from datetime import datetime
from pathlib import Path

LOG_FILE = Path("/var/log/claude-audit.jsonl")
data = json.loads(sys.stdin.read())
entry = {
    "ts": datetime.utcnow().isoformat(),
    "event": data.get("hook_event_name"),
    "tool": data.get("tool_name"),
    "input": data.get("tool_input"),
    "session": data.get("session_id"),
}
with LOG_FILE.open("a") as f:
    f.write(json.dumps(entry) + "\n")

Python — leer log de auditoría:

import json
from pathlib import Path

def leer_audit_log(path: str = "/var/log/claude-audit.jsonl"):
    lineas = Path(path).read_text().strip().split("\n")
    return [json.loads(l) for l in lineas if l]

for entrada in leer_audit_log():
    print(f"{entrada['ts']} | {entrada['tool']} | {entrada['input']}")

TypeScript — leer log de auditoría:

import { readFileSync } from "fs";

function leerAuditLog(path = "/var/log/claude-audit.jsonl") {
  return readFileSync(path, "utf-8")
    .trim()
    .split("\n")
    .filter(Boolean)
    .map((line) => JSON.parse(line));
}

for (const entrada of leerAuditLog()) {
  console.log(`${entrada.ts} | ${entrada.tool} | ${JSON.stringify(entrada.input)}`);
}

Receta 8: Agente con Timeout

Cuándo usar: Evitar que el agente se cuelgue en tareas largas.

Python:

import asyncio
from claude_code_sdk import query, ClaudeCodeOptions, ResultMessage

async def query_con_timeout(prompt: str, timeout_seg: int = 30) -> str | None:
    salida = []
    try:
        async with asyncio.timeout(timeout_seg):
            async for msg in query(
                prompt=prompt,
                options=ClaudeCodeOptions(max_turns=10),
            ):
                if isinstance(msg, ResultMessage):
                    return msg.subtype
    except asyncio.TimeoutError:
        print(f"Timeout después de {timeout_seg}s")
        return None

resultado = asyncio.run(query_con_timeout("Analiza este proyecto", timeout_seg=60))

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

async function queryConTimeout(prompt: string, timeoutMs = 30_000): Promise<string | null> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    for await (const msg of query({ prompt, options: { maxTurns: 10 } })) {
      if (msg.type === "result") {
        clearTimeout(timer);
        return msg.subtype;
      }
    }
  } catch (err) {
    if ((err as Error).name === "AbortError") {
      console.log(`Timeout después de ${timeoutMs}ms`);
      return null;
    }
    throw err;
  } finally {
    clearTimeout(timer);
  }
  return null;
}

Receta 9: Agente con Retry Automático

Cuándo usar: Resiliencia ante errores transitorios de red o API.

Python:

import asyncio
from claude_code_sdk import query, ClaudeCodeOptions, ResultMessage
from claude_code_sdk.errors import CLIConnectionError

async def query_con_retry(
    prompt: str,
    max_intentos: int = 3,
    espera_base: float = 2.0,
) -> str | None:
    for intento in range(max_intentos):
        try:
            async for msg in query(
                prompt=prompt,
                options=ClaudeCodeOptions(max_turns=5),
            ):
                if isinstance(msg, ResultMessage):
                    return msg.subtype
        except CLIConnectionError as e:
            if intento == max_intentos - 1:
                raise
            espera = espera_base * (2 ** intento)
            print(f"Intento {intento+1} fallido: {e}. Esperando {espera}s...")
            await asyncio.sleep(espera)
    return None

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

async function queryConRetry(
  prompt: string,
  maxIntentos = 3,
  espera = 2000,
): Promise<string | null> {
  for (let intento = 0; intento < maxIntentos; intento++) {
    try {
      for await (const msg of query({ prompt, options: { maxTurns: 5 } })) {
        if (msg.type === "result") return msg.subtype;
      }
    } catch (err) {
      if (intento === maxIntentos - 1) throw err;
      const delay = espera * Math.pow(2, intento);
      console.log(`Intento ${intento + 1} fallido. Esperando ${delay}ms...`);
      await new Promise((r) => setTimeout(r, delay));
    }
  }
  return null;
}

Receta 10: Reanudar Sesión Anterior

Cuándo usar: Continuar una conversación o tarea donde se dejó.

Python:

import asyncio
from claude_code_sdk import query, ClaudeCodeOptions, ResultMessage

# Primera sesión — guardar el session_id
async def primera_sesion() -> str:
    session_id = None
    async for msg in query(
        prompt="Empieza a analizar el archivo main.py",
        options=ClaudeCodeOptions(max_turns=5, cwd="/mi/proyecto"),
    ):
        if isinstance(msg, ResultMessage):
            session_id = msg.session_id
    return session_id

# Segunda sesión — continuar
async def continuar_sesion(session_id: str):
    async for msg in query(
        prompt="Continúa el análisis que empezaste",
        options=ClaudeCodeOptions(
            max_turns=5,
            cwd="/mi/proyecto",
            session_id=session_id,
        ),
    ):
        pass

sid = asyncio.run(primera_sesion())
print(f"Session ID guardado: {sid}")
asyncio.run(continuar_sesion(sid))

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ic/claude-code-sdk";

async function primeraSesion(): Promise<string | undefined> {
  let sessionId: string | undefined;
  for await (const msg of query({
    prompt: "Empieza a analizar el archivo main.ts",
    options: { maxTurns: 5, cwd: "/mi/proyecto" },
  })) {
    if (msg.type === "result") sessionId = msg.sessionId;
  }
  return sessionId;
}

async function continuarSesion(sessionId: string) {
  for await (const msg of query({
    prompt: "Continúa el análisis que empezaste",
    options: { maxTurns: 5, cwd: "/mi/proyecto", sessionId },
  })) {
    // procesar
  }
}

Receta 11: Múltiples Agentes en Paralelo

Cuándo usar: Analizar varios archivos o módulos simultáneamente.

Python:

import asyncio
from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage, TextBlock

async def analizar_modulo(modulo: str) -> tuple[str, str]:
    partes = []
    async for msg in query(
        prompt=f"Resume en 2 oraciones qué hace el módulo {modulo}",
        options=ClaudeCodeOptions(
            cwd="/mi/proyecto",
            allowed_tools=["Read", "Glob"],
            max_turns=3,
        ),
    ):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    partes.append(block.text)
    return modulo, "".join(partes)

async def analizar_todos():
    modulos = ["auth", "payments", "notifications", "reports"]
    tareas = [analizar_modulo(m) for m in modulos]
    resultados = await asyncio.gather(*tareas)
    for modulo, resumen in resultados:
        print(f"\n## {modulo}\n{resumen}")

asyncio.run(analizar_todos())

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

async function analizarModulo(modulo: string): Promise<[string, string]> {
  const parts: string[] = [];
  for await (const msg of query({
    prompt: `Resume en 2 oraciones qué hace el módulo ${modulo}`,
    options: { cwd: "/mi/proyecto", allowedTools: ["Read", "Glob"], maxTurns: 3 },
  })) {
    if (msg.type === "assistant") {
      for (const block of msg.content) {
        if (block.type === "text") parts.push(block.text);
      }
    }
  }
  return [modulo, parts.join("")];
}

async function analizarTodos() {
  const modulos = ["auth", "payments", "notifications", "reports"];
  const resultados = await Promise.all(modulos.map(analizarModulo));
  for (const [modulo, resumen] of resultados) {
    console.log(`\n## ${modulo}\n${resumen}`);
  }
}

Receta 12: Agente con Herramienta Custom (MCP In-Process)

Cuándo usar: Exponer una función propia como herramienta al agente.

Python:

import asyncio
import json
from claude_code_sdk import query, ClaudeCodeOptions

# El MCP in-process se define como servidor MCP simple en un proceso separado
# Para uso básico, usa system_prompt para describir la herramienta como función Bash

opts = ClaudeCodeOptions(
    allowed_tools=["Bash"],
    permission_mode="bypassPermissions",
    system_prompt="""Tienes acceso a una herramienta custom via Bash:
    Para consultar la base de datos interna usa: python /tools/db_query.py '<sql>'
    """,
    max_turns=5,
)

async def main():
    async for msg in query(
        prompt="Cuántos usuarios se registraron hoy",
        options=opts,
    ):
        pass

asyncio.run(main())

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

const opts: ClaudeCodeOptions = {
  allowedTools: ["Bash"],
  permissionMode: "bypassPermissions",
  systemPrompt: `Tienes acceso a una herramienta custom via Bash:
Para consultar la BD interna usa: node /tools/db_query.js '<sql>'`,
  maxTurns: 5,
};

for await (const msg of query({
  prompt: "Cuántos usuarios se registraron hoy",
  options: opts,
})) {
  // procesar
}

Receta 13: Agente con Playwright (MCP Externo)

Cuándo usar: Automatizar tareas de navegador web.

Python:

import asyncio
from claude_code_sdk import query, ClaudeCodeOptions
from claude_code_sdk.types import MCPStdioServer

opts = ClaudeCodeOptions(
    mcp_servers={
        "playwright": MCPStdioServer(
            command="npx",
            args=["@playwright/mcp"],
        )
    },
    allowed_tools=["mcp__playwright__browser_navigate", "mcp__playwright__browser_take_screenshot"],
    permission_mode="bypassPermissions",
    max_turns=10,
)

async def main():
    async for msg in query(
        prompt="Navega a https://example.com y toma un screenshot",
        options=opts,
    ):
        pass

asyncio.run(main())

TypeScript:

import { query, ClaudeCodeOptions, MCPStdioServer } from "@anthropic-ai/claude-code-sdk";

const opts: ClaudeCodeOptions = {
  mcpServers: {
    playwright: {
      command: "npx",
      args: ["@playwright/mcp"],
    } as MCPStdioServer,
  },
  allowedTools: ["mcp__playwright__browser_navigate", "mcp__playwright__browser_take_screenshot"],
  permissionMode: "bypassPermissions",
  maxTurns: 10,
};

for await (const msg of query({
  prompt: "Navega a https://example.com y toma un screenshot",
  options: opts,
})) {
  // procesar
}

Receta 14: Agente con PostgreSQL (MCP para Base de Datos)

Cuándo usar: Consultar o modificar datos en PostgreSQL con lenguaje natural.

Python:

import asyncio
from claude_code_sdk import query, ClaudeCodeOptions
from claude_code_sdk.types import MCPStdioServer

DATABASE_URL = "postgresql://user:pass@localhost:5432/midb"

opts = ClaudeCodeOptions(
    mcp_servers={
        "postgres": MCPStdioServer(
            command="npx",
            args=["-y", "@modelcontextprotocol/server-postgres", DATABASE_URL],
        )
    },
    permission_mode="bypassPermissions",
    max_turns=5,
    system_prompt="Solo ejecuta consultas SELECT. Nunca modifiques datos.",
)

async def consulta_nl(pregunta: str) -> str:
    partes = []
    async for msg in query(prompt=pregunta, options=opts):
        if hasattr(msg, "content"):
            for block in msg.content:
                if hasattr(block, "text"):
                    partes.append(block.text)
    return "".join(partes)

print(asyncio.run(consulta_nl("¿Cuáles son los 5 productos más vendidos este mes?")))

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

const DATABASE_URL = "postgresql://user:pass@localhost:5432/midb";

const opts: ClaudeCodeOptions = {
  mcpServers: {
    postgres: {
      command: "npx",
      args: ["-y", "@modelcontextprotocol/server-postgres", DATABASE_URL],
    },
  },
  permissionMode: "bypassPermissions",
  maxTurns: 5,
  systemPrompt: "Solo ejecuta consultas SELECT. Nunca modifiques datos.",
};

async function consultaNL(pregunta: string): Promise<string> {
  const parts: string[] = [];
  for await (const msg of query({ prompt: pregunta, options: opts })) {
    if (msg.type === "assistant") {
      for (const block of msg.content) {
        if (block.type === "text") parts.push(block.text);
      }
    }
  }
  return parts.join("");
}

Receta 15: Agente con Rate Limiting

Cuándo usar: No superar un número máximo de queries por minuto.

Python:

import asyncio
import time
from claude_code_sdk import query, ClaudeCodeOptions

class RateLimiter:
    def __init__(self, max_por_minuto: int):
        self.max = max_por_minuto
        self.ventana = 60.0
        self.timestamps: list[float] = []

    async def esperar(self):
        ahora = time.monotonic()
        # Limpiar timestamps viejos
        self.timestamps = [t for t in self.timestamps if ahora - t < self.ventana]
        if len(self.timestamps) >= self.max:
            mas_antiguo = self.timestamps[0]
            espera = self.ventana - (ahora - mas_antiguo)
            if espera > 0:
                await asyncio.sleep(espera)
        self.timestamps.append(time.monotonic())

limiter = RateLimiter(max_por_minuto=10)

async def query_limitado(prompt: str):
    await limiter.esperar()
    async for msg in query(
        prompt=prompt,
        options=ClaudeCodeOptions(max_turns=3),
    ):
        pass

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

class RateLimiter {
  private timestamps: number[] = [];
  constructor(private maxPorMinuto: number, private ventana = 60_000) {}

  async esperar(): Promise<void> {
    const ahora = Date.now();
    this.timestamps = this.timestamps.filter((t) => ahora - t < this.ventana);
    if (this.timestamps.length >= this.maxPorMinuto) {
      const masAntiguo = this.timestamps[0];
      const espera = this.ventana - (ahora - masAntiguo);
      if (espera > 0) await new Promise((r) => setTimeout(r, espera));
    }
    this.timestamps.push(Date.now());
  }
}

const limiter = new RateLimiter(10);

async function queryLimitado(prompt: string) {
  await limiter.esperar();
  for await (const msg of query({ prompt, options: { maxTurns: 3 } })) {
    // procesar
  }
}

Receta 16: Agente Seguro Solo Lectura

Cuándo usar: Análisis de código sin riesgo de modificar nada.

Python:

import asyncio
from pathlib import Path
from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage, TextBlock

SOLO_LECTURA = ClaudeCodeOptions(
    allowed_tools=["Read", "Glob", "Grep", "LS"],
    permission_mode="default",
    system_prompt="Solo puedes leer archivos. Nunca sugieras crear, modificar ni eliminar nada.",
    max_turns=10,
)

async def analisis_seguro(proyecto: str, tarea: str) -> str:
    partes = []
    opts = ClaudeCodeOptions(
        **{**SOLO_LECTURA.__dict__, "cwd": Path(proyecto)}
    )
    async for msg in query(prompt=tarea, options=opts):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    partes.append(block.text)
    return "".join(partes)

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

const baseOpts: ClaudeCodeOptions = {
  allowedTools: ["Read", "Glob", "Grep", "LS"],
  permissionMode: "default",
  systemPrompt: "Solo puedes leer archivos. Nunca sugieras crear, modificar ni eliminar nada.",
  maxTurns: 10,
};

async function analisisSeguro(proyecto: string, tarea: string): Promise<string> {
  const parts: string[] = [];
  for await (const msg of query({
    prompt: tarea,
    options: { ...baseOpts, cwd: proyecto },
  })) {
    if (msg.type === "assistant") {
      for (const block of msg.content) {
        if (block.type === "text") parts.push(block.text);
      }
    }
  }
  return parts.join("");
}

Receta 17: Agente con Hook de Seguridad

Cuándo usar: Bloquear acceso a archivos o rutas sensibles.

Python — hook de seguridad (/hooks/security.py):

#!/usr/bin/env python3
import json, sys, re

RUTAS_BLOQUEADAS = [
    r"\.env$", r"\.env\.", r"secrets\.", r"credentials\.",
    r"id_rsa", r"\.pem$", r"\.key$", r"config/database\.yml",
]

data = json.loads(sys.stdin.read())
tool = data.get("tool_name", "")
inp = data.get("tool_input", {})

if tool in ("Read", "Write", "Edit"):
    ruta = inp.get("file_path", "")
    for patron in RUTAS_BLOQUEADAS:
        if re.search(patron, ruta):
            print(json.dumps({
                "action": "block",
                "reason": f"Acceso bloqueado a archivo sensible: {ruta}",
            }))
            sys.exit(0)

if tool == "Bash":
    cmd = inp.get("command", "")
    if "env" in cmd and "export" not in cmd:
        print(json.dumps({
            "action": "block",
            "reason": "Comando que accede a variables de entorno bloqueado",
        }))
        sys.exit(0)

print(json.dumps({"action": "approve"}))

TypeScript — lector del resultado del hook:

import { execSync } from "child_process";

function verificarPermisoHook(toolName: string, toolInput: object): boolean {
  const input = JSON.stringify({ tool_name: toolName, tool_input: toolInput });
  const result = execSync("python /hooks/security.py", {
    input,
    encoding: "utf-8",
  });
  const response = JSON.parse(result);
  return response.action === "approve";
}

Receta 18: Agente que Manda Notificación al Terminar

Cuándo usar: Tareas largas donde quieres ser notificado cuando terminan.

Python — hook Stop (/hooks/notificar.py):

#!/usr/bin/env python3
import json, sys, os, smtplib
from email.message import EmailMessage

data = json.loads(sys.stdin.read())
result = data.get("result", {})
subtype = result.get("subtype", "unknown")
session = data.get("session_id", "?")

# Opción 1: notificación del sistema
try:
    os.system(f'notify-send "Agente Claude terminó" "Estado: {subtype} | Sesión: {session}"')
except Exception:
    pass

# Opción 2: email (configurar SMTP)
# msg = EmailMessage()
# msg["Subject"] = f"Agente Claude: {subtype}"
# msg["From"] = "[email protected]"
# msg["To"] = "[email protected]"
# msg.set_content(f"Sesión {session} terminó con estado: {subtype}")
# with smtplib.SMTP("smtp.empresa.com", 587) as s:
#     s.send_message(msg)

TypeScript — webhook al terminar:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

async function queryConNotificacion(prompt: string, webhookUrl: string) {
  let result = null;
  for await (const msg of query({ prompt, options: { maxTurns: 20 } })) {
    if (msg.type === "result") result = msg;
  }

  // Notificar via webhook
  await fetch(webhookUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      estado: result?.subtype,
      sesion: result?.sessionId,
      costo: result?.totalCostUsd,
    }),
  });
}

Receta 19: Calcular Costo de una Query

Cuándo usar: Monitorear gasto de API, presupuesto por tarea.

Python:

import asyncio
from claude_code_sdk import query, ClaudeCodeOptions, ResultMessage

async def query_con_costo(prompt: str) -> dict:
    result_msg = None
    async for msg in query(
        prompt=prompt,
        options=ClaudeCodeOptions(max_turns=10),
    ):
        if isinstance(msg, ResultMessage):
            result_msg = msg

    return {
        "estado": result_msg.subtype if result_msg else None,
        "costo_query": result_msg.cost_usd if result_msg else 0,
        "costo_sesion_total": result_msg.total_cost_usd if result_msg else 0,
        "session_id": result_msg.session_id if result_msg else None,
    }

info = asyncio.run(query_con_costo("Analiza la arquitectura del proyecto"))
print(f"Costo esta query: ${info['costo_query']:.4f}")
print(f"Costo total sesión: ${info['costo_sesion_total']:.4f}")

TypeScript:

import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

interface CostoResult {
  estado: string | undefined;
  costoQuery: number;
  costoSesionTotal: number;
  sessionId: string | undefined;
}

async function queryConCosto(prompt: string): Promise<CostoResult> {
  let result = null;
  for await (const msg of query({ prompt, options: { maxTurns: 10 } })) {
    if (msg.type === "result") result = msg;
  }
  return {
    estado: result?.subtype,
    costoQuery: result?.costUsd ?? 0,
    costoSesionTotal: result?.totalCostUsd ?? 0,
    sessionId: result?.sessionId,
  };
}

const info = await queryConCosto("Analiza la arquitectura del proyecto");
console.log(`Costo esta query: $${info.costoQuery.toFixed(4)}`);
console.log(`Costo total sesión: $${info.costoSesionTotal.toFixed(4)}`);

Receta 20: Agente como Endpoint FastAPI

Cuándo usar: Exponer un agente como servicio HTTP REST básico.

Python:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import asyncio
from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage, TextBlock, ResultMessage

app = FastAPI(title="Agente Claude API")

class QueryRequest(BaseModel):
    prompt: str
    max_turns: int = 10
    cwd: str = "/workspace"

class QueryResponse(BaseModel):
    respuesta: str
    estado: str
    costo_usd: float

@app.post("/query", response_model=QueryResponse)
async def ejecutar_query(req: QueryRequest):
    partes = []
    estado = "unknown"
    costo = 0.0

    try:
        opts = ClaudeCodeOptions(
            max_turns=req.max_turns,
            cwd=req.cwd,
            allowed_tools=["Read", "Glob", "Grep", "LS"],
            permission_mode="default",
        )
        async for msg in query(prompt=req.prompt, options=opts):
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        partes.append(block.text)
            elif isinstance(msg, ResultMessage):
                estado = msg.subtype
                costo = msg.cost_usd or 0.0
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

    return QueryResponse(
        respuesta="".join(partes),
        estado=estado,
        costo_usd=costo,
    )

# uvicorn app:app --host 0.0.0.0 --port 8000

TypeScript (Express):

import express from "express";
import { query, ClaudeCodeOptions } from "@anthropic-ai/claude-code-sdk";

const app = express();
app.use(express.json());

app.post("/query", async (req, res) => {
  const { prompt, maxTurns = 10, cwd = "/workspace" } = req.body;
  const parts: string[] = [];
  let estado = "unknown";
  let costo = 0;

  try {
    const opts: ClaudeCodeOptions = {
      maxTurns,
      cwd,
      allowedTools: ["Read", "Glob", "Grep", "LS"],
      permissionMode: "default",
    };
    for await (const msg of query({ prompt, options: opts })) {
      if (msg.type === "assistant") {
        for (const block of msg.content) {
          if (block.type === "text") parts.push(block.text);
        }
      } else if (msg.type === "result") {
        estado = msg.subtype;
        costo = msg.costUsd ?? 0;
      }
    }
    res.json({ respuesta: parts.join(""), estado, costoUsd: costo });
  } catch (err) {
    res.status(500).json({ error: (err as Error).message });
  }
});

app.listen(8000, () => console.log("Servidor en http://localhost:8000"));

6. Troubleshooting Guide

Diagnóstico Rápido

flowchart TD
    Error[Error al ejecutar agente] --> TipoCLI{¿CLINotFoundError?}
    TipoCLI -->|Sí| InstalarCLI[Instalar Claude Code CLI:\nnpm install -g @anthropic-ai/claude-code]
    TipoCLI -->|No| TipoConex{¿CLIConnectionError?}
    TipoConex -->|Sí| VerAPIKey[Verificar ANTHROPIC_API_KEY\ny conectividad de red]
    TipoConex -->|No| TipoProceso{¿ProcessError?}
    TipoProceso -->|Sí| VerExitCode[Ver exit_code y stderr\npara diagnóstico específico]
    TipoProceso -->|No| TipoTimeout{¿Stream se corta?}
    TipoTimeout -->|Sí| AgregarTimeout[Agregar asyncio.timeout()\no AbortController]
    TipoTimeout -->|No| OtroError[Revisar sección específica\nen esta guía]

Problema 1: CLINotFoundError — Claude Code CLI No Instalado

Síntoma: CLINotFoundError: claude CLI not found

Diagnóstico:

which claude
claude --version

Solución:

# Instalar globalmente con npm
npm install -g @anthropic-ai/claude-code

# Verificar PATH
echo $PATH
export PATH="$HOME/.npm-global/bin:$PATH"

Python — verificar en código:

import shutil
if not shutil.which("claude"):
    raise RuntimeError("Claude Code CLI no está instalado. Ejecuta: npm install -g @anthropic-ai/claude-code")

Problema 2: CLIConnectionError — Fallo de Conexión

Síntoma: CLIConnectionError: Failed to connect to CLI process

Causas comunes:

Solución:

export ANTHROPIC_API_KEY="sk-ant-..."
claude --version  # verificar que el CLI responde

Python:

import os
if not os.environ.get("ANTHROPIC_API_KEY"):
    raise EnvironmentError("ANTHROPIC_API_KEY no está configurada")

Problema 3: ProcessError con exit_code 1

Síntoma: ProcessError: Process exited with code 1

Diagnóstico:

from claude_code_sdk.errors import ProcessError

try:
    async for msg in query(prompt="...", options=opts):
        pass
except ProcessError as e:
    print(f"Exit code: {e.exit_code}")
    print(f"Stderr: {e.stderr}")

Causas y soluciones:

Exit CodeCausa ProbableSolución
1Error genérico del CLIVer stderr para detalles
2Argumento inválidoVerificar ClaudeCodeOptions
126Permiso denegado al ejecutarchmod +x $(which claude)
127CLI no encontradoVer Problema 1

Problema 4: API Key Inválida o No Configurada

Síntoma: Error 401 en stderr, o mensaje de “authentication failed”

Solución:

# Configurar la key
export ANTHROPIC_API_KEY="sk-ant-api03-..."

# Verificar que es válida
curl -H "x-api-key: $ANTHROPIC_API_KEY" https://api.anthropic.com/v1/models

Python — configurar programáticamente:

import os
os.environ["ANTHROPIC_API_KEY"] = "sk-ant-..."  # Antes de importar el SDK

Problema 5: Agente en Loop Infinito

Síntoma: El agente no termina, consume muchos tokens.

Diagnóstico: Contar turnos y herramientas usadas.

Solución:

from claude_code_sdk import query, ClaudeCodeOptions, ResultMessage, AssistantMessage, ToolUseBlock

async def query_con_limite(prompt: str):
    conteo_herramientas = 0
    MAX_HERRAMIENTAS = 50

    opts = ClaudeCodeOptions(
        max_turns=20,  # SIEMPRE poner max_turns
        system_prompt="Sé conciso. Si no puedes resolver algo en 5 pasos, detente y reporta.",
    )
    async for msg in query(prompt=prompt, options=opts):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, ToolUseBlock):
                    conteo_herramientas += 1
                    if conteo_herramientas > MAX_HERRAMIENTAS:
                        raise RuntimeError(f"Loop detectado: {conteo_herramientas} herramientas usadas")

Problema 6: Agente No Usa las Herramientas Esperadas

Síntoma: El agente escribe código en lugar de ejecutarlo, o no lee archivos.

Solución:

opts = ClaudeCodeOptions(
    # 1. Especifica las herramientas explícitamente
    allowed_tools=["Read", "Bash", "Glob"],
    # 2. Guía en el system_prompt
    system_prompt="SIEMPRE usa las herramientas disponibles. No generes código de memoria.",
    # 3. Sé específico en el prompt
)

await query(prompt="USA la herramienta Read para leer /mi/archivo.py y analiza su contenido", options=opts)

Problema 7: Write/Edit Fallan por Permisos

Síntoma: Claude intenta escribir pero la operación es bloqueada o necesita confirmación.

Solución:

# Para entornos automatizados:
opts = ClaudeCodeOptions(
    permission_mode="bypassPermissions",  # O "acceptEdits" para solo archivos
)

# Para revisar qué pide permiso, usar "plan" primero:
opts_plan = ClaudeCodeOptions(permission_mode="plan")

Verificar que el proceso tiene permisos del OS:

ls -la /ruta/al/archivo
whoami

Problema 8: El Stream se Corta sin ResultMessage

Síntoma: El async for termina pero nunca llega un ResultMessage.

Diagnóstico:

result_recibido = False
async for msg in query(prompt="...", options=opts):
    if isinstance(msg, ResultMessage):
        result_recibido = True

if not result_recibido:
    print("ADVERTENCIA: Stream terminó sin ResultMessage — el proceso fue terminado externamente")

Causas: timeout del sistema, OOM killer, señal SIGTERM.


Problema 9: MCP Server No Inicia

Síntoma: Error: MCP server 'nombre' failed to start

Diagnóstico:

# Probar el servidor MCP manualmente
npx @modelcontextprotocol/server-postgres postgresql://...

# Verificar que npx puede encontrar el paquete
npx --yes @modelcontextprotocol/server-postgres --help

Python — agregar logging al servidor MCP:

from claude_code_sdk.types import MCPStdioServer

opts = ClaudeCodeOptions(
    mcp_servers={
        "postgres": MCPStdioServer(
            command="npx",
            args=["-y", "@modelcontextprotocol/server-postgres", DATABASE_URL],
            env={"DEBUG": "mcp:*"},  # logging de debug del servidor
        )
    },
    verbose=True,  # logging del SDK
)

Problema 10: Session Resume No Funciona

Síntoma: session_id ignorado o error “session not found”

Causas:

  1. El session_id expiró (las sesiones tienen TTL)
  2. El session_id es de una instancia diferente del CLI
  3. Se usó session_id sin esperar a que el primer stream termine

Solución:

# CORRECTO: Esperar a que el primer stream complete
async def flujo_correcto():
    session_id = None

    # Primera sesión — completar TODA la iteración
    async for msg in query(prompt="Tarea 1", options=ClaudeCodeOptions()):
        if isinstance(msg, ResultMessage):
            session_id = msg.session_id  # Guardar AQUÍ

    # Segunda sesión — solo después
    if session_id:
        async for msg in query(
            prompt="Tarea 2",
            options=ClaudeCodeOptions(session_id=session_id),
        ):
            pass

Problema 11: Costo Inesperadamente Alto

Síntoma: Facturas de API mucho mayores de lo esperado.

Diagnóstico:

from claude_code_sdk import query, ClaudeCodeOptions, ResultMessage, AssistantMessage, ToolUseBlock

herramientas_usadas = []
async for msg in query(prompt="...", options=opts):
    if isinstance(msg, AssistantMessage):
        for block in msg.content:
            if isinstance(block, ToolUseBlock):
                herramientas_usadas.append(block.name)
    elif isinstance(msg, ResultMessage):
        print(f"Costo: ${msg.cost_usd:.4f}")
        print(f"Herramientas usadas: {len(herramientas_usadas)}")
        print(f"Distribución: {set(herramientas_usadas)}")

Soluciones:

  1. Agregar max_turns para limitar iteraciones
  2. Usar allowed_tools para restringir herramientas costosas
  3. Detectar loops de herramientas (misma herramienta repetida 10+ veces)

Problema 12: Output de Herramienta Muy Grande

Síntoma: Error de contexto demasiado largo, o el agente se queda sin contexto.

Solución:

opts = ClaudeCodeOptions(
    system_prompt="""
    Cuando leas archivos grandes (>500 líneas), lee solo las secciones relevantes.
    Cuando ejecutes comandos que producen mucho output, usa head/tail/grep para filtrar.
    Ejemplo: en lugar de `cat archivo.log`, usa `tail -n 100 archivo.log | grep ERROR`.
    """,
)

Problema 13: Agente No Lee Archivos Correctamente

Síntoma: El agente dice que no encuentra archivos que existen.

Causa: El cwd incorrecto hace que las rutas relativas no resuelvan.

Solución:

from pathlib import Path

proyecto = Path("/mi/proyecto").resolve()

opts = ClaudeCodeOptions(
    cwd=proyecto,  # SIEMPRE usar path absoluto resuelto
    system_prompt=f"El directorio raíz del proyecto es {proyecto}. Usa rutas relativas a este directorio.",
)

Problema 14: Subagentes Fallan Silenciosamente

Síntoma: Los sub-agentes lanzados con Task fallan pero el agente padre no reporta error.

Solución:

opts = ClaudeCodeOptions(
    system_prompt="""
    Cuando uses la herramienta Task para lanzar sub-agentes:
    1. Siempre verifica el resultado del sub-agente
    2. Si el sub-agente falla, repórtalo explícitamente con el error
    3. No continúes si un paso crítico falló
    """,
)

Problema 15: Tests Flaky de Agentes

Síntoma: Tests que pasan a veces y fallan otras.

Causas y soluciones:

CausaSolución
No-determinismo del LLMUsar system_prompt muy específico y max_turns=1 para tests
Dependencia de redMockear el SDK en tests unitarios
Estado del filesystemLimpiar fixtures en setUp/tearDown
Timeouts variablesUsar asyncio.timeout() con margen generoso
# Test con fixture limpio
import tempfile, asyncio
from pathlib import Path

async def test_crea_archivo():
    with tempfile.TemporaryDirectory() as tmp:
        opts = ClaudeCodeOptions(
            cwd=Path(tmp),
            allowed_tools=["Write"],
            permission_mode="bypassPermissions",
            max_turns=2,
            system_prompt="Crea exactamente el archivo pedido. Nada más.",
        )
        async for msg in query(prompt="Crea test.txt con el contenido 'hola'", options=opts):
            pass
        assert (Path(tmp) / "test.txt").read_text() == "hola"

Problema 16: TypeScript — Type Errors con el SDK

Síntoma: TypeScript no puede inferir tipos en los mensajes del stream.

Solución con type guards:

import { query, AssistantMessage, ResultMessage } from "@anthropic-ai/claude-code-sdk";

function esAssistantMessage(msg: unknown): msg is AssistantMessage {
  return typeof msg === "object" && msg !== null && (msg as any).type === "assistant";
}

function esResultMessage(msg: unknown): msg is ResultMessage {
  return typeof msg === "object" && msg !== null && (msg as any).type === "result";
}

for await (const msg of query({ prompt: "...", options: opts })) {
  if (esAssistantMessage(msg)) {
    // msg es AssistantMessage aquí
  } else if (esResultMessage(msg)) {
    // msg es ResultMessage aquí
  }
}

Problema 17: Docker — Claude CLI No Encontrado

Síntoma: El agente funciona local pero falla en contenedor Docker.

Dockerfile correcto:

FROM node:20-slim

# Instalar Claude Code CLI
RUN npm install -g @anthropic-ai/claude-code

# Instalar Python y dependencias
RUN apt-get update && apt-get install -y python3 python3-pip
RUN pip3 install claude-code-sdk

WORKDIR /app
COPY . .

ENV ANTHROPIC_API_KEY=""

CMD ["python3", "mi_agente.py"]

Verificar en el contenedor:

docker run --rm mi-imagen which claude
docker run --rm mi-imagen claude --version

Problema 18: Hooks No Se Ejecutan

Síntoma: El hook está configurado pero nunca se invoca.

Checklist de debugging:

# 1. Verificar que el archivo settings.json es válido JSON
python3 -m json.tool ~/.claude/settings.json

# 2. Verificar permisos del script del hook
chmod +x /hooks/mi_hook.py

# 3. Verificar que el matcher coincide con el evento
# PreToolUse con matcher "Write" se dispara solo para Write, no para Edit

# 4. Probar el hook manualmente
echo '{"tool_name": "Write", "tool_input": {"file_path": "/tmp/test.txt"}}' | python /hooks/mi_hook.py

# 5. Verificar que el hook imprime JSON válido a stdout

Problema 19: Rate Limit de API (429)

Síntoma: Error: 429 Too Many Requests en stderr del CLI.

Solución — backoff exponencial:

import asyncio
from claude_code_sdk.errors import ProcessError

async def query_con_backoff(prompt: str, max_intentos: int = 5):
    for intento in range(max_intentos):
        try:
            async for msg in query(prompt=prompt, options=opts):
                yield msg
            return
        except ProcessError as e:
            if "429" in str(e.stderr) and intento < max_intentos - 1:
                espera = (2 ** intento) * 10  # 10s, 20s, 40s, 80s...
                print(f"Rate limit. Esperando {espera}s...")
                await asyncio.sleep(espera)
            else:
                raise

Problema 20: Memory Leak en Agentes de Larga Duración

Síntoma: El proceso Python/Node.js crece en memoria con el tiempo.

Causas y soluciones:

# PROBLEMA: acumular todos los mensajes en memoria
todos_los_mensajes = []
async for msg in query(prompt="...", options=opts):
    todos_los_mensajes.append(msg)  # Crece indefinidamente

# SOLUCIÓN: procesar y descartar
async for msg in query(prompt="...", options=opts):
    if isinstance(msg, AssistantMessage):
        procesar_inmediatamente(msg)  # No guardar
    elif isinstance(msg, ResultMessage):
        guardar_solo_metadata(msg.session_id, msg.cost_usd)

# Para agentes de larga duración: usar sesiones cortas
async def procesar_en_lotes(items: list[str]):
    for chunk in chunks(items, size=10):
        # Nueva sesión por lote — no acumula contexto
        async for msg in query(
            prompt=f"Procesa: {chunk}",
            options=ClaudeCodeOptions(max_turns=5),
        ):
            pass
        await asyncio.sleep(1)  # Dar tiempo al GC

7. Decision Trees

¿Qué Modelo Usar?

flowchart TD
    Start[Elegir modelo] --> Velocidad{¿Prioridad?}
    Velocidad -->|Máxima calidad| Calidad[claude-opus-4-5\nMejor razonamiento\nMás costoso]
    Velocidad -->|Balance calidad/velocidad| Balance[claude-sonnet-4-5\nRecomendado para\nla mayoría de casos]
    Velocidad -->|Máxima velocidad y bajo costo| Rapido[claude-haiku-3-5\nTareas simples\nAlto volumen]
    Balance --> Agente{¿Es un agente\ncon herramientas?}
    Agente -->|Sí, muchas herramientas| Opus[Considerar Opus\npara mejor planificación]
    Agente -->|No, solo texto| Sonnet[Sonnet es suficiente]

¿Qué permission_mode Usar?

flowchart TD
    Start[Elegir permission_mode] --> Humano{¿Hay un humano\nviendo la terminal?}
    Humano -->|Sí| Default["default"\nClaude pide confirmación\npara operaciones destructivas]
    Humano -->|No| Automatizado{¿Es entorno\nautomatizado/CI?}
    Automatizado -->|Solo lee archivos| ReadOnly["default" con\nallowed_tools de solo lectura]
    Automatizado -->|Edita archivos conocidos| AcceptEdits["acceptEdits"\nAuto-acepta ediciones]
    Automatizado -->|Sandbox aislado| Bypass["bypassPermissions"\nTodo automático\nSolo en ambientes seguros]
    Automatizado -->|Revisar antes de ejecutar| Plan["plan"\nSolo planifica, no ejecuta]

¿Agente Único o Multi-Agente?

flowchart TD
    Start[¿Agente único o multi-agente?] --> Paralelizable{¿Las tareas son\nparalelizables?}
    Paralelizable -->|Sí| Independientes{¿Son independientes\nentre sí?}
    Independientes -->|Sí| MultiParalelo[Multi-agente paralelo\nasyncio.gather]
    Independientes -->|No| MultiSecuencial[Multi-agente secuencial\ncon paso de contexto]
    Paralelizable -->|No| Grande{¿La tarea es\nmuy grande?}
    Grande -->|Sí| Descomponer[Descomponer en sub-tareas\ncon Task tool]
    Grande -->|No| Unico[Agente único\nes suficiente]

¿Session Resume o Nueva Sesión?

flowchart TD
    Start[¿Reanudar sesión?] --> TieneID{¿Tienes un\nsession_id guardado?}
    TieneID -->|No| Nueva[Nueva sesión]
    TieneID -->|Sí| Reciente{¿Es reciente?\nmenos de 24h}
    Reciente -->|No| Nueva
    Reciente -->|Sí| Continua{¿La nueva tarea\nusa el mismo contexto?}
    Continua -->|Sí| Resume[Usar session_id\npara continuar]
    Continua -->|No| Decision{¿Necesitas el contexto\nde la sesión anterior?}
    Decision -->|Sí| Resume
    Decision -->|No| Nueva

¿MCP In-Process o Externo?

flowchart TD
    Start[¿Cómo integrar herramienta custom?] --> Existe{¿Existe un servidor\nMCP para esto?}
    Existe -->|Sí, en npm/pip| Externo[MCP externo con\nMCPStdioServer]
    Existe -->|No| Complejidad{¿Qué tan compleja\nes la herramienta?}
    Complejidad -->|Simple, 1-2 funciones| Bash["Bash tool + script\nEjemplo: python /tools/mi_tool.py"]
    Complejidad -->|Compleja, necesita estado| InProcess[Implementar servidor\nMCP in-process\ncon SDK de MCP]
    Complejidad -->|Ya existe como API| HTTP[MCP con transport\nSSE o HTTP]

8. Anti-Patrones Rápidos

Anti-Patrón 1: No Limitar max_turns

Problema: El agente puede iterar indefinidamente consumiendo tokens.

Anti-patrón:

opts = ClaudeCodeOptions()  # max_turns=None por defecto

Correcto:

opts = ClaudeCodeOptions(max_turns=20)  # Siempre poner un límite razonable

Anti-Patrón 2: bypassPermissions en Producción con Datos Reales

Problema: El agente puede eliminar archivos, bases de datos, ejecutar comandos destructivos sin confirmación.

Anti-patrón:

opts = ClaudeCodeOptions(permission_mode="bypassPermissions")  # En servidor de producción

Correcto:

# En producción: default o acceptEdits + restricción de herramientas
opts = ClaudeCodeOptions(
    permission_mode="acceptEdits",
    allowed_tools=["Read", "Write"],  # Sin Bash en producción sensible
)

Anti-Patrón 3: Guardar Toda la Historia de Mensajes en Memoria

Problema: En agentes de larga duración, acumular todos los mensajes llena la RAM.

Anti-patrón:

historial = []
async for msg in query(prompt="...", options=opts):
    historial.append(msg)  # Nunca se libera

Correcto: Procesar y descartar cada mensaje al recibirlo.


Anti-Patrón 4: Ignorar ResultMessage

Problema: No sabes si el agente terminó con éxito o con error.

Anti-patrón:

async for msg in query(prompt="...", options=opts):
    if isinstance(msg, AssistantMessage):
        procesar(msg)
# Nunca verificas si hubo error

Correcto: Siempre capturar y verificar ResultMessage.subtype.


Anti-Patrón 5: System Prompt Demasiado Vago

Problema: El agente interpreta la tarea de forma impredecible.

Anti-patrón:

opts = ClaudeCodeOptions(system_prompt="Ayuda con el código.")

Correcto:

opts = ClaudeCodeOptions(
    system_prompt="""
    Eres un agente de análisis de código. Tu tarea es:
    1. Leer los archivos indicados con la herramienta Read
    2. Identificar problemas de seguridad OWASP Top 10
    3. Reportar: archivo, línea, tipo de problema, severidad
    NO modifiques ningún archivo.
    """,
)

Anti-Patrón 6: Usar el SDK sin Manejar Excepciones

Problema: Errores del CLI o de red derrumban la aplicación entera.

Anti-patrón:

async for msg in query(prompt="...", options=opts):
    procesar(msg)

Correcto:

from claude_code_sdk.errors import CLINotFoundError, CLIConnectionError, ProcessError

try:
    async for msg in query(prompt="...", options=opts):
        procesar(msg)
except CLINotFoundError:
    logging.error("Claude Code CLI no instalado")
except CLIConnectionError as e:
    logging.error(f"Error de conexión: {e}")
except ProcessError as e:
    logging.error(f"Error del proceso (exit {e.exit_code}): {e.stderr}")

Anti-Patrón 7: Concatenar Prompts con String Formatting Inseguro

Problema: Inyección de prompt si los datos vienen de usuarios externos.

Anti-patrón:

prompt = f"Analiza el archivo {user_input}"  # user_input puede ser malicioso

Correcto:

import re

def sanitizar_path(path: str) -> str:
    # Solo permitir caracteres seguros en rutas
    if not re.match(r'^[a-zA-Z0-9_\-./]+$', path):
        raise ValueError(f"Ruta no permitida: {path}")
    return path

prompt = f"Analiza el archivo {sanitizar_path(user_input)}"

Anti-Patrón 8: MCP Servers sin Timeout de Inicio

Problema: Si el servidor MCP tarda en iniciar, el agente espera indefinidamente.

Anti-patrón:

opts = ClaudeCodeOptions(mcp_servers={"db": MCPStdioServer(command="npx", args=[...])})

Correcto:

async with asyncio.timeout(30):  # 30s max para que el MCP inicie
    async for msg in query(prompt="...", options=opts):
        pass

Anti-Patrón 9: Lanzar Muchos Agentes Paralelos sin Control

Problema: Superar rate limits de la API o agotar recursos del sistema.

Anti-patrón:

tareas = [analizar(f) for f in archivos_1000]
resultados = await asyncio.gather(*tareas)  # 1000 agentes a la vez

Correcto:

import asyncio

semaforo = asyncio.Semaphore(5)  # Máximo 5 agentes a la vez

async def analizar_limitado(archivo):
    async with semaforo:
        return await analizar(archivo)

tareas = [analizar_limitado(f) for f in archivos_1000]
resultados = await asyncio.gather(*tareas)

Anti-Patrón 10: No Testear con Datos de Producción Reales

Problema: Los agentes se comportan diferente con datos reales vs datos de prueba.

Anti-patrón: Solo testear con archivos ficticios simples.

Correcto:

# Usar fixtures realistas que representen casos edge
FIXTURES = {
    "archivo_grande": "x" * 100_000,  # 100KB
    "unicode": "こんにちは 🎉 Ñoño",
    "binario": b"\x00\x01\x02".decode("latin-1"),
    "rutas_profundas": "a/b/c/d/e/f/g/archivo.txt",
    "nombres_especiales": "archivo con espacios & símbolos!.txt",
}

9. Índice de Variables de Entorno

VariableDescripciónEjemplo
ANTHROPIC_API_KEYAPI key de Anthropic. Requerida.sk-ant-api03-...
ANTHROPIC_BASE_URLURL base de la API (para proxies o enterprise)https://api.empresa.com
CLAUDE_CODE_MAX_OUTPUT_TOKENSLímite de tokens de output por respuesta4096
CLAUDE_CODE_DEFAULT_MODELModelo por defecto cuando no se especifica en opcionesclaude-sonnet-4-5
CLAUDE_CODE_DISABLE_TELEMETRYDeshabilitar telemetría del CLI (1 = deshabilitar)1
CLAUDE_CODE_SETTINGS_PATHPath alternativo al archivo settings.json/etc/claude/settings.json
DEBUGHabilitar logging de debug del CLIclaude:*
NO_COLORDeshabilitar colores en output del CLI1
HTTP_PROXY / HTTPS_PROXYProxy para conexiones HTTP/HTTPShttp://proxy:3128
NODE_EXTRA_CA_CERTSCertificados CA adicionales (para redes corporativas)/etc/ssl/corp-ca.pem
CLAUDE_CODE_HOOKS_DIRDirectorio alternativo para scripts de hooks/opt/hooks

Configurar en Código (Python)

import os

# Configurar antes de usar el SDK
os.environ.setdefault("ANTHROPIC_API_KEY", "sk-ant-...")
os.environ.setdefault("CLAUDE_CODE_DEFAULT_MODEL", "claude-sonnet-4-5")
os.environ.setdefault("CLAUDE_CODE_DISABLE_TELEMETRY", "1")

from claude_code_sdk import query, ClaudeCodeOptions

Configurar en Código (TypeScript)

// Antes de importar el SDK
process.env.ANTHROPIC_API_KEY ??= "sk-ant-...";
process.env.CLAUDE_CODE_DEFAULT_MODEL ??= "claude-sonnet-4-5";

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

10. Tabla de Compatibilidad

SDK Python

claude-code-sdkPythonanthropicEstado
0.0.14+3.11+≥0.50Actual ✓
0.0.10–0.0.133.10+≥0.45Mantenimiento
< 0.0.103.9+≥0.40Obsoleto

SDK TypeScript/JavaScript

@anthropic-ai/claude-code-sdkNode.jsTypeScriptEstado
0.0.14+18+5.0+Actual ✓
0.0.10–0.0.1316+4.9+Mantenimiento
< 0.0.1014+4.7+Obsoleto

Claude Code CLI

CLI VersionSDK Python mínimoSDK TS mínimoNotas
1.x+0.0.140.0.14Versión actual. Recomendada.
0.9.x0.0.100.0.10Funcional con limitaciones
0.8.x0.0.50.0.5Sin soporte para hooks avanzados
< 0.8No compatibleNo compatible

Verificar Compatibilidad

# Verificar versiones instaladas
claude --version
python3 -c "import claude_code_sdk; print(claude_code_sdk.__version__)"
node -e "const s = require('@anthropic-ai/claude-code-sdk'); console.log(s.version ?? 'ver no exportada')"

# Python: verificar dependencias
pip show claude-code-sdk

# Node: verificar
npm list @anthropic-ai/claude-code-sdk

Plataformas Soportadas

PlataformaPython SDKTypeScript SDKClaude CLI
Linux x86_64
Linux ARM64
macOS x86_64
macOS ARM64 (M1/M2)
Windows x86_64✓ (WSL recomendado)
Docker Alpine✓ (con Node.js)

Volver al inicio: Índice del Tutorial