Vous voulez qu'Hermes modifie votre config SSH ? Il demande désormais d'abord


Vous demandez à Hermes d’ajouter un nouveau serveur à votre config SSH — un alias d’hôte, un bastion ProxyJump, pour que ssh work se connecte en une seule ligne à partir de maintenant. write_file claque la porte : « Écriture refusée : ~/.ssh/config est un fichier système/d’identifiants protégé. » Vous basculez alors sur l’outil terminal et printf les mêmes lignes dans le même fichier — et là, une simple boîte de confirmation apparaît : vous cliquez sur approuver, terminé. Même fichier, deux outils, deux règlements contradictoires. Lequel a raison ? La PR #84663, fusionnée le 12 août, tranche : la configuration du client SSH n’est plus une zone « n’y pense même pas » — elle passe sous un approval gate qui vous demande d’abord.

La cause racine : un fichier, deux règles contradictoires

La contradiction venait de deux couches de sécurité indépendantes :

  • write_file / patch vérifient la liste des refus catégoriques dans agent/file_safety.py. ~/.ssh/config se heurtait autrefois au refus sur le chemin exact comme au refus sur le préfixe ~/.ssh/ : il était donc rejeté d’office, sans aucune possibilité de négociation.
  • terminal passe par la logique d’approbation des commandes dangereuses dans tools/approval.py. Écrire dans ~/.ssh était seulement signalé comme « nécessite une approbation » — approuvez et l’écriture passe.

Ainsi, le même ~/.ssh/config était une impasse côté outils de fichiers et une confirmation en un clic côté terminal. Ce grand écart déroutait les utilisateurs — et l’agent lui-même rapportait d’abord « écriture échouée », puis « réussie » via l’autre chemin, laissant derrière lui une traînée de journaux contradictoires.

Le correctif : ~/.ssh/config rétrogradé de refus catégorique à approbation requise

Le raisonnement de la PR #84663 : la configuration client SSH n’est pas un matériel d’identification. Elle ne contient aucun octet de clé privée, et l’éditer (alias d’hôtes, ProxyJump, cibles VS Code Remote-SSH) est une tâche courante, initiée par l’utilisateur — un refus sec serait une erreur. Mais elle peut transporter des directives ProxyCommand / Match exec qui exécutent des commandes : une écriture libre et silencieuse serait tout aussi erronée. L’approbation est la bonne politique — en cohérence avec ce que l’outil terminal faisait déjà pour les écritures dans ~/.ssh.

Côté code, sur origin/main :

  • agent/file_safety.py : ~/.ssh/config retiré du refus d’identifiants plat ; les nouveaux build_write_approval_paths() et is_write_approval_required() soustraient les chemins soumis à approbation au refus sur le préfixe ~/.ssh/ dans _classify_write_denial.
  • Les clés privées et fichiers d’authentification restent intouchables : id_rsa, id_ed25519, authorized_keys, et tout le reste sous ~/.ssh/ — aucune exception.
  • tools/file_tools.py : write_file et patch routent désormais les écritures de config SSH via le _run_approval_gate partagé, juste après la porte de protection contre les instructions.
  • Les appelants non interactifs échouent en fail-closed : le pont de fichiers ACP (agent/copilot_acp_client.py) rejette d’office les chemins soumis à approbation ; le sélecteur de chemin de sortie TTS (tools/tts_tool.py) les refuse également.

Le modèle de sécurité par paliers des fichiers d’Hermes

Ce changement révèle aussi l’ensemble du modèle de sécurité des écritures, qui compte trois paliers à retenir :

Palier 1 : refus catégorique — n’y pense même pas. Les identifiants et les fichiers système critiques ne sont inscriptibles par aucun outil, quel que soit le mode, yolo compris : ~/.ssh/authorized_keys, id_rsa, id_ed25519, .env, .anthropic_oauth.json, .netrc, .pgpass, .npmrc, .pypirc, .git-credentials, /etc/sudoers, /etc/passwd, /etc/shadow, ainsi que les préfixes de répertoires comme ~/.ssh/, ~/.aws/, ~/.gnupg/, ~/.kube/, ~/.docker/, ~/.config/gh/. C’est le plancher qui ne bouge jamais.

Palier 2 : soumis à approbation — demandez-moi d’abord. Un seul membre pour l’instant : ~/.ssh/config. Inscriptible, mais uniquement via une invite d’approbation humaine, car il peut faire passer en contrebande des directives exécutant des commandes. La porte propose trois portées de persistance : once (cette fois-ci), session (mémorisé pour la session), always (mémorisé pour toujours).

Palier 3 : écriture libre — tout le reste. Les fichiers de travail ordinaires et le code des projets : l’agent peut écrire librement.

Comment se comporte réellement l’approval gate

En lisant la porte partagée (_run_approval_gate dans tools/approval.py), l’ordre de décision est le suivant :

  1. --yolo passe en premier : le mode yolo (au niveau du processus, HERMES_YOLO_MODE, ou au niveau de la session) va droit au but — mais les refus catégoriques du palier 1 sont vérifiés avant la porte : yolo ne peut donc toujours pas y toucher ;
  2. Cache de session en court-circuit : une écriture déjà approuvée (portée session/always) passe en silence ;
  3. Branche interactive / gateway / cron : les sessions interactives reçoivent une invite ; les sessions cron suivent approvals.cron_mode (par défaut : refus) ;
  4. Fail-closed sans canal humain : ni terminal interactif, ni session gateway (scripts d’arrière-plan, appels ACP par exemple) → BLOQUÉ, sans appel. Le refus précise même à l’agent de ne pas réessayer via terminal ou execute_code, sauf consentement explicite de l’utilisateur.

Pour conclure

Déplacer ~/.ssh/config du refus catégorique vers l’approbation requise, c’est avant tout rendre la décision à l’humain : les modifications courantes ne sont plus bloquées d’office, tandis que les écritures capables d’exécuter des commandes passent toujours par une porte humaine. Pour en savoir plus sur la machinerie d’approbation d’Hermes, consultez notre guide Smart Approvals à trois portes et la façon dont la fatigue d’approbation se résout ; les modes --yolo et les lignes rouges sont détaillés dans cet article ; les commandes de configuration figurent sur la page de référence hermes-config.