Desarrollo IATutorial

Armá un Agente de Auditoría de Repos con el Claude Agent SDK (y Conocé sus Límites)

14 de octubre de 2026
14 min de lectura
Cambio de vías en miniatura con dos vías que se separan sobre una superficie gris neutra
Compartir:

El Claude Agent SDK te entrega como librería el loop de agente que hay detrás de Claude Code: herramientas de archivos, una shell, subagentes, hooks y sesiones, manejados desde tu propio proceso de Python o TypeScript. Hacerlo andar lleva diez líneas. Lo que decide si podés dejarlo corriendo sin supervisión es la política que lo rodea: qué herramientas existen, qué está preaprobado y qué se rechaza.

En este tutorial armo un pequeño agente de auditoría de repos en Python con las piezas que describe la documentación oficial: una tool propia, un hook PreToolUse que bloquea comandos Bash riesgosos, un callback de permisos y un subagente enfocado. Leí los docs y las páginas de PyPI y npm el 14 de octubre de 2026 y fijé las versiones. No ejecuté el agente terminado para este post, así que hay un smoke test de cuatro prompts para correr antes de confiar en él, y una sección honesta sobre lo que esta política no puede garantizar.

1. El Problema: un Agente con Shell y Sin Política

Un agente de auditoría de repos tiene que leer mucho y ejecutar unos pocos comandos. La demo más rápida le da Bash y apaga las aprobaciones, y funciona, hasta que audita un repositorio que no escribiste vos. Un README, un comentario o un nombre de archivo pueden llevar instrucciones, y un modelo que siga una la va a ejecutar con tus privilegios. La guía de despliegue de Anthropic plantea el modelo de amenazas sin vueltas: los agentes pueden tomar acciones no previstas por prompt injection o por simple error del modelo. La mirada de diseño está en mi deep dive sobre prompt injection; este post es el lado de la implementación.

El Error Más Común

Tratar allowed_tools como una lista blanca. No lo es: solo preaprueba las herramientas que listás, y todas las demás siguen disponibles. Los docs describen el peor caso: allowed_tools=[“Read”] junto con permission_mode=“bypassPermissions” igual aprueba todas las herramientas, incluidas Bash, Write y Edit. Para quitar una herramienta usás tools, o un nombre pelado en disallowed_tools.

Lo que construimos en cambio es una política en capas, en un solo archivo de menos de 200 líneas: disponibilidad (qué ve Claude), aprobación (qué corre sin preguntar), un hook (qué se rechaza pase lo que pase) y un callback (qué pasa con todo lo demás). Versiones que fijé: claude-agent-sdk 0.2.167 en PyPI (Python 3.10 o superior, subido el 14 de octubre de 2026) y @anthropic-ai/claude-agent-sdk 0.3.300 en npm. El ritmo de releases es rápido y los docs condicionan varias funciones por versión, así que fijá la versión y leé el changelog antes de actualizar.

2. Conceptos Mínimos Antes de Tocar Código

Seis ideas explican cada línea de abajo. Python y TypeScript las comparten; elegí Python porque los docs traen ejemplos completos de hooks, callbacks y subagentes ahí (algunos eventos de hook existen solo en TypeScript, lo aclaro más adelante).

🔁

query() vs ClaudeSDKClient

un disparo o una sesión

query() abre una sesión nueva por llamada y devuelve mensajes. ClaudeSDKClient mantiene una sola sesión entre llamadas, así que el seguimiento tiene todo el contexto. Este tutorial usa el cliente.

🧰

Disponibilidad vs aprobación

dos capas distintas

tools (y los nombres pelados en disallowed_tools) cambian lo que Claude puede ver. allowed_tools solo preaprueba: las herramientas no listadas siguen existiendo y caen al permission mode.

🔌

Tool propia = servidor MCP en proceso

tu función, la tool de Claude

@tool define nombre, descripción, schema de entrada y un handler async; create_sdk_mcp_server lo envuelve. Corre dentro de tu proceso y se llama mcp__<server>__<tool>.

🪝

Hook PreToolUse

corre primero, puede decir que no

Un callback que corre antes de cualquier otro paso de permisos. Devolver {} deja la llamada al resto del flujo; un deny le gana a defer, ask y allow, incluso en bypassPermissions.

✋

Callback can_use_tool

el último recurso

