Ir al contenido

Sandbox y seguridad

Un agente lee archivos, ejecuta comandos de shell y llama a API externas en tu nombre. El fallo que te importa rara vez es malicia: es un agente que malinterpreta una ruta y reescribe tu checkout de trabajo, o uno que hace push de una rama que aún no habías terminado de revisar. Así que la pregunta que el producto tiene que responder no es «¿es confiable el agente?», sino «cuál es el radio de impacto cuando se equivoca».

Control Center responde eso por capas y las capas son independientes a propósito. Si falta una, las demás siguen en pie. Eso importa, porque la capa más externa —el sandbox del sistema operativo— es la única condicional: depende de que el host ofrezca un backend, y en Windows ninguno lo hace.

Cinco cosas limitan la ejecución de un agente en cualquier host, haya sandbox o no. Ninguna es una frontera del kernel; todas son reales. Donde el host ofrece un backend, el sandbox del sistema operativo se sitúa por encima de ellas, y la sección «Qué añade el sandbox y dónde se detiene», más abajo, explica qué cambia eso.

Su propio worktree de copia en escritura. Cada conversación aprovisiona su propio checkout de cada repositorio vinculado, clonado mediante copia en escritura con el binario nativo rift que se incluye, en lugar de copiarse byte a byte, así que el aislamiento es lo bastante barato como para ser la opción predeterminada. El repositorio registrado que agregaste al espacio de trabajo no es el directorio en el que trabaja el agente: dos conversaciones que tocan el mismo repositorio no pueden ver las ediciones de la otra y ninguna está editando tu checkout.

Vale la pena ser precisos con el nivel de detalle: el worktree es por conversación, no por agente. Dos agentes de la misma conversación reciben directorios de trabajo overlay distintos que comparten el repos/ de esa conversación —que es justo lo que se busca, ya que colaboran en un mismo cambio—, mientras que sus directorios privados de agente permanecen separados. La copia en escritura es el único backend allí donde se distribuye rift: un sistema de archivos que no puede hacer reflink, o un directorio de datos en un volumen distinto al del repositorio, hace fallar el aprovisionamiento en vez de recurrir a otra cosa. Windows es la excepción: allí no hay backend de copia en escritura, así que git worktree es el backend, no una degradación silenciosa.

Confinamiento de rutas en las herramientas de archivos del arnés integrado. Las herramientas read, write, edit, apply_patch, search, find y search_files del arnés resuelven cada ruta contra el directorio de trabajo de la ejecución más las raíces compartidas —el directorio repos/ de la conversación, donde viven los worktrees, y el directorio de adjuntos del espacio cuando existe— y rechazan todo lo que quede fuera. Las rutas absolutas solo se aceptan si ya estaban dentro. Un enlace simbólico dentro del worktree que apunte fuera de él también se resuelve y se rechaza. Por eso un agente no puede usar write sobre ~/.ssh aunque no haya ningún sandbox de por medio.

La superficie de herramientas del modo. El modo de una conversación decide qué herramientas se materializan siquiera para la ejecución. En los modos de solo lectura (plan, review, orchestrate), las herramientas que mutan simplemente no están en el registro y la política de comandos de shell de esos modos niega sin más todo comando que mute —git commit, git push, npm install, docker y el resto— en lugar de preguntar por ellos. Una herramienta que no está presente no se puede llamar, lo cual es una garantía más fuerte que la de una herramienta presente y condicionada.

Las barandillas de acciones unificadas. Cada herramienta que muta declara las clases de efecto que puede producir, y el motor de políticas resuelve cada una como permitir, preguntar o denegar antes de que se ejecute la llamada. Un prompt sin ningún aprobador conectado se resuelve como denegado: una pregunta que nadie puede responder nunca es un sí. Consulta Barandillas para ver el modelo de resolución.

Credenciales del broker y un entorno saneado. Las credenciales se emiten en cada lanzamiento y se revocan cuando termina la ejecución, y el token de GitHub que tiene una ejecución puede leer pero no hacer push; consulta «Los push pasan por Control Center» más abajo. ACP, el bash del arnés y Claude Code bajo el wrapper nativo reciben un entorno al que se le quitaron los vectores de inyección del enlazador dinámico y del intérprete (LD_PRELOAD, DYLD_INSERT_LIBRARIES, NODE_OPTIONS, GIT_SSH_COMMAND y similares) antes de que arranque el proceso. Claude Code a través de NoSandboxAdapter hereda el entorno del host.

