Claude Code 2.1.251 : quand le contrôle de permission arrivait trop tard

La plus grosse version du mois corrige une famille entière de contournements : un lien symbolique échangé après le feu vert, des règles deny que Grep ignorait, une affectation shell auto-approuvée. Et elle change en silence CLAUDE_CODE_SUBAGENT_MODEL, dont la doc décrit encore l'ancien comportement.

claude-code changelog securite permissions

Une seule version depuis le dernier digest, la 2.1.251 du 28 août, mais c'est la plus fournie du mois : plus de soixante-dix entrées. Autant dire qu'il faut trier.

Le fil rouge n'est pas écrit dans les notes, il apparaît quand on aligne les correctifs de sécurité : ce n'est pas que Claude Code manquait de contrôles de permission, c'est qu'une poignée d'entre eux s'exécutaient au mauvais moment, ou sur le mauvais chemin. Un feu vert donné sur un fichier, puis l'ouverture d'un autre. Une règle deny écrite pour un répertoire, contournée par un lien symbolique. Une commande shell auto-approuvée pour une raison qui n'a plus rien à voir avec ce qu'elle fait. En dessous, un second fil, plus discret et qui va casser des configurations existantes : plusieurs réglages ne s'appliquent plus depuis un fichier de projet.

Le lien symbolique échangé entre le feu vert et l'ouverture

C'est le correctif le plus important de la version :

Corrigé : les outils de fichiers (Read, Write, Edit) suivaient un lien symbolique échangé à l'intérieur du répertoire de travail après le contrôle de permission, ce qui pouvait lire ou écrire en dehors de l'emplacement approuvé.

Le nom académique de ce défaut, c'est un TOCTOU : time-of-check to time-of-use. Le contrôle a lieu sur un chemin, l'opération a lieu un instant plus tard, et rien ne garantissait qu'entre les deux le chemin désigne toujours la même chose. Il suffisait qu'un processus concurrent remplace ./projet/notes.md par un lien vers ~/.ssh/id_rsa dans cette fenêtre. La permission avait été accordée pour le premier, la lecture portait sur le second.

Ce qui rend l'histoire intéressante, c'est que la doc des permissions a été mise à jour en même temps et va plus loin que le changelog. Elle rappelle d'abord la règle de base sur les liens, celle qui existait déjà : une règle allow ne s'applique que si le lien et sa cible correspondent tous les deux, alors qu'une règle deny s'applique dès que l'un des deux correspond. Puis elle ajoute la nouvelle garantie : « quand un outil ouvre un fichier approuvé, Claude Code vérifie que le chemin résout toujours vers l'emplacement approuvé par le contrôle de permission ».

La page d'erreurs donne le détail que le changelog passe sous silence, et c'est celui qui te servira en pratique : les messages exacts que tu vas voir passer.

Refusing to read <path>: its symlink resolution changed after permission was checked
Refusing to write <path>: it is a symbolic link. Write to the link's target path instead
Refusing to search <path>: it could not be opened

Retiens surtout la deuxième. Écrire sur un lien symbolique est maintenant refusé tout court, il faut viser la cible. Si tu as un dépôt où des fichiers de configuration sont des liens vers un répertoire partagé, montage classique en monorepo ou en dotfiles, tes sessions vont commencer à refuser des écritures qui passaient hier. Ce n'est pas un bug, c'est la correction. La doc recommande de résoudre le lien et de travailler directement sur le chemin réel.

Un quatrième message mérite le coup d'oeil : its permission check expired before it ran (too many concurrent file operations). Le contrôle a désormais une durée de validité, et un essaim de sous-agents qui martèle le disque peut la faire expirer. Si tu vois ça, c'est de la contention, pas une attaque.

Grep et Glob passaient à travers tes règles deny

Deuxième contournement, même famille :

Corrigé : Grep et Glob n'appliquaient pas les règles Read(...) de type deny aux fichiers atteints via un chemin de recherche traversant un lien symbolique.