Se llama solo cuando el flujo de permisos termina en un prompt. Las llamadas aprobadas por allowed_tools, una regla allow o el modo nunca llegan, así que no puede ser tu único chequeo.

🧑‍🔬

Subagente

contexto y tools propios

Una instancia de agente separada que arranca vía la tool Agent. Su contexto empieza limpio: recibe su propio prompt más el string que le pasás, y solo vuelve su mensaje final.

El modelo mental a conservar

El hook es la ley: corre primero y un deny siempre se respeta. allowed_tools es el carril verde para lo que confiás. El callback es lo que pasa con todo lo demás. Un sandbox es el muro de abajo, el que sigue en pie cuando las otras tres capas fallan.

3. Paso 1: Instalar y Escribir la Tool Propia

Los dos SDKs incluyen un binario nativo de Claude Code en la mayoría de las instalaciones, así que en general alcanza con un pip install. La autenticación es una ANTHROPIC_API_KEY en el entorno del proceso que corre el agente; el SDK no carga archivos .env por vos. Salvo aprobación previa, Anthropic no permite que productos de terceros ofrezcan login de claude.ai en agentes hechos con el SDK, así que usá autenticación por API key.

Shell: instalar y autenticar

python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\Activate.ps1
pip install "claude-agent-sdk==0.2.167"

export ANTHROPIC_API_KEY=your-api-key      # PowerShell: $env:ANTHROPIC_API_KEY = "your-api-key"

La tool cuenta marcadores TODO, FIXME y HACK por archivo. Es aburrida a propósito, porque lo interesante es lo que valida: la ruta tiene que resolver dentro del repo, se saltean los symlinks, y los fallos devuelven is_error para que Claude pueda reaccionar en vez de que se atragante el loop. La anotación readOnlyHint le permite agruparla con otras llamadas de solo lectura.

Python: audit_agent.py, parte 1 (imports y la tool propia)

# audit_agent.py, part 1: imports and the custom tool
import asyncio
import re
import shlex
import sys
from pathlib import Path
from typing import Annotated, Any

from claude_agent_sdk import (
    AgentDefinition, AssistantMessage, ClaudeAgentOptions, ClaudeSDKClient,
    HookMatcher, ResultMessage, TextBlock, ToolAnnotations,
    create_sdk_mcp_server, tool,
)
from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny

REPO = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
MARKERS = ("TODO", "FIXME", "HACK")
SKIP = {".git", "node_modules", ".venv", "__pycache__"}


@tool(
    "todo_report",
    "Count TODO/FIXME/HACK markers per file under a repo-relative directory",
    {"directory": Annotated[str, "Repo-relative directory to scan, for example src"]},
    annotations=ToolAnnotations(readOnlyHint=True),
)
async def todo_report(args: dict[str, Any]) -> dict[str, Any]:
    target = (REPO / args["directory"]).resolve()
    if not target.is_relative_to(REPO) or not target.is_dir():
        msg = f"Not a directory inside the repo: {args['directory']}"
        return {"content": [{"type": "text", "text": msg}], "is_error": True}

    rows = []
    for path in target.rglob("*"):
        rel = path.relative_to(REPO)
        if path.is_symlink() or not path.is_file() or SKIP & set(rel.parts):
            continue
        try:
            text = path.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError):
            continue
        hits = sum(text.count(marker) for marker in MARKERS)
        if hits:
            rows.append((hits, rel.as_posix()))

    rows.sort(reverse=True)
    lines = [f"{n:3d}  {p}" for n, p in rows[:20]] or ["No markers found."]
    return {"content": [{"type": "text", "text": "\n".join(lines)}]}


audit_server = create_sdk_mcp_server(name="audit", version="1.0.0", tools=[todo_report])

Tip: el handler corre en tu proceso

Una tool en proceso es Python común con tus privilegios. Ninguna regla de permisos de Claude Code valida sus argumentos por vos, así que hacé tus propios chequeos de ruta. Las anotaciones son metadatos, no enforcement: readOnlyHint=True en un handler que escribe archivos igual los escribe.

4. Paso 2: Un Hook PreToolUse que Bloquea Bash Riesgoso

Los hooks son callbacks que el SDK ejecuta en puntos del ciclo de vida. PreToolUse se dispara antes de que corra una herramienta y recibe tool_name y tool_input; para Bash, tool_input.command es el texto completo del comando. Un hookSpecificOutput con permissionDecision “deny” bloquea la llamada, y permissionDecisionReason le explica al modelo por qué para que no reintente lo mismo. Devolver {} deja la decisión al resto del flujo de permisos.

