← Volver al Blog

Claude Agent SDK: tu primer agente en 15 minutos

La primera sorpresa al abrir la doc del Claude Agent SDK es que el paquete de Python actúa como envoltura: por detrás habla con el proceso de Claude Code. Lo dice la referencia oficial: el campo cli_path existe porque el SDK lanza al CLI de Claude Code como subproceso. Si no está instalado, el query() falla antes del primer token.

Ese detalle resume el tutorial entero. Vas a instalar dos piezas separadas y aprender para qué sirve cada una. La buena noticia es que, una vez puestas, pasas de pip install a tu primer agente autónomo en Python en unos quince minutos.

Esta guía sigue la documentación oficial sin inventar banderas ni valores. Cada bloque de código viene del README o de la página de hooks publicada por Anthropic en code.claude.com. Al final, señalo los dos errores que verás publicados en tutoriales de terceros y que no existen en la API real.

Minutos 1-2: Instalación y variable de entorno

El SDK se publica en PyPI como claude-agent-sdk. Antes de instalarlo, necesitas el CLI, que es un paquete de npm independiente.

# El CLI (necesario como subproceso del SDK)
npm install -g @anthropic-ai/claude-code

# El SDK de Python, dentro de un venv
python3 -m venv .venv && source .venv/bin/activate
pip install claude-agent-sdk

Si trabajas en una Debian, Ubuntu o Homebrew Python reciente, pip install contra el Python del sistema falla con error: externally-managed-environment. La doc recomienda explícitamente un virtualenv; es lo que acabas de crear con python3 -m venv.

La autenticación va por una sola variable: ANTHROPIC_API_KEY. Exportarla en la shell basta para la prueba local.

export ANTHROPIC_API_KEY="sk-ant-..."

Hay otras variables (API_TIMEOUT_MS, CLAUDE_CODE_MAX_RETRIES) que puedes pasar luego por ClaudeAgentOptions.env, pero para el primer agente no hacen falta.

Minutos 3-7: El agente mínimo

El quickstart oficial cabe en diez líneas. Lo copio tal cual de la referencia:

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions


async def main():
    options = ClaudeAgentOptions(
        system_prompt="You are an expert Python developer",
        permission_mode="acceptEdits",
    )

    async for message in query(prompt="Create a Python web server", options=options):
        print(message)


asyncio.run(main())

Guárdalo como primer_agente.py y ejecútalo con python primer_agente.py. Deberías ver un stream de mensajes: primero un SystemMessage con el inicio de sesión, luego AssistantMessage con la respuesta de Claude, los UserMessage que transportan resultados de herramientas, y al final un ResultMessage con el desenlace.

Las piezas que importan de este bloque:

  • query() es la función de entrada. Devuelve un iterable asíncrono: cada iteración es un mensaje del agente. Si vienes del mundo síncrono, el patrón async for es la parte que más se siente nueva.
  • system_prompt acepta tres formas: un string suelto (lo que ves arriba), un diccionario {"type": "preset", "preset": "claude_code"} para heredar el prompt que usa Claude Code, o un objeto que carga el texto desde un archivo. La string directa es la más corta; el preset es lo que quieres si tu agente hace trabajo de ingeniería.
  • permission_mode="acceptEdits" evita que el agente pare a pedirte confirmación cada vez que intenta editar un archivo. Es el modo que casi siempre quieres en un script de tutorial. Más abajo paso por los seis valores posibles, porque la elección cambia cómo se comporta tu agente.

Si el proceso termina sin imprimir nada, el culpable casi siempre es la ANTHROPIC_API_KEY. El segundo culpable es que el CLI de Claude Code no está en el PATH; cuando dudes, pasa cli_path="/ruta/al/claude" dentro de ClaudeAgentOptions.

Minutos 8-11: Los seis valores de permission_mode

Es el punto donde más tutoriales de terceros tropiezan, así que conviene ir despacio. La doc oficial enumera exactamente seis valores para permission_mode, y cada uno tiene un significado específico.

ValorQué hace
defaultEl comportamiento estándar: prompts de permiso cuando corresponden
acceptEditsAuto-aprueba ediciones de archivo. Otras acciones siguen bajo prompt
planClaude explora y produce un plan, sin ejecutar cambios
dontAskNiega todo lo que no esté pre-aprobado en vez de preguntar
bypassPermissionsSalta los controles de permiso. Reglas de ask explícitas aún preguntan
autoUn clasificador del modelo revisa acciones como shell y red

Dos puntos que vas a querer tener presentes antes de elegir:

  1. plan no pide aprobación paso a paso. Es el modo exploratorio: Claude recorre el repositorio, piensa y escribe un plan en texto, sin tocar archivos. Si lo que buscas es “aprobar cada acción”, el modo adecuado es default, porque los prompts de permiso siguen el flujo normal.
  2. “Aceptar todo” no es un string. No existe "acceptAll", ni "yes", ni "all". Si lo que quieres es que un agente desatendido llegue hasta el final sin frenos, el valor es bypassPermissions. El tipo PermissionMode es un Literal de esos seis strings, así que un typo pasa la construcción de ClaudeAgentOptions sin ruido y revienta más abajo, cuando el CLI recibe un modo que no reconoce.