Autrement dit, tu pouvais avoir Read(~/.ssh/**) en deny, une lecture directe correctement bloquée, et un Grep lancé sur un répertoire qui y menait par un lien qui, lui, remontait le contenu. La doc formule maintenant la règle proprement : « Grep et Glob cherchent dans le répertoire vers lequel l'argument path résout. Claude Code applique les règles Read de type deny à ce répertoire ».

Une limite à connaître, et elle est écrite noir sur blanc dans la même page : Claude Code fait un effort « au mieux » (best-effort) pour appliquer les règles Read aux outils qui lisent des fichiers sans être Read, Grep et Glob compris, ainsi qu'aux mentions @fichier de tes prompts et au contexte que ton IDE partage. Best-effort n'est pas garanti. Si un répertoire ne doit vraiment jamais être lu, la règle deny est nécessaire mais ne doit pas être ton seul rempart : les permissions du système de fichiers restent le contrôle qui, lui, ne dépend pas du bon vouloir d'un outil.

OPTIND=1/0 passait sans rien demander

Le correctif le plus amusant de la version, et celui qui illustre le mieux le thème :

Corrigé : les contrôles de permission Bash auto-approuvaient les commandes qui affectent une expression arithmétique à une variable shell entière (par exemple OPTIND=1/0, RANDOM=2+2) ; elles demandent maintenant une approbation.

La logique d'origine se comprend : une affectation de variable n'exécute rien, donc pas besoin de déranger l'utilisateur. Sauf que dans un shell, l'évaluation arithmétique n'est pas inerte. Elle peut déclencher une division par zéro, et surtout OPTIND et RANDOM sont des variables spéciales du shell : leur écrasement modifie le comportement de getopts et du générateur pseudo-aléatoire pour toute la suite de la session. Un préambule anodin qui passe sans prompt, et le comportement des commandes suivantes n'est plus celui que tu crois.

C'est le troisième digest de suite où l'analyseur Bash se fait resserrer, après la règle plus large que son texte de la 2.1.246. Le motif est toujours le même : une catégorie syntaxique jugée inoffensive dans l'abstrait, et qui ne l'est pas dans un shell réel.

Dans la même zone, la version change la façon dont les fichiers de sortie des commandes Bash sont créés et relus quand la commande tourne dans le bac à sable, pour qu'une commande sandboxée ne puisse plus les rediriger ni les remplacer. Le bac à sable contenait la commande, pas le canal par lequel son résultat revenait.

Deux autres du même acabit, plus courtes. L'outil Workflow lisait un scriptPath situé hors de ce que la session a le droit de lire avant que le contrôle ne s'exécute, et citait au passage son contenu dans le message d'erreur. Et les commandes d'un plugin déclarées dans une entrée de place de marché pouvaient pointer en dehors du répertoire du plugin ; ces chemins sont maintenant rejetés avec une erreur de traversée de répertoire.

Des réglages qui s'appliquaient sans que tu les approuves

Le second fil de la version, et celui qui va casser des choses chez toi. Quatre entrées, une même idée : un fichier de réglages de projet, c'est-à-dire un fichier que n'importe quel contributeur du dépôt peut modifier, avait plus de pouvoir qu'il ne devrait.

  • Le traçage et la journalisation des corps de requête. Les réglages de projet pouvaient activer le traçage bêta détaillé ou la journalisation brute des corps d'API. Sur un dépôt public, c'est une ligne de JSON pour faire écrire tes prompts et tes réponses dans un fichier. Corrigé, et au passage un point de terminaison de traçage de portée inférieure ne contourne plus un collecteur OTLP fixé par les managed settings.
  • ANTHROPIC_CUSTOM_HEADERS depuis les réglages gérés ou de projet exige maintenant une approbation quand il définit un en-tête d'identification, d'organisation, de routage ou de comportement d'API, Authorization et Host en tête. La doc de la variable ne mentionne toujours pas cette approbation, elle décrit seulement le format Nom: Valeur et la validation des caractères.
  • Les réglages gérés côté serveur qui terminent le TLS du bac à sable, routent son trafic vers ton propre proxy, injectent des identifiants ou affaiblissent son isolation demandent désormais une approbation avant de s'appliquer. Le dialogue d'approbation, lui, ne liste plus que ce qui a changé depuis ta dernière validation, ce qui le rend enfin lisible.
  • Et le vrai piège de la version : le bloc env d'un .claude/settings.json de projet ne définit plus CLAUDE_CONFIG_DIR, CLAUDE_CODE_TMPDIR, ni TMPDIR/TMP/TEMP. Il faut les poser dans ton shell, tes réglages utilisateur ou les réglages gérés. Si ton dépôt s'appuie là-dessus pour isoler un répertoire de configuration ou de fichiers temporaires, ça ne marche plus, et rien ne te le dira au démarrage : tu retomberas simplement sur les valeurs par défaut. Va vérifier tes settings.json de projet aujourd'hui.

Deux ajustements d'ergonomie complètent le lot, et ils règlent un problème réel : les suggestions d'installation de plugin ou de LSP et l'offre de passage en mode auto attendent maintenant que tu aies envoyé ou effacé ce que tu es en train de taper, pour que la touche entrée qui envoie ton prompt ne réponde pas à leur place. Même correctif côté sessions non surveillées, où l'offre « faire du mode auto ton défaut » n'apparaît plus du tout.

CLAUDE_CODE_SUBAGENT_MODEL ne fait plus la loi

Changement de comportement à connaître, parce qu'il est silencieux et que la doc n'a pas suivi :

Changé : CLAUDE_CODE_SUBAGENT_MODEL définit maintenant le modèle de sous-agent par défaut plutôt que de tout écraser : le model: d'une définition d'agent et un modèle explicite au lancement ont désormais la priorité.

Le sens de cette variable s'inverse. Avant, elle imposait son modèle à tous les sous-agents, quoi qu'en dise leur définition. Maintenant, elle ne sert que de valeur de repli. Si tu l'as posée dans ton environnement pour forcer toute ta flotte de sous-agents sur un modèle plus économique, tes agents qui portent un model: explicite dans leur frontmatter viennent de reprendre leur propre modèle. Selon ce qu'il y a écrit dedans, ta facture a changé hier sans prévenir.

Le piège, c'est que la doc des sous-agents décrit toujours l'ancien ordre, et le décrit très explicitement : elle liste la résolution du modèle avec CLAUDE_CODE_SUBAGENT_MODEL en position 1, devant le paramètre par invocation, puis le frontmatter, puis le modèle de la conversation principale. Le changelog dit l'inverse pour les deux premières positions. C'est le changelog qui a raison, il date d'hier. Ne te fie pas au tableau de la doc tant qu'il n'a pas été corrigé, vérifie plutôt sur une invocation réelle.

Le reste, en vrac

  • Deux nouveaux hooks, PreModelSwitch et PostModelSwitch, qui permettent de bloquer, confirmer ou annoter un changement de modèle. Les hooks SessionStart de reprise reçoivent en plus l'obsolescence de la session et le coût estimé de remise en cache. Attention, ces trois nouveautés sont absentes de la doc des hooks au moment où on écrit : le format exact de leur entrée n'est pas publié, il faudra le découvrir à l'usage.
  • /cost gagne une ligne de cache de prompt par session : taux de succès, échecs, tokens remis en cache, chaud ou froid. Et un objet prompt_cache correspondant pour les scripts de ligne de statut. C'est la suite logique des trois correctifs de cache de la 2.1.248 : après avoir bouché les fuites, on te donne enfin de quoi les voir.
  • /usage gagne une barre de limite de dépense, avec un champ rate_limits.spend_limit pour la ligne de statut, pour ceux qui passent par une passerelle Claude apps.
  • /effort mémorise ton niveau par modèle, chacun garde donc son réglage quand tu bascules. Et un correctif lié : les requêtes Opus 5 échouaient avec « effort … is not supported when thinking is disabled » quand l'effort était xhigh ou max et la réflexion coupée ; l'effort est maintenant envoyé en high dans ce cas.
  • Le modèle par défaut des abonnements Enterprise par siège passe à Opus 5, comme les autres formules premium.
  • Sous-agents et sessions d'arrière-plan : les appels d'outils d'un sous-agent au premier plan sont diffusés en direct vers les clients Remote Control (les sous-agents d'arrière-plan, qui sont le défaut, restent en statut seul). La latence de l'interface avec beaucoup de sous-agents en parallèle est corrigée, les tics de progression se remplacent au lieu de s'empiler. La réponse finale d'un coéquipier arrive enfin au chef d'équipe. Et les sessions d'arrière-plan peuvent de nouveau éditer les fichiers d'un worktree qu'elles ont créé elles-mêmes.
  • claude --help documente enfin attach, logs, stop, respawn et rm, et le message de --resume sur une session d'arrière-plan en cours nomme la commande claude attach <id> exacte.
  • Poids et vitesse : le binaire natif perd environ 5 Mo, plus 2,5 Mo en retirant la coloration syntaxique de six langages rares (1c, gml, isbl, mathematica, maxima, sqf). Le processeur souffle un peu grâce à moins de re-rendus inutiles de l'interface.
  • Divers : les conversations ne se bloquent plus sur « text content blocks must be non-empty » après un tour de réflexion pure. Les transcripts ne s'écrasent plus en silence quand un changement de répertoire relocalise une session sur un transcript de même identifiant. --input-format stream-json ne fusionne plus les appels d'outils injectés sans identifiant de message. Le badge PR du pied de page interroge directement l'API GitHub sur Bedrock, Vertex et Foundry. Et /radio devient disponible sur ces mêmes plateformes.

Ce qu'il faut en retenir

Mets-toi à jour, et pas pour les nouveautés. La série de contournements corrigés ici partage un même profil : chacun rendait inopérant un contrôle que tu croyais actif, en particulier tes règles deny, celles-là même qu'on écrit pour les fichiers auxquels on tient. Une version de rattrapage de permissions ne se remarque pas à l'usage, c'est exactement pour ça qu'on la repousse.

Puis prends dix minutes pour deux vérifications concrètes. Ouvre les .claude/settings.json de tes projets et regarde si leur bloc env posait TMPDIR ou CLAUDE_CONFIG_DIR : ces lignes sont mortes depuis hier, en silence. Et si tu utilises CLAUDE_CODE_SUBAGENT_MODEL pour contenir tes coûts, relis le model: de tes définitions d'agents, parce que ce sont eux qui décident maintenant.

Enfin, si tu écris des hooks, garde un oeil sur PreModelSwitch. Pouvoir bloquer une bascule de modèle est le premier point de contrôle sérieux sur ce que consomme réellement une session. Mais tant que la doc ne l'a pas décrit, considère son format comme instable.

Pierre Rondeau

Pierre Rondeau

Développeur et indie builder. Je construis des produits et automatisations avec l'IA. Créateur de Claude Hub.

LinkedIn