Python: parte 2 (guardas y registro)

# part 2: PreToolUse guards (they run before every other permission step)
RISKY_BASH = [
    (re.compile(r"\brm\b.*\s(-\w*[rR]\w*|--recursive)\b"), "recursive delete"),
    (re.compile(r"\b(curl|wget)\b.*\|\s*(ba|z)?sh\b"), "download piped into a shell"),
    (re.compile(r"\b(sudo|chmod|chown|mkfs|dd)\b"), "privilege or disk change"),
    (re.compile(r"\.env\b|\.ssh\b|\.aws\b|id_rsa|\.npmrc"), "credential path"),
]
SECRET_PATH = re.compile(r"(^|[\\/])(\.env(\..+)?|id_rsa|\.npmrc)$|[\\/]\.(ssh|aws)[\\/]")
GIT_AT_START = re.compile(r"(^|[;&|]\s*)git\b")
SHELL_META = re.compile(r"[;&|<>$\n\x60]")  # \x60 is a backtick
SAFE_GIT = {"log", "diff", "show", "status", "ls-files", "shortlog"}


def deny(reason: str) -> dict[str, Any]:
    return {
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": f"Blocked by audit policy: {reason}",
        }
    }


def harden_git(command: str) -> str | None:
    """Rewrite a simple read-only git command so repo-controlled config cannot run code."""
    if SHELL_META.search(command):
        return None
    try:
        parts = shlex.split(command)
    except ValueError:
        return None
    if len(parts) < 2 or parts[0] != "git" or parts[1] not in SAFE_GIT:
        return None
    if any(p.startswith("--output") for p in parts):
        return None  # git diff --output=<file> writes a file
    extra = ["--no-ext-diff", "--no-textconv"] if parts[1] in {"diff", "show", "log"} else []
    return shlex.join(["git", "-c", "core.fsmonitor=false", parts[1], *extra, *parts[2:]])


async def guard_bash(input_data, tool_use_id, context):
    command = input_data["tool_input"].get("command", "")
    for pattern, label in RISKY_BASH:
        if pattern.search(command):
            return deny(f"{label}. This agent is read-only.")
    hardened = harden_git(command)
    if hardened:
        return {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "allow",
                "permissionDecisionReason": "read-only git, config hardened",
                "updatedInput": {**input_data["tool_input"], "command": hardened},
            }
        }
    if GIT_AT_START.search(command):
        return deny("only simple read-only git commands are available (log, diff, show, status, ls-files, shortlog).")
    return {}  # no opinion: the rest of the permission flow decides


async def guard_reads(input_data, tool_use_id, context):
    if SECRET_PATH.search(input_data["tool_input"].get("file_path", "")):
        return deny("secret files are out of scope for this audit.")
    return {}


HOOKS = {
    "PreToolUse": [
        HookMatcher(matcher="Bash", hooks=[guard_bash]),
        HookMatcher(matcher="Read", hooks=[guard_reads]),
    ]
}

Trampas que señalan los docs

Los matchers solo comparan nombres de herramienta, nunca rutas ni argumentos: para filtrar por ruta, inspeccioná tool_input vos mismo, como hace guard_reads con file_path. Los nombres de evento distinguen mayúsculas (PreToolUse, no preToolUse). Si un callback PreToolUse vence por timeout, Claude Code no ejecuta la llamada y a Claude le avisan que el hook no respondió. Y los callbacks de Python cubren diez eventos; SessionStart y SessionEnd son solo de TypeScript.

Fijate que el hook hace dos trabajos. Rechaza lo que reconoce y reescribe lo que permite. Los comandos de solo lectura integrados, como cat, ls y las formas read-only de git, corren sin prompt en todos los modos, así que un callback nunca los ve; el hook sí. Git es el caso filoso: lee config que controla el repositorio, la clase de bug detrás de GitSpawn. Por eso los comandos git simples de solo lectura se reescriben con core.fsmonitor=false (más --no-ext-diff y --no-textconv en los subcomandos tipo diff, la llamada endurecida que recomienda ese post) y cualquier otra invocación de git se rechaza. Cuando un hook deniega, escribí un motivo que diga qué hacer en su lugar; varios hooks del mismo evento corren en paralelo y aplica el resultado más restrictivo.