Las herramientas de archivos del arnés se ejecutan en el proceso del servidor, no en un proceso hijo, así que ningún perfil de sandbox podría limitarlas aunque se aplicara uno: solo bash pasa por el ejecutor de comandos con sandbox. Su confinamiento es la comprobación de rutas descrita antes, de modo que esa comprobación es determinante y no una segunda línea de defensa.

La herramienta bash es el otro punto donde conviene ser precisos. Sus comandos se evalúan contra la política de comandos del modo y luego se lanzan con un entorno saneado y el worktree como directorio de trabajo. Donde existe un backend, ese lanzamiento va envuelto, así que es el sandbox lo que impide que un comando de shell escriba fuera del worktree; donde no existe —Windows, o Linux sin bwrap y socat—, la política de comandos es lo único que separa al comando del resto del disco. En el modo chat, la política niega sin más una lista fija (escalada de privilegios, destrucción del sistema y del disco, escape de contenedores y de espacios de nombres) y pregunta ante mutaciones de estado remoto (git push, gh pr create, publicación e instalación de paquetes). Todo lo que no está en ninguna de las dos listas se ejecuta. Los pipelines, las cadenas y los subshells se analizan y cada subcomando se evalúa por separado, y gana la decisión más restrictiva.

Claude Code, cuando se usa como adaptador, es el caso en el que el sandbox carga con el mayor peso. Control Center lo lanza con --dangerously-skip-permissions, porque un claude -p no interactivo se bloquearía para siempre en su propia solicitud de aprobación. Sus llamadas de lectura, escritura y edición ocurren dentro de su propio proceso y Control Center nunca las ve. Sus llamadas a Bash se envían, antes de ejecutarse, a un hook del servidor que clasifica la línea de comandos y pregunta a la regla correspondiente, y las herramientas mcp__* que sirve Control Center pasan por el control como siempre. Donde existe un backend, esas llamadas invisibles siguen ocurriendo dentro del wrapper del sandbox, así que el sistema operativo es la frontera que las rodea. Donde no existe, no hay ninguna. Cada adaptador lo declara en su matriz de aplicación; consulta la matriz de honestidad de los adaptadores.

No hay indicadores de permisos por agente. Si un agente puede crear un commit, hacer push, abrir una pull request, escribir en un rastreador o llegar a la red lo deciden las barandillas —permitir, preguntar primero o denegar—, y las mismas reglas se aplican sea cual sea el adaptador que ejecute al agente.

