Fail-closed en hooks y reglas deny: por qué un control que falla deja pasar
Escribes una regla deny para rm -rf. La pruebas y bloquea. Semanas después, alguien antepone una variable de entorno al comando y la regla ya no lo ve. Nadie recibe un error. El agente simplemente ejecuta lo que debías haber bloqueado.
Esa es la forma más común de fallo en los controles de un agente: no se rompen con ruido, fallan en silencio hacia el lado permisivo. Las notas de Claude Code de esta semana tienen varios ejemplos, y casi todos caen en dos familias.
Familia 1: la regla compara la escritura, no el efecto
Una regla deny suele comparar cómo está escrita la acción. Lo que importa es qué hace. Cada vez que esas dos cosas se separan, hay un bypass. Las notas de la v2.1.289 listan varios casos:
- Reglas deny y ask de Bash que no veían un comando detrás de un prefijo de variable con valor expandido, como
TZ="$HOME" rm -rf build, cuando el sandbox auto-permite comandos. - Reglas de Bash que se saltaban cuando había una asignación de variable sola antes del comando.
- Reglas deny de
Readque no aplicaban a archivos mencionados con @, modificados o seleccionados en el IDE a través de un symlink. - Una regla deny o ask sobre una parte anidada de un comando compuesto que no se mantenía frente a la aprobación de un mod instalado por el usuario, en máquinas administradas.
La v2.1.290 añade otro: un CLAUDE.md, una regla o un AGENTS.md que era un symlink fuera de los directorios de trabajo se cargaba aunque estuviera activo permissions.blockReadsOutsideWorkingDirectories o una regla deny de Read. También corrige patrones de URL con comodín dentro de una etiqueta de host xn-- que coincidían distinto de un proceso a otro.
La v2.1.292 sigue con lo mismo. Corrige que rm -rf sobre el nombre corto 8.3 de la carpeta home, u otra grafía alternativa en Windows, no se tratara como borrado del home o de una unidad. Y corrige que los hooks PreToolUse y el auto mode se saltaran el prompt de permisos al leer archivos desde rutas de red UNC.
El SDK de TypeScript 0.3.290 tiene el mismo patrón en versión de API. Las reglas deny y ask no alcanzaban herramientas definidas en toolAliases cuando se escribían con comodín (Bash*), con un patrón que coincide con todo (Bash(**)) o con un campo de entrada que también estaba en disallowedTools.
Hay un caso en otro proyecto. Codex 0.162.0 corrige que la configuración de ripgrep pudiera debilitar las máscaras deny-glob.
El patrón es siempre el mismo. Existe una segunda forma de nombrar lo mismo: un alias, una ruta alterna, un prefijo, un enlace. La regla cubría la primera forma y no la segunda.
Familia 2: el control falla y la acción pasa
La segunda familia no es de coincidencia, sino de error. Un hook que no arranca, que expira o que sale con un código inesperado no está diciendo “sí”, pero el sistema puede tratarlo como un sí.
La v2.1.295 de Claude Code añade onFailure: "block" para hooks de comando y HTTP. Con esa opción, un hook que no puede arrancar, expira o sale con un código inesperado bloquea la acción “en lugar de dejarla pasar”. La propia redacción de la nota dice cuál era el comportamiento sin ella.
Otros dos arreglos apuntan en la misma dirección:
- v2.1.294: los hooks
promptyagentescritos como instrucciones (por ejemplo, “Bloquea comandos que…”) dejaban pasar justo lo que debían bloquear. - v2.1.296: los hooks
PreToolUsede la configuración administrada que denegaban con"continue": false, y los hookspromptadministrados que bloqueaban, rechazaban la llamada pero no terminaban el turno.
Un control cuyo veredicto no se interpreta bien es equivalente a un control ausente.
Qué tienen en común
Ninguno de estos casos es un exploit sofisticado. Son huecos entre lo que el control entiende y lo que el sistema ejecuta, y huecos entre “el control no pudo decidir” y “el sistema decidió permitir”. En ambos, el valor por defecto de la ruta desconocida es permitir.
Un guardrail sólido invierte ese valor. Lo que no reconoce, lo que no puede evaluar y lo que falla se bloquea.
Reglas accionables
- Declara qué hace cada hook de seguridad al fallar. Si usas hooks de comando o HTTP como control, configura
onFailure: "block". Un hook que se cae no debe equivaler a aprobación. - Prueba cada regla deny con variantes del mismo comando. Prefijo de variable, asignación previa, comando anidado dentro de uno compuesto, ruta a través de un symlink, grafía alterna en Windows. Si una variante pasa, la regla cubre texto y no efecto.
- No escribas hooks de bloqueo como instrucciones sueltas. Los hooks
promptyagentredactados como “Bloquea comandos que…” fallaron en la práctica. Verifica con casos que deban bloquearse, no solo con casos que deban pasar. - Al definir reglas por herramienta, prueba también los alias y los comodines. Comprueba que
toolAliasesqueda cubierto por la regla deny que escribiste. - Después de cada actualización, vuelve a correr tu suite de casos prohibidos. Una regla que bloqueaba antes puede dejar de hacerlo, y una corrección puede cambiar lo que tu equipo daba por cierto. Esa suite es el único registro de que el control sigue funcionando.
- En máquinas administradas, revisa las rutas de lectura y las fuentes de configuración. Las correcciones de
CLAUDE.mdyAGENTS.mdcon symlinks muestran que lo que carga el agente también es superficie de control.
Fuentes:
- https://github.com/anthropics/claude-code/releases/tag/v2.1.289
- https://github.com/anthropics/claude-code/releases/tag/v2.1.290
- https://github.com/anthropics/claude-code/releases/tag/v2.1.292
- https://github.com/anthropics/claude-code/releases/tag/v2.1.294
- https://github.com/anthropics/claude-code/releases/tag/v2.1.295
- https://github.com/anthropics/claude-code/releases/tag/v2.1.296
- https://github.com/anthropics/claude-agent-sdk-typescript/releases/tag/v0.3.290
- https://github.com/openai/codex/releases/tag/rust-v0.162.0
Seguir leyendo
Esto se demuestra en video, y se sigue a diario en la comunidad
Las demos completas van al canal. El seguimiento release por release, adentro.