Como Claude Code decide si ejecutar un comando: el sistema de permisos con ML
Nota: Los fragmentos de codigo que aparecen en esta serie son recreaciones en Python basadas en la arquitectura y patrones descritos publicamente tras la filtracion. No se reproduce codigo original de Anthropic. El objetivo es puramente educativo: entender los conceptos de ingenieria detras de una herramienta de IA de produccion.
1. Los modos de permisos
Claude Code no tiene un unico nivel de seguridad. Tiene 6 modos que cambian completamente como se comporta el sistema de permisos:
# Recreacion de los modos de permisos
PERMISSION_MODES = {
"default": "Pregunta al usuario para todo (modo normal)",
"acceptEdits": "Auto,aprueba ediciones de archivos y comandos basicos de filesystem",
"plan": "Solo lectura — no ejecuta nada, solo planifica",
"bypassPermissions": "Salta todas las comprobaciones (modo peligroso)",
"dontAsk": "Convierte todas las preguntas en denegaciones silenciosas",
"auto": "Un clasificador de IA decide automaticamente (interno de Anthropic)",
}
# Ademas hay dos modos internos no expuestos al usuario final
INTERNAL_MODES = {
"auto": "Usa un modelo Opus para clasificar cada accion",
"bubble": "Modo interno, excluido de la API publica",
}Cada modo tiene una representacion visual distinta en la interfaz: default no muestra nada especial, plan muestra un icono de pausa, y bypassPermissions muestra un indicador rojo de peligro.
El modo auto es especialmente interesante — y lo veremos en detalle en la seccion 6. Internamente lo llaman el "YOLO classifier", y esta protegido detras de un feature flag que solo Anthropic puede activar.
2. El pipeline de decision: de la peticion al veredicto
Cuando Claude quiere usar una herramienta (leer un archivo, ejecutar un comando, hacer una busqueda web), la peticion pasa por un pipeline con multiples capas de decision. El orden importarque las capas tienen prioridad:
Claude quiere ejecutar: rm ,rf ~/proyecto/tmp
↓
1. ¿Hay una regla de DENEGACION explicita para esta herramienta?
→ Si: DENEGAR (fin)
↓
2. ¿Hay una regla de ASK para esta herramienta?
→ Si: PREGUNTAR al usuario (fin)
↓
3. ¿La propia herramienta dice algo sobre este input?
→ Cada herramienta implementa su propio checkPermissions()
→ BashTool revisa el comando, EditTool revisa la ruta...
↓
4. ¿Es un archivo/ruta peligroso? (.git/, .claude/, .bashrc...)
→ Si: PREGUNTAR (incluso en bypassPermissions)
↓
5. ¿El modo actual permite saltarse permisos?
→ bypassPermissions: PERMITIR
↓
6. ¿Hay una regla de PERMITIR para esta herramienta?
→ Si: PERMITIR (fin)
↓
7. Resultado por defecto: PREGUNTAR al usuario# Recreacion simplificada del pipeline de permisos
def has_permissions_to_use_tool(tool, parsed_input, context):
"""Pipeline completo de decision de permisos."""
# Patas
deny_rule = get_deny_rule_for_tool(tool.name, context.rules)
if deny_rule:
return "deny"
# Paso 2: Reglas de ask explicitas
ask_rule = get_ask_rule_for_tool(tool.name, context.rules)
if ask_rule:
return "ask"
# Paso 3: La herramienta decide internamente
tool_decision = tool.check_permissions(parsed_input, context)
if tool_decision == "deny":
return "deny"
# Paso 4: Comprobaciones de seguridad (bypass,immune)
if is_dangerous_path(parsed_input.get("path", "")):
return "ask" # Ni bypassPermissions puede saltarse esto
# Paso 5: Modo bypass
if context.mode == "bypassPermissions":
return "allow"
# Paso 6: Reglas de permitir
if is_tool_always_allowed(tool.name, context.rules):
return "allow"
# Paso 7: Default
return "ask"Fijate en un detalle critico del paso 4: hay comprobaciones de seguridad que son inmunes al modo bypass. Aunque actives bypassPermissions, el sistema sigue pidiendo confirmacion para tocar archivos dentro de .git/, .claude/, .vscode/, o archivos de configuracion del shell como .bashrc o .zshrc. Es una red de seguridad que no se puede desactivar.
3. Las 4 capas de seguridad de BashTool
La herramienta mas peligrosa de Claude Code es la ejecucion de comandos bash. Por eso tiene el sistema de seguridad mas elaborado: 4 capas independientes que se evaluan en cascada.
Capa 1: Analisis de patrones (heuristicas)
La primera linea de defensa es un conjunto de mas de 20 validadores que revisan el comando con expresiones regulares buscando patrones peligrosos:
# Recreacion del sistema de validacion por patrones
SECURITY_CHECKS = {
"INCOMPLETE_COMMANDS": "Empieza con tab, guion, u operador",
"JQ_SYSTEM_FUNCTION": "Llamadas a system() desde jq",
"OBFUSCATED_FLAGS": "Ofuscacion de flags (e.g., ,\\x72f)",
"SHELL_METACHARACTERS": "Metacaracteres peligrosos del shell",
"DANGEROUS_VARIABLES": "Manipulacion de variables de entorno peligrosas",
"COMMAND_SUBSTITUTION": "Uso de $(), ``, <(), >() u otras sustituciones",
"INPUT_REDIRECTION": "Redireccion de entrada",
"OUTPUT_REDIRECTION": "Redireccion de salida",
"IFS_INJECTION": "Inyeccion via IFS",
"PROC_ENVIRON_ACCESS": "Lectura de /proc/*/environ",
"BRACE_EXPANSION": "Expansion de llaves",
"CONTROL_CHARACTERS": "Caracteres de control (0x00,0x08, etc.)",
"UNICODE_WHITESPACE": "Espacios Unicode invisibles (NBSP, zero,width)",
"ZSH_DANGEROUS_COMMANDS": "Comandos de zsh que acceden a bajo nivel",
}
# Patrones de sustitucion de comandos bloqueados
COMMAND_SUBSTITUTION_PATTERNS = [
(r"<\(", "process substitution <()"),
(r">\(", "process substitution >()"),
(r"=\(", "Zsh process substitution =()"),
(r"\$\(", "$() command substitution"),
(r"\$\{", "${} parameter substitution"),
(r"\$\[", "$[] legacy arithmetic expansion"),
]
# Comandos peligrosos de zsh
ZSH_DANGEROUS = {
"zmodload", "emulate", "sysopen", "sysread", "syswrite",
"sysseek", "zpty", "ztcp", "zsocket", "mapfile",
"zf_rm", "zf_mv", "zf_ln", "zf_chmod", "zf_chown",
"zf_mkdir", "zf_rmdir", "zf_chgrp",
}Capa 2: Analisis de AST con Tree,sitter
Si la capa 1 no encuentra nada sospechoso, el comando pasa a un parser real que construye un arbol de sintaxis abstracta (AST). No analiza el texto del comando — analiza su estructura.
# Recreacion del analisis Tree,sitter
MAX_COMMAND_LENGTH = 10_000 # Comandos mas largos se rechazan directamente
MAX_NODES = 50_000 # Limite de nodos en el AST
# Tipos de nodo estructurales (permitidos)
STRUCTURAL_TYPES = {"program", "list", "pipeline", "redirected_statement"}
# Tipos de nodo peligrosos (causan rechazo inmediato)
DANGEROUS_TYPES = {
"command_substitution", # $(...)
"process_substitution", # <(...)
"expansion", # Expansiones complejas
"subshell", # ..)
"for_statement", # for ... in
"while_statement", # while ...
"if_statement", # if ...
"case_statement", # case ...
"function_definition", # function foo() {...}
"heredoc_redirect", # <<EOF
"herestring_redirect", # <<<
}
class ParseResult:
"""Resultado del analisis Tree,sitter."""
SIMPLE = "simple" # Se pudo extraer un argv confiable
TOO_COMPLEX = "too,complex" # No se puede analizar → preguntar al usuario
UNAVAILABLE = "unavailable" # Tree,sitter no esta cargado
def parse_for_security(command):
"""Analiza un comando bash para seguridad.
PRINCIPIO CLAVE: fail,closed allowlist.
Cualquier nodo que NO este explicitamente en la allowlist
hace que todo el comando se clasifique como 'too,complex'.
"""
if len(command) > MAX_COMMAND_LENGTH:
return ParseResult.TOO_COMPLEX, "comando demasiado largo"
tree = tree_sitter_parse(command)
if tree is None:
returnarseResult.UNAVAILABLE, None
if tree.root_node.count > MAX_NODES:
return ParseResult.TOO_COMPLEX, "demasiados nodos"
commands = []
for node in walk(tree.root_node):
if node.type in STRUCTURAL_TYPES:
continue
if node.type in DANGEROUS_TYPES:
return ParseResult.TOO_COMPLEX, f"nodo peligroso: {node.type}"
if node.type == "command":
commands.append(extract_argv(node))
return ParseResult.SIMPLE, commandsEl principio fundamental es fail,closed allowlist: si Tree,sitter encuentra CUALQUIER tipo de nodo que no esta explicitamente en la lista blanca, el comando entero se clasifica como "demasiado complejo" y se escala a la siguiente capa. No intenta adivinar si es seguro — si no lo reconoce, lo bloquea.
Ademas, antes de llamar a Tree,sitter, el sistema ejecuta pre,checks que detectan discrepancias entre como Tree,sitter interpreta el comando y como lo ejecutaria bash realmente. Esto incluye caracteres de control, eacios Unicode invisibles, y trucos de expansion especificos de zsh. El objetivo es cerrar cualquier brecha entre "lo que el parser entiende" y "lo que el shell ejecuta".
Capa 3: Validacion de comandos de solo lectura
Hay una lista extensa de comandos que son seguros por naturaleza porque solo leen informacion sin modificar nada. Estos se auto,aprueban sin preguntar:
# Recreacion de la lista de comandos de solo lectura
READ_ONLY_SIMPLE = {
# Lectura de archivos
"cat", "head", "tail", "wc", "stat", "strings",
"hexdump", "od", "nl",
# Informacion del sistema
"id", "uname", "free", "df", "du", "nproc",
"uptime", "pwd", "whoami", "arch",
# Manipulacion de texto (sin escritura)
"cut", "paste", "tr", "column", "tac", "rev",
"fold", "expand", "unexpand", "fmt", "comm",
"cmp", "diff", "numfmt",
# Utilidades basicas
"true", "false", "sleep", "which", "type",
"expr", "test", "seq", "cal", "basename",
"dirname", "realpath", "readlink",
}
# Comandos seguros con validacion de flags
READ_ONLY_WITH_FLAGS = {
"git": {
"safe_subcommands": ["diff", "status", "log", "blame",
"ls,files", "remote", "tag",
"branch", "stash list"],
"unsafe_subcommands": ["push", "reset", "clean"],
},
"grep": {"safe_flags": [",r", ",n", ",l", ",c", ",i", ",v", ",w"]},
"sort": {"safe_flags": [",n", ",r", ",k", ",t", ",u"]},
"docker": {
"safe_subcommands": ["ps", "images", "logs", "inspect",
"stats", "top", "port"],
},
"xargs": {"requires_safe_child_command": True},
}Fijate en que git no se auto,aprueba en bloque — solo subcomandos especificos como diff, status o log. Un git push o git reset ,,hard nunca se auto,aprueba.
Capa 4: El clasificador ML (modo auto)
Si las tres capas anteriores no dan un veredicto claro — es decir, el comando no es claramente peligroso ni claramente seguro — y el usuario tiene actel modo auto, entra en juego un clasificador basado en IA. Lo veremos en detalle en la seccion 6.
4. El sistema de reglas
Las reglas de permisos vienen de 8 fuentes diferentes, evaluadas en orden de prioridad:
# Recreacion del sistema de fuentes de reglas
RULE_SOURCES = [
"userSettings", # ~/.claude/settings.json (configuracion global)
"projectSettings", # .claude/settings.json (configuracion del proyecto)
"localSettings", # .claude/settings.local.json (configuracion local)
"flagSettings", # Feature flags (remotos)
"policySettings", # Politicas de empresa (gestionadas)
"cliArg", # Argumentos de linea de comando (,,allow, ,,deny)
"command", # Comandos /allow, /deny en la sesion
"session", # Decisiones "siempre permitir" durante la sesion
]
class PermissionRule:
def __init__(self, source, behavior, tool_name, rule_content=None):
self.source = source # De donde viene la regla
self.behavior = behavior # "allow", "deny", o "ask"
self.tool_name = tool_name # "Bash", "Edit", "WebFetch"...
self.rule_content = rule_content # Patron opcionalLas reglas de shell soportan tres tipos de coincidencia:
# Tipos de reglas para comandos shell
class ShellRuleType:
EXACT = "exact" # "npm run build" — coincidencia exacta
PREFIX = "prefix" # "npm run:*" — coincidencia por prefijo
WILDCARD = "wildcard" # "git * ,,no,verify" — con comodines
# Ejemplos:
# Bash(npm run build) → exacta: solo ese comando
# Bash(npm run:*) → prefijo: cualquier npm run ...
# Bash(git * ,,no,verify) → wildcard: cualquier git con ,,no,verifyAntes de comparar un comando contra las reglas, el sistema limpia prefijos seguros. Por ejemplo, si el comando es RUST_BACKTRACE=1 cargo test, se elimina RUST_BACKTRACE=1 antes de comparar, porque esa variable de entorno es inofensiva.
Pero no todas las variables se limpian. El sistemlista explicita de variables que nunca se eliminan porque podrian cambiar el comportamiento de los comandos de forma peligrosa:
# Variables de entorno que NUNCA se eliminan antes de comparar reglas
NEVER_STRIP_VARS = {
"PATH", # Podria redirigir que binarios se ejecutan
"LD_PRELOAD", # Inyeccion de librerias
"LD_LIBRARY_PATH", # Carga de librerias maliciosas
"DYLD_*", # Equivalente en macOS
"PYTHONPATH", # Carga de modulos Python arbitrarios
"NODE_PATH", # Carga de modulos Node arbitrarios
"NODE_OPTIONS", # Flags arbitrarios a Node
"HOME", # Cambiaria donde busca configuraciones
"BASH_ENV", # Script que bash ejecuta al arrancar
"SHELL", # Podria cambiar el interprete
}5. Las denegaciones automaticas
Independientemente de las reglas, el modo y el clasificador, hay patrones que nunca se aprueban automaticamente. Si el modo auto esta activo y un comando coincide con estos patrones, se fuerza la pregunta al usuario:
# Recreacion de los patrones peligrosos (se eliminan de auto,allow al entrar en modo auto)
DANGEROUS_BASH_PATTERNS = [
# Interpretes de lenguajes (pueden ejecutar codigo arbitrario)
"python", "python3", "python2", "node", "deno", "tsx",
"ruby", "perl", "php", "lua",
# Gestores de paquetes (pueden ejecutar scripts)
"npx", "bunx", "npm run", "yarn run", "pnpm run", "bun run",
# Shells (ejecucion arbitraria)
"bash", "sh", "zsh", "fish", "eval", "exec",
# Herramientas de red y sistema
"env", "xargs", "sudo", "ssh",
"curl", "wget",
]Ademas, el sistema nunca sugiere reglas de permiso para prefijos de shell porque serian equivalentes a permitir cualquier cosa:
# Prefijos que nunca se sugieren como reglas
# Porque Bash(bash:*) es equivalente a Bash(*)
BARE_SHELL_PREFIXES = {
"sh", "bash", "zsh", "fish", "csh", "tcsh", "ksh", "dash",
"cmd", "powershell", "pwsh", "env", "xargs", "nice", "stdbuf",
"nohup", "timeout", "time", "sudo", "doas", "pkexec",
}6. El clasificador de IA (YOLO classifier)
El componente mas sofisticado del sistema de permisos es un clasificador que usa otro modelo de IA para decidir si una accion es segura. Internamente lo llaman "YOLO classifier" — una ironia: el sistema que decide si algo es seguro se llama "You Only Live Once".
# Recreacion del clasificador YOLO
class YoloClassifier:
"""Clasificador ML que decide si una accion del agente es segura."""
def __init__(self, model=None):
# Usa el mismo modelo que el loop principal (Opus,class)
# Configurable via CLAUDE_CODE_AUTO_MODE_MODEL
self.model = model or get_current_model()
def classify(self, transcript, current_action, context):
"""Clasifica una accion como segura o peligrosa.
Proceso en 2 fases:
, Fase 1 (rapida): max 64 tokens, decision inmediata
, Fase 2 (razonada): solo si Fase 1 dice 'bloquear',
max 4096 tokens con chain,of,thought
"""
# Fase 1: Decision rapida
fast_result = self._classify_fast(transcript, current_action)
if fast_result.should_allow:
return ClassifierResult(blocked=False)
# Fase 2: Razonamiento completo (reduce falsos positivos)
detailed_result = self._classify_with_reasoning(
transcript, current_action, context
)
return detailed_result
def _classify_fast(self, transcript, action):
"""Fase rapida: 64 tokens max, para inmediatamente en </block>."""
response = call_model(
system=self._build_system_prompt(),
messages=[{
"role": "user",
"content": self._format_transcript(transcript) + str(action),
}],
max_tokens=64,
stop_sequences=["</block>"],
)
return parse_xml_decision(response)
def _classify_with_reasoning(self, transcript, action, context):
"""Fase razonada: 4096 tokens, chain,of,thought completo."""
response = call_model(
system=self._build_system_prompt(),
messages=[{
"role": "user",
"content": self._format_transcript(transcript) + str(action),
}],
max_tokens=4096,
)
return parse_reasoning_result(response)
def _format_transcript(self, transcript):
"""Formatea la conversacion para el clasificador.
DECISION DE SEGURIDAD CLAVE: el texto del asistente se EXCLUYE.
Solo se incluyen los bloques tool_use, porque el texto libre
del asistente podria estar crafteado para influir al clasificador.
"""
lines = []
for msg in transcript:
if msg["role"] == "user":
lines.append(f"User: {msg['content']}")
elif msg["role"] == "assistant":
for block in msg.get("tool_use_blocks", []):
lines.append(f"{block['name']} {block['input']}")
return "\n".join(lines)Fijate en la decision de excluir el texto libre del asistente del transcript. Es una medida anti,manipulacion: como el texto lo genera el modelo (no el usuario), un modelo comprometido podria generar texto diseñado para engañar al clasificador y hacer que apruebe acciones peligrosas. Enviando solo las acciones concretas (herramientas usadas), el clasificador toma decisiones basandose en hechos, no en narrativas.
El clasificador tiene herramientas que se saltan la clasificacion completamente — las que son seguras por definicion:
# Herramientas exentas del clasificador (siempre seguras)
SAFE_TOOLS = {
"Read", "Grep", "Glob", "LSP",
"TodoWrite", "TaskCreate", "TaskUpdate", "TaskList", "TaskGet",
"AskUserQuestion", "EnterPlanMode", "ExitPlanMode",
"SendMessage", "TeamCreate", "TeamDelete",
"Sleep",
}Y tiene un mecanismo de fail,closed: si el clasificador falla (error de API, no puede parsear la respuesta, el usuario aborta), la dion por defecto es bloquear.
7. El sistema anti,bucle de denegaciones
¿Que pasa si el clasificador automatico se equivoca repetidamente? El sistema tiene limites para evitar bucles infinitos:
# Recreacion del sistema de tracking de denegaciones
DENIAL_LIMITS = {
"max_consecutive": 3, # 3 denegaciones consecutivas del clasificador
"max_total": 20, # 20 denegaciones totales en la sesion
}
class DenialTracker:
def __init__(self):
self.consecutive_denials = 0
self.total_denials = 0
def record_denial(self):
self.consecutive_denials += 1
self.total_denials += 1
def record_allow(self):
self.consecutive_denials = 0 # Se resetea con cada aprobacion
def should_fallback_to_user(self):
"""Cuando se superan los limites, se deja de usar el
clasificador y se pregunta directamente al usuario."""
return (
self.consecutive_denials >= DENIAL_LIMITS["max_consecutive"]
or self.tota_denials >= DENIAL_LIMITS["max_total"]
)Despues de 3 denegaciones consecutivas o 20 totales en una sesion, el modo auto se degrada automaticamente a preguntas directas al usuario. En modo headless (sin usuario presente), se lanza un AbortError directamente.
8. Proteccion de archivos y rutas
El sistema de permisos no solo protege comandos bash. Tambien tiene un sistema dedicado a proteger archivos y rutas del sistema:
# Archivos que siempre requieren confirmacion (incluso en bypassPermissions)
DANGEROUS_FILES = [
".gitconfig", ".gitmodules",
".bashrc", ".bash_profile",
".zshrc", ".zprofile", ".profile",
".ripgreprc",
".mcp.json", ".claude.json",
]
# Directorios protegidos
DANGEROUS_DIRECTORIES = [".git", ".vscode", ".idea", ".claude"]
# Validaciones de seguridad en rutas
def validate_path(path, operation):
"""Valida que una ruta es segura antes de permitir acceso."""
# Bloquear rutas UNC (podrian filtrar credenciales via WebDAV)
if path.startswith("\\\\"):
return "deny", "ruta UNC bloqueada"
# Rechazar variantes de tilde (vulnerabilidad TOCTOU)
if path.startswith("~+") or path.startswith("~,"):
return "deny", "expansion de tilde peligrosa"
# Rechazar sintaxis de expansion del shell
if any(c in path for c in ["$", "%"]):
return "deny", "posible expansion de shell en ruta"
# Proteger rutas de eliminacion peligrosa
if is_dangerous_removal_path(path):
return "deny", "ruta protegida contra eliminacion"
return "allow", None
def is_dangerous_removal_path(path):
"""Rutas que nunca deberian poder eliminarse."""
dangerous = [
"/", # Raiz del sistema
os.path.expanduser("~"), # Directorio home
# Hijos directos de la raiz: /usr, /etc, /var...
# Raices de unidades en Windows: C:\, D:\...
]
return path in dangerous9. El sandbox
Para una capa adicional de seguridad, Claude Code puede ejecutar comandos dentro de un sandbox — un entorno aislado que restringe que puede hacer el comando:
# Recreacion del sistema de sandbox
class SandboxManager:
"""Entorno aislado para ejecucion de comandos."""
def __init__(self, config):
# Restricciones de red
self.allowed_domains = config.get("allowed_domains", [])
self.denied_domains = config.get("denied_domains", [])
# Restricciones de filesystem
self.read_allow_paths = config.get("read_allow", [])
self.read_deny_paths = config.get("read_deny", [])
self.write_allow_only = config.get("write_allow_only", [])
def should_use_sandbox(self, command, tool_input):
"""Decide si un comando debe ejecutarse en sandbox."""
if not self.is_enabled:
return False
# El modelo puede pedir explicitamente salir del sandbox
# (requiere permiso especial)
if tool_input.get("dangerouslyDisableSandbox"):
return not self.are_unsandboxed_commands_allowed()
# Comandos excluidos por el usuario
if self.is_excluded_command(command):
return False
return TrueCuando el sandbox esta activo, hay una optimizacion importante: los comandos que se ejecutan dentro del sandbox pueden auto,aprobarse sin pasar por el clasificador, ya que el sandbox limita el daño que pueden hacer. El parametro se llama autoAllowBashIfSandboxed.
El modelo puede solicitar salir del sandbox usando un parametro llamado dangerouslyDisableSandbox — pero esto solo funciona si el sistema esta configurado para permitir comandos fuera del sandbox.
10. Los hooks de permisos
Los usuarios pueden definir hooks — comandos de shell que se ejecutan en puntos especificos del flujo de permisos:
# Hooks relevantes para el sistema de permisos
PERMISSION_HOOKS = {
"PreToolUse": {
"when": "Antes de ejecutar cualquier herramienta",
"can_return": ["allow", "deny", "ask"],
"use_case": "Auto,aprobar herramientas en CI/CD" },
"PostToolUse": {
"when": "Despues de ejecutar una herramienta",
"can_return": None, # Solo notificacion
"use_case": "Logging, auditorias",
},
"PermissionRequest": {
"when": "Cuando se va a mostrar un prompt de permiso",
"can_return": ["allow", "deny"],
"use_case": "Auto,responder en agentes headless",
},
"PermissionDenied": {
"when": "Cuando un permiso es denegado",
"can_return": None, # Solo notificacion
"use_case": "Alertas, metricas",
},
}
# Resultado de un hook de permiso
class HookResult:
behavior: str # "allow" o "deny"
message: str = None # Mensaje opcional
updated_input: dict = None # Input modificado (el hook puede alterar el comando)
interrupt: bool = False # Si True, aborta el agente enteroLos hooks pueden incluso modificar el input de una herramienta antes de que se ejecute. Pero las politicas empresariales pueden restringir que hooks estan permitidos para evitar que un hook malicioso desactive las protecciones.
11. Flujo completo: que pasa cuando Claude quiere ejecutar rm ,rf ~/important
Vamos a seguir el recorrido completo de un comando peligroso:
Claude genera: Bash(command="rm ,rf ~/important")
↓
[CAPA 1: Patrones]
→ No hay sustitucion de comandos
→ No hay metacaracteres sospechosos
→ No hay ofuscacion
→ Resultado: PASA (no hay patron peligroso obvio)
↓
[CAPA 2: Tree,sitter]
→ Parse: programa → lista → comando simple
→ Comando: ["rm", ",rf", "~/important"]
→ No hay nodos peligrosos
→ Resultado: SIMPLE (comando extraible)
↓
[CAPA 3: Solo lectura]
→ "rm" NO esta en la lista de comandos de solo lectura
→ Resultado: NO ES SEGURO
↓
[PIPELINE DE PERMISOS]
→ ¿Regla de denegacion para Bash? No
→ ¿rm ,rf es un patron peligroso? No esta en la blocklist exacta
→ ¿"~/important" es una ruta peligrosa?
→ ~ se expande al home del uso (es un subdirectorio, no ~ directamente)
→ ¿Modo bypass? No (modo default)
→ ¿Regla de permitir? No
→ Resultado: ASK
↓
[MODO AUTO, si esta activo]
→ ¿Es una herramienta segura exenta? No (Bash nunca es exenta)
→ Fase 1 del clasificador: "¿rm ,rf ~/important es seguro?"
→ Respuesta rapida: BLOQUEAR
→ Fase 2 (razonamiento): "El usuario no ha pedido borrar archivos importantes..."
→ Confirmacion: BLOQUEAR
→ Resultado: DENY (con motivo explicado)
↓
[MODO DEFAULT]
→ Se muestra prompt al usuario: "Claude quiere ejecutar: rm ,rf ~/important"
→ Opciones: Permitir / Denegar / Permitir siempre para este patron
→ El usuario decide12. Curiosidades
El killswitch remoto de bypassPermissions
Anthropic puede desactivar remotamente el modo bypassPermissions para todos los usuarios a traves de un feature flag (Statsig gate). Se comprueba una sola vez al inicio de la sesion. Esto significa que si se descubre un desactivar el modo peligroso para toda la flota sin necesidad de actualizar el cliente.
Proteccion contra repos git maliciosos
El sistema de validacion de solo lectura detecta un ataque especifico: comandos compuestos que primero hacen cd a un directorio y luego ejecutan git. ¿Por que es peligroso? Porque un repositorio git puede contener hooks maliciosos en su directorio .git/hooks/. Si Claude hace cd /repo,malicioso && git status, el hook post,checkout se ejecutaria automaticamente. El sistema bloquea este patron combinado.
Placeholders anti,inyeccion
Cuando el sistema procesa comandos para analisis, usa placeholders aleatorios generados con randomBytes(8) convertidos a hexadecimal. ¿Por que no placeholders predecibles? Porque un atacante podria incluir la string del placeholder en el comando, confundiendo al parser. Con 16 caracteres hex aleatorios, la probabilidad de colision es practicamente cero.
El limite de subcomandos
Si un comando tiene mas de *50 subcomandos(por ejemplo, una cadena enorme de &&), el sistema deja de intentar analizarlo y directamente pregunta al usuario. Tambien limita a 5 las reglas sugeridas a partir de un solo comando compuesto.
Heredocs seguros
El sistema tiene un caso especial para heredocs: $(cat <<'DELIM'...DELIM) es seguro cuando el delimitador esta entre comillas simples o escapado, porque el cuerpo se trata como texto literal. Esta excepcion permite que Claude use heredocs para pasar texto largo a comandos sin que el sistema lo bloquee por ser una "sustitucion de comando".
La gracia de 200ms
Cuando el clasificador automatico esta evaluando una accion, el usuario puede cancelar la auto,aprobacion pulsando una tecla. Pero hay un periodo de gracia de 200 milisegundos antes de que la pulsacion cuente — para evitar que una tecla accidental interrumpa el flujo.
,,,
Fuentes
, Analisis basado en la filtracion del codigo fuente de Claude Code v2.1.88, documentada en el Post 1 de esta serie
, The Verge — "Claude Code leak exposes a Tamagotchi,style 'pet' and an always,on agent"
, Zscaler ThreatLabz — Analisis tecnico del leak
,,,
Esta serie es un proyecto de Levante — un cliente de escritorio open source para trabajar con MCPs, modelos de IA y herramientas en una sola interfaz. Si te interesa la arquitectura de herramientas de IA, te interesa Levante.
Saul Gomez es fundador de Levante, un cliente de escritorio open source para trabajar con modelos de IA.
