¿Quieres que Hermes edite tu config SSH? Ahora pregunta primero


Le pides a Hermes que añada un servidor nuevo a tu config SSH — un alias de host, un bastión con ProxyJump, para que ssh work conecte en una sola línea de ahora en adelante. write_file cierra la puerta de golpe: “Write denied: ~/.ssh/config is a protected system/credential file.” Así que cambias a la herramienta terminal y haces printf de las mismas líneas en el mismo archivo — y solo aparece un diálogo de confirmación, haces clic en aprobar, listo. El mismo archivo, dos herramientas, dos reglamentos contradictorios. ¿Cuál es el correcto? El PR #84663, fusionado el 12 de agosto, lo zanja: la config del cliente SSH ya no es una zona de “ni lo pienses” — pasó a un approval gate que te pregunta primero.

La causa raíz: un archivo, dos reglas en conflicto

La contradicción venía de dos capas de seguridad independientes:

  • write_file / patch consultan la lista de denegados de plano en agent/file_safety.py. ~/.ssh/config solía chocar tanto con la denegación por ruta exacta como con la denegación por prefijo ~/.ssh/, así que se rechazaba sin más, sin margen de negociación.
  • terminal pasa por la lógica de aprobación de comandos peligrosos en tools/approval.py. Escribir en ~/.ssh solo se marcaba como “requiere aprobación” — la apruebas y la escritura pasa.

Así que el mismo ~/.ssh/config era un callejón sin salida a través de las herramientas de archivos y una confirmación de un clic a través de terminal. Esa incoherencia confundía a los usuarios — y el propio agente reportaba primero “write failed” y luego “succeeded” por la otra vía, dejando un rastro de registros contradictorios.

La solución: ~/.ssh/config baja de denegado de plano a approval gate

El razonamiento del PR #84663: la config del cliente SSH no es material de credenciales. No contiene bytes de claves privadas, y editarla (alias de host, ProxyJump, destinos de VS Code Remote-SSH) es una tarea rutinaria iniciada por el usuario — una negativa rotunda es incorrecta. Pero puede llevar directivas ProxyCommand / Match exec que ejecutan comandos, así que una escritura libre en silencio es igualmente incorrecta. La aprobación es la política correcta — en línea con lo que la herramienta terminal ya hacía para las escrituras en ~/.ssh.

En concreto, el PR #84663 hace lo siguiente:

  • agent/file_safety.py: ~/.ssh/config sale de la denegación plana de credenciales; los nuevos build_write_approval_paths() e is_write_approval_required() excluyen por cortocircuito las rutas con approval gate de la denegación por prefijo ~/.ssh/ en _classify_write_denial.
  • Las claves privadas y los archivos de autenticación siguen denegados de plano: id_rsa, id_ed25519, authorized_keys y todo lo demás bajo ~/.ssh/ — sin excepciones.
  • tools/file_tools.py: write_file y patch ahora enrutan las escrituras de la config SSH por el _run_approval_gate compartido, justo después del gate de instrucciones protegidas.
  • Los llamadores no interactivos fallan cerrados (fail-closed): el puente de archivos ACP (agent/copilot_acp_client.py) rechaza de plano las rutas que requieren aprobación; el selector de rutas de salida de TTS (tools/tts_tool.py) también las rechaza.

El modelo de niveles de seguridad de archivos de Hermes

Este cambio también saca a la luz el modelo completo de seguridad de escritura, que tiene tres niveles que merece la pena recordar:

Nivel 1: denegado de plano — ni lo pienses. Las credenciales y los archivos críticos del sistema no se pueden escribir con ninguna herramienta en ningún modo, yolo incluido: ~/.ssh/authorized_keys, id_rsa, id_ed25519, .env, .anthropic_oauth.json, .netrc, .pgpass, .npmrc, .pypirc, .git-credentials, /etc/sudoers, /etc/passwd, /etc/shadow, además de prefijos de directorio como ~/.ssh/, ~/.aws/, ~/.gnupg/, ~/.kube/, ~/.docker/, ~/.config/gh/. Este es el piso que nunca se mueve.

Nivel 2: approval gate — pregunta primero. Actualmente con un solo miembro: ~/.ssh/config. Se puede escribir, pero solo a través de un prompt de aprobación humana, porque puede colar directivas que ejecutan comandos. El gate ofrece tres alcances de persistencia: once (solo esta vez), session (recordarlo durante esta sesión), always (recordarlo para siempre).

Nivel 3: escritura libre — todo lo demás. Los archivos de trabajo normales y el código de los proyectos, el agente puede escribirlos libremente.

Cómo se comporta realmente el approval gate

Leyendo el gate compartido (_run_approval_gate en tools/approval.py), el orden de decisión es:

  1. --yolo pasa primero: el modo yolo (a nivel de proceso, HERMES_YOLO_MODE, o a nivel de sesión) pasa de largo — pero las denegaciones de plano del nivel 1 se comprueban antes del gate, así que yolo tampoco puede tocarlas;
  2. El caché de sesión corta camino: una escritura aprobada previamente (alcance session/always) pasa en silencio;
  3. Rama interactiva / gateway / cron: las sesiones interactivas reciben un prompt; las sesiones cron siguen approvals.cron_mode (por defecto: denegar);
  4. Fail-closed sin canal humano: sin terminal interactiva ni sesión de gateway (p. ej. scripts en segundo plano, llamadas ACP) → BLOQUEADO, sin más. La denegación incluso le dice al agente que no reintente vía terminal ni execute_code salvo que el usuario lo consienta explícitamente.

Resumen

Mover ~/.ssh/config de denegado de plano a approval gate consiste en realidad en devolver la decisión de “la herramienta decide” a “tú decides”: las ediciones rutinarias ya no se bloquean de plano, mientras que las escrituras que llevan comandos siempre pasan por un gate humano. Para saber más sobre la maquinaria de aprobación de Hermes, consulta nuestra guía de Smart Approvals de tres gates y cómo se resuelve la fatiga de aprobaciones; los modos --yolo y las líneas rojas se cubren en este post; los comandos de configuración viven en la página de referencia de hermes-config.