Un push es la acción que tiene que aguantar incluso frente a un agente que intenta esquivar una regla, así que se aplica por lo que el agente tiene y no por lo que ejecuta. Con una GitHub App configurada, una ejecución en un worktree de GitHub recibe un token de instalación con alcance de repositorio que puede leer contenido y trabajar con pull requests, pero no hacer push. La configuración de git de la ejecución reescribe cada URL de push de GitHub (https://github.com/, git@github.com:, ssh://git@github.com/) hacia la pasarela de ejecuciones de agentes, un endpoint de loopback en cc_server. La pasarela lee qué refs actualiza el push, pregunta a la regla Enviar a un remoto —así que se aplica una regla limitada a ramas, como «denegar los push a main»— y solo entonces reenvía el push a GitHub con un token de escritura que acuña para ese fin y que nunca entrega al agente. Un push rechazado aparece en la propia salida de git:

! [remote rejected] main -> main (Control Center policy: …)

La pasarela solo reenvía push al repositorio propio del worktree o a un repositorio de GitHub vinculado al espacio de trabajo. Los fetch van directamente a GitHub con el token de lectura.

Los demás comandos de shell encuentran una puerta más blanda. La herramienta bash del arnés y, mediante un hook, la herramienta Bash de Claude Code clasifican cada línea de comandos y preguntan a la regla que le corresponde: git commit, merge, rebase, cherry-pick, revert y am son Crear un commit; gh pr create es Abrir una pull request; gh pr merge, review, comment y close son Publicar una revisión o fusionar; gh issue create, edit, comment y close son Escribir en un rastreador externo; las instalaciones de paquetes son Instalar un paquete; curl y wget son Acceder a la red. Esto reconoce la forma habitual de escribir una acción, y un agente decidido a ocultarla (eval, un archivo de script, una llamada directa a la API) puede hacerlo. Los push no dependen de ello.

En un sandbox no se coloca ninguna clave de API de tickets. Los agentes escriben tickets con las herramientas MCP de Control Center, que preguntan a Escribir en un rastreador externo.

La regla Acceder a la red se lee una sola vez, cuando arranca la ejecución. La red de un sandbox está activada o desactivada, así que denegar la desactiva, mientras que permitir y preguntar primero la dejan activada; las herramientas web_fetch y web_search del arnés siguen preguntando en cada llamada. Las credenciales se revocan cuando la ejecución se desmonta, junto con cualquier token de escritura que haya acuñado la pasarela, así que un identificador no sobrevive a la ejecución.

La enumeración SandboxBackend tiene tres valores: native (Seatbelt en macOS, bubblewrap en Linux y WSL2), none y microvm. El envoltorio de comandos de agentes solo usa los dos primeros: microvm es el backend de VM aislada para terminales interactivos y rigs, no una mejora silenciosa de un envoltorio de bash. Entender la forma nativa explica qué ganas allí donde se aplica y qué nunca te iba a dar.

cc_server sondea el host una vez al arrancar y, donde un backend responde, alimenta ambos puntos de enganche que tiene la ruta de despacho, porque los transportes no comparten uno solo. El primero (SandboxPort) envuelve la CLI de Claude Code. El segundo (SandboxManager) envuelve los adaptadores ACP y la herramienta bash del arnés integrado. Alimentar solo uno dejaría a los demás lanzando sin envoltorio, que es lo que solía pasar. En un host sin backend, o con --sandbox off, todos lanzan sin envoltorio y el servidor indica en qué caso estás en su línea de arranque. ACP y el bash del arnés siguen saneando el entorno; Claude Code a través de NoSandboxAdapter hereda el entorno del host.

Una superficie se queda deliberadamente al margen: el terminal alojado en el servidor acepta un argumento de backend por sesión y cc_server lo construye con el shell del host como valor predeterminado, así que un terminal que abras desde la aplicación se ejecuta sin sandbox a menos que un cliente indique native de forma explícita. Es un shell real en el host del servidor, no una ejecución de agente.

En Linux, bubblewrap crea un espacio de nombres de montaje, así que el agente ve realmente solo lo que se montó dentro de él. En macOS, sandbox-exec aplica un perfil de Seatbelt que es permisivo de forma predeterminada: un perfil totalmente de denegación por defecto vuelve inutilizable un shell interactivo, porque el número de llamadas dyld, XPC y Mach que habría que enumerar es impracticable. Por eso el perfil de macOS parte de (allow default) y recorta denegaciones: lecturas de rutas secretas, una política de escritura reajustada a denegar con permisos explícitos por encima, bloqueo de unlink en cada ruta denegada y sus ancestros para que un rename no pueda eludir una denegación de escritura, denegaciones de exec para binarios siempre peligrosos y para cualquier cosa copiada a un directorio con permiso de escritura, y red restringida a los puertos del proxy de loopback más DNS.

La consecuencia práctica: en Linux se estrecha la visión que el agente tiene del sistema de archivos; en macOS el agente todavía puede leer buena parte del host, y tus checkouts registrados están protegidos por denegaciones explícitas de escritura y de renombrado en lugar de por invisibilidad. Ambos son aislamiento a nivel de espacios de nombres, no una frontera de seguridad del kernel; los propios backends lo dicen en sus notas de capacidades.

El conjunto de montajes bind es donde se traza la frontera entre agentes. El despacho con overlay (Claude Code y ACP) monta hasta cuatro rutas, resueltas por ejecución: el directorio de trabajo overlay del agente en lectura y escritura, el directorio de configuración del agente en solo lectura, el directorio repos/ compartido de la conversación en lectura y escritura cuando existe, y el directorio de adjuntos del espacio en solo lectura cuando existe. El directorio de un agente hermano nunca se monta. El envoltorio de bash del arnés es más estrecho: solo monta con bind el directorio de trabajo de la ejecución. Esta es la misma forma que la comprobación de rutas en proceso del arnés aplica por software, así que ambas coinciden, pero solo la comprobación de rutas cubre las herramientas de archivos del propio arnés, que nunca llegan a un espacio de nombres de montaje.

Tus checkouts registrados se suman además al conjunto de denegación de escritura, en todos los modos y en las tres rutas envueltas (Claude Code, ACP, el bash del arnés), y cada regla se emite tanto en su forma literal como en su forma resuelta por enlaces simbólicos. La segunda forma no es una sutileza: el kernel compara una regla de denegación con la ruta resuelta, y macOS hace que la resolución sea el caso habitual y no el exótico —/tmp en realidad es /private/tmp, /var/folders/… en realidad es /private/var/folders/…—, así que un checkout al que se llegara por cualquiera de las dos vías habría sido escribible mientras el perfil parecía prohibirlo.

La red, bajo un sandbox, es una lista de permitidos de dominios por sesión que aplican los proxies de loopback en proceso. La salida es de denegación predeterminada: con la red desactivada, los proxies siguen arrancando, pero su lista de permitidos está vacía, así que se rechaza toda conexión saliente. Con ella activada, el resolutor construye la lista de permitidos a partir de una línea base curada —proveedores de API de LLM, alojamiento de git, registros de paquetes, el catálogo de modelos— más los dominios de GitHub, y compara el host de destino de cada conexión con esa lista. Los comodines funcionan (*.example.com coincide con subdominios, no con el dominio raíz). Una lista aparte de siempre denegados prevalece sobre la lista de permitidos y cubre los endpoints de metadatos de la nube y los sumideros de telemetría. Lo que no es configurable es la propia lista: la especificación incluye un campo egressAllowlist para dominios adicionales, pero nada en la ruta de despacho lo rellena, así que en la práctica cada ejecución recibe la línea base más GitHub, y nada más.

En macOS, las denegaciones son observables: un monitor intercepta el registro del sistema en busca de denegaciones de Sandbox, filtra el ruido conocido, deduplica las tormentas de reintentos y expone cada una como un SandboxViolation que indica qué se intentó y qué se bloqueó. En Linux, la ruta del envoltorio analiza stderr en busca de EPERM, Operation not permitted y Permission denied, y emite el mismo tipo de evento: no hay interceptación del registro del sistema. Todos ellos llegan al flujo de eventos de la propia ejecución, así que una llamada al sistema denegada aparece en la transcripción y no solo en un registro. La telemetría solo existe mientras un sandbox nativo está envolviendo realmente el proceso; en un host sin backend no hay nada que observar. Tampoco tiene relación con las barandillas: una violación proviene de una llamada al sistema denegada, nunca de una consulta de regla.

Las confirmaciones vienen de las barandillas, no del sandbox

Sección titulada «Las confirmaciones vienen de las barandillas, no del sandbox»

El sandbox no tiene ninguna vía de aprobación. Lanza, ejecuta, emite eventos y se desmonta; no hay ningún sitio en él donde una persona pueda responder a una pregunta. Cuando ves una confirmación en un espacio —aprobar este push, aprobar este borrado—, la planteó el motor de barandillas (o la política de comandos del arnés) antes de que la acción llegara siquiera al runtime.

Este orden es deliberado. Una aprobación que ocurriera después de la llamada al sistema sería un registro de auditoría, no un control. También es la razón de que la regla de fallo cerrado viva en las barandillas: sin ningún aprobador conectado, la decisión se toma antes de que se ejecute nada y es una denegación.

Diagnosticar un agente es el único sitio donde se indica qué se puede comprobar a mano.

  • Rigs y recintos: el escalón por encima del sandbox, para cuando el trabajo necesita una máquina desechable completa con una frontera del kernel, y no un proceso envuelto
  • Barandillas: la capa de políticas por encima de la base del sistema operativo y el origen real de las aprobaciones
  • Ciclo de vida del despacho de un agente: dónde encaja el aislamiento en el flujo de despacho
  • Modos: cómo un modo estrecha la superficie de herramientas y la política de comandos
  • Backends de sandbox: los backends y su configuración