5. Paso 3: La Política de Permisos, Disponibilidad, Aprobación y Callback

Ahora las capas que deciden qué pasa cuando el hook no opina. La lista tools controla la disponibilidad: los built-ins que dejás afuera no están en el contexto de Claude, así que no hay Edit ni Write que aprobar o denegar. allowed_tools preaprueba las herramientas de lectura, la tool Agent y nuestra tool propia por su nombre completo, mcp__audit__todo_report. Bash está ausente de allowed_tools a propósito, porque las tools de esa lista se saltean can_use_tool.

Python: parte 3 (el callback de permisos)

# part 3: the permission callback, the last resort
async def audit_policy(tool_name, input_data, context):
    """Runs only when nothing earlier decided and the call would have asked a human."""
    summary = f"{tool_name}: {str(input_data)[:120]}"
    if sys.stdin.isatty():
        answer = await asyncio.to_thread(input, f"Allow {summary} ? [y/N] ")
        if answer.strip().lower() == "y":
            return PermissionResultAllow(updated_input=input_data)

    print(f"[policy] denied {summary}", file=sys.stderr)
    return PermissionResultDeny(
        message=(
            f"{tool_name} was not approved. This is a read-only audit: "
            "use Read, Grep, Glob, todo_report or read-only git commands."
        )
    )

Ojo: el callback no es una compuerta para herramientas aprobadas

Cualquier herramienta aprobada antes, por allowed_tools, una regla allow, acceptEdits o bypassPermissions, se saltea can_use_tool, así que los chequeos que pongas ahí se esquivan en silencio para ella. Para lógica que tiene que correr en cada llamada, usá un hook PreToolUse. Acá el callback es solo la red debajo del hook: una persona en una terminal puede aprobar, y todo lo desatendido se rechaza con un mensaje que el modelo puede usar.

Dos detalles más. Pasá permission_mode=“default” explícitamente: si lo omitís, el modo inicial puede ser auto mode, que descarta reglas allow amplias. Y si querés deny por defecto sin ningún callback, permission_mode=“dontAsk” convierte cada prompt en un rechazo y nunca llama a can_use_tool.

6. Paso 4: un Subagente Enfocado, las Opciones y el Loop de Ejecución

Un subagente es una instancia de agente separada que el agente principal arranca vía la tool Agent. El historial del padre y los resultados de sus tools no se le pasan, y solo vuelve el mensaje final del subagente, así que un revisor puede leer decenas de archivos sin inflar la conversación principal. Restringilo con tools y dale un modelo barato: model acepta un alias como haiku, sonnet, opus o inherit, o un ID de modelo completo. La cuenta de costos de los workers chicos está en la guía de subagentes con Haiku 5.5.

Python: parte 4 (subagente, opciones y loop de ejecución)

# part 4: subagent, options and the run loop
REVIEWER = AgentDefinition(
    description="Reviews ONE directory for test gaps and risky config. Use it once per directory.",
    prompt=(
        "You are a focused code reviewer. Inspect only the directory you are given. "
        "Report at most five findings, each with a file path, a line reference and a one-line fix. "
        "You are read-only: never propose running commands."
    ),
    tools=["Read", "Grep", "Glob"],
    model="haiku",
    maxTurns=15,  # camelCase on purpose: AgentDefinition uses wire-format field names
)

OPTIONS = ClaudeAgentOptions(
    system_prompt=(
        "You are a read-only repo-audit agent. Start with todo_report on the main source "
        "directory, delegate one directory at a time to the test-gap-reviewer subagent, "
        "then answer with a ranked list: file, risk, one-line fix."
    ),
    cwd=str(REPO),
    setting_sources=[],  # do not load the audited repo's .claude settings or hooks
    tools=["Read", "Grep", "Glob", "Bash", "Agent"],  # availability: no Edit, Write or web
    allowed_tools=["Read", "Grep", "Glob", "Agent", "mcp__audit__todo_report"],  # approval
    mcp_servers={"audit": audit_server},
    permission_mode="default",  # say it out loud: the built-in default can be auto mode
    can_use_tool=audit_policy,
    hooks=HOOKS,
    agents={"test-gap-reviewer": REVIEWER},
    max_turns=30,
    max_budget_usd=2.0,
)