Un ejemplo útil: para una sesión no interactiva en CI, donde el agente parchea un lint sin que nadie esté frente al teclado, bypassPermissions es lo que el runtime necesita. Para una sesión local donde prefieres revisar antes de cada edición, default es el valor que pide confirmaciones. El punto medio, acceptEdits, deja pasar las ediciones y detiene cosas más delicadas como Bash.

Minutos 12-15: Un hook PreToolUse que bloquea .env

El último tramo mete una regla determinista: antes de ejecutar cualquier herramienta, un hook revisa qué va a hacer y puede bloquearlo. Copio el ejemplo oficial casi textual, porque la forma exacta del callback es donde más guías de terceros fallan.

import asyncio
from claude_agent_sdk import (
    AssistantMessage,
    ClaudeSDKClient,
    ClaudeAgentOptions,
    HookMatcher,
    ResultMessage,
)


async def protect_env_files(input_data, tool_use_id, context):
    file_path = input_data["tool_input"].get("file_path", "")
    file_name = file_path.split("/")[-1]

    if file_name.lower() == ".env":
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": "Cannot modify .env files",
            }
        }

    return {}


async def main():
    options = ClaudeAgentOptions(
        hooks={
            "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
        }
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Create a .env file with local database config")
        async for message in client.receive_response():
            if isinstance(message, (AssistantMessage, ResultMessage)):
                print(message)


asyncio.run(main())

Cuatro detalles de forma que son fáciles de equivocar y que el runtime castiga:

  • Las claves del dict hooks son strings. Se escribe "PreToolUse", no un enum importado. HookMatcher y los nombres de evento son cosas distintas: el primero es la clase que envuelve matcher + callbacks; el segundo es la cadena literal que elige dónde se registra.
  • La firma del callback tiene tres argumentos. async def nombre(input_data, tool_use_id, context). Es asíncrono y recibe los tres aunque no uses los dos últimos. En Python, context queda reservado para uso futuro; puedes ignorarlo pero tiene que estar en la firma.
  • HookMatcher recibe matcher y hooks. Varios tutoriales mencionan pattern y handler, que no existen en la API real. El matcher es un string que puede ser el nombre exacto de una herramienta ("Bash"), un | de varias ("Write|Edit") o una regex ("^mcp__"). Si lo omites, el callback dispara para toda invocación del evento.
  • El retorno es un dict plano. {} deja pasar la acción sin cambios. Para denegar, se devuelve {"hookSpecificOutput": {"hookEventName": ..., "permissionDecision": "deny", "permissionDecisionReason": ...}}. Los valores válidos de permissionDecision son "allow", "deny", "ask" y "defer": no hay otros.

Cuando ejecutes el snippet, verás que el agente intenta un Write sobre .env, el hook lo intercepta, y la salida reporta la razón Cannot modify .env files. Es la diferencia entre escribirle al modelo “no toques .env” en el system_prompt (una petición que puede olvidar) y sellar la puerta en el runtime (una regla que no depende del contexto).

Si prefieres leer el ejemplo en la forma larga con todas sus variantes (incluyendo updatedInput, permissionDecision: "allow" para auto-aprobar, y el callback que modifica el file_path sobre la marcha), la página de hooks del Agent SDK los enumera uno por uno.

Qué no hace este tutorial

Para que el desenlace no te pille por sorpresa, tres cosas que quedan fuera del cuarto de hora:

  • No cubre ClaudeSDKClient en profundidad. query() es el camino corto para un one-shot; ClaudeSDKClient es la forma con contexto que mantiene estado entre mensajes. El ejemplo de hook arriba ya lo usa, pero la API completa (tool overrides, receive_response, cierre de sesión) necesita su propia guía.
  • No toca MCP. El SDK acepta mcp_servers en las opciones y el matcher de hooks puede filtrar por ^mcp__, pero conectar un servidor MCP suma dos decisiones (qué servidor, qué transporte) que no se resuelven en quince minutos.
  • No mide costo de tokens. Si te importa eso, el comparativo ChatGPT Codex vs Claude Code: 3.4x Cost Gap tiene los números por PR de un periodo real; aquí el objetivo era arrancar el primer loop.

El mapa mental que te llevas

Tres piezas, en una sola frase: pip install claude-agent-sdk + CLI de Claude Code + ANTHROPIC_API_KEY es la base; query() o ClaudeSDKClient es tu puerta de entrada; permission_mode + hooks es donde defines qué puede hacer el agente sin preguntarte.

Antes de pasar a algo más grande, conviene fijar estos puntos:

  • permission_mode tiene seis valores con nombres exactos. Teclearlos mal rompe la construcción de opciones.
  • Los hooks se aplican en el runtime, antes de que el modelo decida nada. Son la contraparte determinista de un system_prompt.
  • La doc oficial en code.claude.com es la única fuente que se actualiza con la versión; los tutoriales de terceros, incluido éste, envejecen.

El siguiente paso natural es reemplazar el print(message) del loop por algo que acumule AssistantMessage y escriba el resultado en un archivo. Desde ahí, agregar un segundo hook (por ejemplo, un PostToolUse que logue toda llamada a Bash) y ya tienes un auditor vigilando a tu agente, todo en menos de cien líneas de Python.

Referencias

Artículos relacionados

Practical Claude Code Libro relacionado Practical Claude Code Claude Code desde cero hasta producción — CLAUDE.md, Plan Mode y workflows de equipo, desde un año de uso real Ver la página del libro →