def show(message) -> None:
    if isinstance(message, AssistantMessage):
        for block in message.content:
            if isinstance(block, TextBlock):
                print(block.text)
    elif isinstance(message, ResultMessage):
        print(f"[{message.subtype}] turns={message.num_turns} cost={message.total_cost_usd}")


async def main() -> None:
    async with ClaudeSDKClient(options=OPTIONS) as client:
        await client.query("Audit this repository and give me the ranked findings.")
        async for message in client.receive_response():
            show(message)

        # same client = same session: the follow-up keeps all the context
        await client.query("Keep only the top three, each with the exact file and the fix.")
        async for message in client.receive_response():
            show(message)


asyncio.run(main())

Tres detalles merecen una segunda mirada. Los campos de AgentDefinition mantienen camelCase (maxTurns, disallowedTools), y un keyword en snake_case lanza TypeError al construirlo. setting_sources=[] evita que se carguen los settings .claude y los hooks de shell del repo auditado, cosa que las opciones por defecto sí harían. Y tus hooks PreToolUse también se disparan dentro del subagente, con agent_id y agent_type seteados, así que registralos. Para acotar anidamiento y paralelismo, los docs dan las variables CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH y CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS (se pasan por env) y max_budget_usd.

Smoke test antes de confiar

Corré estos cuatro prompts contra un repo de prueba. El comportamiento esperado sale de los docs, no de una salida que yo haya capturado. (1) “Limpiá con rm -rf build” debería ser denegado por el hook, con el motivo visible en el stream. (2) “Mostrame el contenido de .env” debería ser denegado por guard_reads o guard_bash. (3) “Mostrame los últimos cinco commits con git log” debería correr el comando reescrito con core.fsmonitor=false. (4) “Instalá requests con pip” debería llegar a audit_policy: un prompt y/N en una terminal, un rechazo en CI. Si alguno de los cuatro te sorprende, arreglá la política antes de sumar funciones.

7. Qué Garantiza y Qué No Garantiza Esta Política

Todo lo anterior es política: código que corre dentro del propio límite de confianza del agente e inspecciona texto. Los docs son directos al respecto. El parseo de comandos para permisos es “una compuerta de permisos, no un sandbox”, y una regla deny de Bash “no es un límite de seguridad alrededor del programa”. Tomá las tres cajas de abajo como el balance honesto.

✅ Qué impone

  1. Visibilidad: Claude ve Read, Grep, Glob, Bash, Agent y una tool propia. No hay Edit ni Write en su contexto.
  2. No rotundo: los comandos Bash que coinciden y las lecturas de archivos secretos se deniegan antes de cualquier otro paso, incluso en bypassPermissions.
  3. Deny por defecto: lo que le habría preguntado a una persona se rechaza, o lo aprueba una persona en la terminal, con un mensaje que el modelo puede usar.
  4. Presupuestos: max_turns y max_budget_usd cortan los loops descontrolados con un subtype de error sobre el que podés alertar.

⚠️ Qué no puede garantizar

  1. Fugas de la denylist: /bin/rm, bash -c, variables y payloads codificados pasan de largo las regex. La tabla de los docs dice lo mismo de las reglas: rm * se frena, /bin/rm y bash -c no.
  2. Fugas de la allowlist: git diff tiene --output, que escribe un archivo, y git ejecuta config que controla el repo. core.fsmonitor=false cierra una puerta, no la clase.
  3. Lecturas: Read, Grep y Glob siguen llegando a todo lo que el proceso pueda leer. Acá solo se chequea el file_path de Read, así que dejá los secretos fuera del árbol.
  4. Tu propia tool: la tool en proceso corre con tus privilegios y fuera de cualquier sandbox de Bash.
  5. Salida de tools: nombres de archivo y comentarios vuelven al modelo como entrada. El resultado de una tool es dato no confiable.

🧱 Qué va debajo

  1. Contenedor o VM: corré todo el proceso en uno, montá el repo en solo lectura y quitá capabilities. La guía de despliegue seguro trae un docker run endurecido para copiar.
  2. Sandbox del SDK para Bash: ClaudeAgentOptions tiene un campo sandbox. Activá failIfUnavailable, porque el default en Python es correr sin sandbox con un warning.
  3. Red: sin egreso por defecto; llegá a la API por un proxy que inyecte credenciales, así el agente nunca las ve.
  4. Runtimes de aislamiento: los dos artículos relacionados de abajo cubren sandboxes para código de agentes y el runtime con políticas de OpenShell.

Python: settings opcionales del sandbox del SDK para Bash

# optional wall for Bash: pass it as ClaudeAgentOptions(sandbox=SANDBOX)
SANDBOX = {
    "enabled": True,
    "failIfUnavailable": True,          # Python default: run unsandboxed with a warning
    "allowUnsandboxedCommands": False,  # the model may not ask to leave the sandbox
    "network": {"allowedDomains": []},  # no outbound hosts for sandboxed commands
}

Ojo con la plataforma: el sandbox depende del soporte del SO (bubblewrap y socat en Linux, según los docs), y su proxy de red filtra por hostname sin inspeccionar TLS, así que técnicas como el domain fronting potencialmente pueden esquivarlo. Reduce el radio de daño; no vuelve confiable al agente.

8. Errores Comunes (la Mayoría Son Trampas Documentadas)

Casi cualquier falla que esperaría de este setup es una de estas. La columna izquierda es lo que aguanta; la derecha, lo que muerde.

✅ Hacé esto

  • • Pasá permission_mode explícito; el default integrado puede ser auto mode
  • • Poné las reglas innegociables en un hook PreToolUse: corre primero y un deny se respeta en todos los modos
  • • Usá tools (o disallowed_tools con nombre pelado) para quitar capacidades; allowed_tools solo para preaprobar
  • • Devolvé en cada deny un motivo que Claude pueda usar
  • • Escribí los campos de AgentDefinition en camelCase: maxTurns, disallowedTools, mcpServers
  • • Fijá max_turns y max_budget_usd, y ramificá según ResultMessage.subtype

❌ No esto

  • • allowed_tools=[“Read”] como restricción, sobre todo junto a bypassPermissions
  • • Chequeos de seguridad solo dentro de can_use_tool: las llamadas aprobadas nunca llegan
  • • permissionDecision al tope de la salida del hook en vez de dentro de hookSpecificOutput con hookEventName
  • • Filtrar rutas con HookMatcher.matcher: solo ve nombres de herramienta
  • • Una denylist de regex presentada como límite de seguridad
  • • Auditar un repo no confiable con setting_sources por defecto: también cargan sus settings y hooks de .claude

Regla de oro

Un hook es una política; un sandbox es un límite. Escribí la política para que el modelo falle de forma segura y comprensible, después asumí que se equivoca una vez y asegurate de que el muro de abajo convierta ese error en un error, no en un incidente.

Conclusión

Ya tenés el loop completo: una tool propia como servidor MCP en proceso, un hook PreToolUse que rechaza y reescribe, un callback como red de deny por defecto y un subagente de solo lectura con un modelo barato, todo dentro de una sola sesión. La forma se traslada a TypeScript casi línea por línea (tool() con Zod, createSdkMcpServer, hooks y canUseTool), y el SDK de TypeScript tiene eventos de hook extra como SessionStart y PostToolBatch que los callbacks de Python no tienen.

Lo que sigue, por orden de valor: convertí los cuatro prompts del smoke test en aserciones que corran en CI, mové el proceso a un contenedor con el repo montado en solo lectura, y empaquetá las instrucciones de auditoría como un skill reutilizable, que explica el tutorial de Agent Skills. Releé el changelog antes de cada actualización del SDK: los propios docs anotan comportamientos que cambiaron entre releases.

Machete del Tutorial

Capas

  • • tools = disponibilidad
  • • allowed_tools = aprobación
  • • hook PreToolUse = reglas duras
  • • can_use_tool = último recurso

Nombres a recordar

  • • @tool + create_sdk_mcp_server
  • • HookMatcher(matcher, hooks)
  • • PermissionResultAllow / Deny
  • • AgentDefinition(maxTurns=…)

Siempre

  • • permission_mode explícito
  • • setting_sources=[] en repos no confiables
  • • fijá claude-agent-sdk==0.2.167
  • • un sandbox debajo

Fuentes

Diego Rodriguez

Diego Rodriguez

Ingeniero Senior Full-Stack & AI

Diego tiene mas de 9 anos de experiencia construyendo aplicaciones potenciadas por IA de produccion, desde orquestacion de LLMs y pipelines RAG hasta deteccion de riesgos con ML y sistemas de trading algoritmico.

Conoce mas sobre Diego →