Sommaire du guide
Autres ressources

Configuration inspectée

Configurer Claude Code
et Codex

Architecture d'une configuration d'agent partagée entre 2 hôtes : pipeline de releases, 4 couches d'exécution, routage lexical des skills.

Inspection du 16 septembre 2026 · Extraits relus · Comptes historiques non revérifiés

À retenir

Consulter le glossaire

Les termes en pointillés donnent leur définition au survol, au clavier ou au toucher.

I

Vue d'ensemble

01

Une source unique alimente Claude Code et Codex

Le dépôt ai-agents est l'autorité. Les fichiers que lisent les agents sont des artefacts rendus, jamais des fichiers édités à la main.

La configuration n'est pas un ensemble de fichiers posés dans ~/.claude. C'est un pipeline de build. Les sources d'écriture vivent dans ~/.config/ai-agents/src/ et aucun client ne les lit. Un script de rendu en produit une release immuable, et ce sont les artefacts de cette release qui sont installés dans le home.

Cette indirection donne aux 2 agents une configuration cohérente par construction. Éditer ~/.claude/CLAUDE.md à la main marcherait, mais laisserait ~/.codex/AGENTS.md en arrière, et produirait 2 agents au jugement différent sur le même code.

Le rendu produit une release immuable, que l'installation projette vers Claude Code et Codex. Le diagramme suivant détaille chaque fichier installé.
DIAGRAMME · Du dépôt source aux 2 agents
Le rendu produit une release immuable ; l'installation projette ses artefacts vers les 2 hôtes.
Le runtime n'a aucune dépendance npm Le pipeline ne fait aucun appel réseau et n'installe aucun paquet. Node 22 ou plus récent suffit. La contrainte est volontaire, parce qu'une configuration d'agent qui dépend d'un registre distant devient indisponible au moment où elle est le plus utile.
02

Instructions et skills sont suggérées, hooks et permissions sont imposés

Instructions, skills, hooks et permissions ne sont pas interchangeables. Le critère de choix est le niveau de garantie exigé.

L'erreur la plus fréquente est de mettre dans un fichier d'instructions une règle qui doit être garantie. Le modèle la lira, la respectera souvent, et l'oubliera parfois. Si l'oubli coûte cher, c'est un hook, pas une phrase.

La 2e erreur est d'accumuler dans le fichier d'instructions. Il reste dans le contexte de chaque session, sur chaque projet. Chaque ligne se paie en contexte sur toute la durée d'usage de la machine. Une procédure longue réservée à une tâche précise appartient à une skill : seule sa description pèse tant que le corps n'est pas chargé.

Les 2 couches blanches sont suggérées au modèle, les 2 couches orange lui sont imposées. Le critère de choix est le niveau de garantie exigé, jamais la longueur du texte à écrire.
DIAGRAMME · Où placer une nouvelle règle
Arbre de décision pour choisir la couche d'une règle nouvelle.
Les 4 couches et leur coût
CoucheFichierStatut d'exécutionCoût de contexte
InstructionsCLAUDE.md, AGENTS.md, rules/Toujours en contextePermanent, à chaque tour
Skillsskills/<nom>/SKILL.mdChargée à la demandeLa description seule, puis le corps si chargée
Hookssettings.json, hooks.jsonCode exécuté par le harnessNul, hors sortie injectée
Permissionssettings.jsonFiltre avant exécutionNul
Une instruction n'est pas une garantie Un fichier d'instructions est une guidance que le modèle pondère avec le reste du contexte. Un hook est un gate qu'il ne peut pas contourner sur les actions que ce hook intercepte ; une action prise par un autre chemin lui échappe. Confondre les deux produit une fausse sécurité, parce que la règle est écrite, paraît appliquée, et saute au moment précis où le contexte est saturé.
II

Le pipeline et les couches

03

Une release est identifiée par le SHA-256 de son manifeste

Le rendu produit un répertoire immuable. Son identifiant est le condensat de son manifeste normalisé, pas un numéro de version.

scripts/render.mjs lit src/, produit un répertoire sous releases/, et le nomme d'après le SHA-256 de son manifeste normalisé. 2 rendus du même contenu produisent le même identifiant ; un octet de différence produit un répertoire différent. Le pointeur current est un lien symbolique vers la release active.

Le champ sourceCommit n'est que la référence Git de base. Le manifeste lie séparément le contenu réel par sourceProvenance.contentDigest et chaque octet rendu par manifest.artifacts. Cette distinction compte, parce qu'un rendu produit depuis un arbre de travail modifié reste tracé, alors qu'un simple hash de commit mentirait.

JSON Extrait de artifact-manifest.json 16 lignes
{
  "schemaVersion": 1,
  "releaseId": "f4528d92…e657847",
  "sourceCommit": "27cb7932…866d0e6",
  "sourceProvenance": {
    "commitRole": "base-reference-not-content-proof",
    "contentBinding": "manifest.sources",
    "contentDigest": "f5e7ea7a…8c5a0067",
    "releaseBinding": "manifest.artifacts"
  },
  "requirements": { "node": ">=22" },
  "artifacts": {
    "claude/CLAUDE.md": { "hash": "8f9790e7…", "mode": 420, "type": "file" },
    "codex/AGENTS.md":  { "hash": "…", "mode": 420, "type": "file" }
  }
}
Ce que contient une release
ArtefactDestination installée
claude/CLAUDE.md~/.claude/CLAUDE.md, rendu à plat, sans import @ mutable
codex/AGENTS.md~/.codex/AGENTS.md
claude/output-styles/flow-lean.md~/.claude/output-styles/ et sélection dans settings.json
reference/ANTI_AI.mdRéférence éditoriale, chargée à la demande seulement
hooks/Adaptateurs anti-marqueurs, routeur BM25, checkpoint Git
skills/common/95 skills normalisées
skills/projections/claudeCopiée vers ~/.claude/skills/
skills/projections/codexCible du lien ~/.agents/skills
artifact-manifest.jsonEmpreinte de chaque octet rendu

L'installation utilise un verrou exclusif, compare les préimages exactes des fichiers vivants, écrit atomiquement cible par cible, et journalise durablement. Une préimage périmée invalide l'approbation : si un fichier a changé entre la validation et l'écriture, la transaction s'arrête. Le rollback ne restaure qu'une cible qui correspond encore à sa postimage enregistrée.

UNKNOWN n'est pas un succès Toute divergence inexpliquée arrête l'installation avec le code de sortie 6. C'est la bonne posture pour un outil qui écrit dans le home, parce que mieux vaut un échec lisible qu'un état partiellement appliqué dont personne ne connaît la forme.
Vérifier l'état d'installation sans rien modifier
Terminal Terminal 8 lignes
# quelle release est active
readlink ~/.config/ai-agents/current

# dérive entre la copie Claude vivante et la projection
node ~/.config/ai-agents/scripts/check.mjs --home

# état des deux racines de skills
node ~/.config/ai-agents/scripts/inventory.mjs

check.mjs --home signale LIVE_CLAUDE_SKILLS_DRIFT quand la copie réelle diffère de la projection active, et LIVE_CODEX_SKILLS_DRIFT quand le lien Codex manque ou pointe ailleurs. Le vérificateur signale, il ne répare jamais. Séparer diagnostic et réparation évite qu'une réparation automatique masque la cause.

04

Claude reçoit une copie réelle, Codex un lien symbolique

L'asymétrie n'est pas un choix esthétique. Elle contourne 2 bugs documentés de Claude Code.

Les 2 hôtes ne lisent pas les skills au même endroit. Claude Code lit ~/.claude/skills, Codex lit ~/.agents/skills puis ~/.codex/skills. La release fournit une projection par hôte, mais elles ne sont pas installées de la même façon.

Racines de skills et mode d'installation
HôteRacine lueModeRaison
Claude Code~/.claude/skills/Copie réelle, remplacée atomiquementUne racine en lien symbolique peut être ignorée, et l'auto-update peut supprimer le lien
Codex~/.agents/skillsLien symbolique vers la projectionAucun bug équivalent constaté
La projection des skills alimente les 2 racines, installées de 2 façons différentes.

Chacun des 2 bugs de Claude Code a sa source : issue 38051 pour la racine en lien symbolique ignorée, issue 50052 pour la suppression du lien à l'auto-update. La copie réelle coûte une duplication sur disque et un risque de dérive ; c'est le prix d'une racine qui survit à une mise à jour.

Dans un dépôt, le même motif s'applique en miroir Pour un projet, garder les skills éditables dans .agents/skills et exposer le même arbre à Claude par .claude/skills -> ../.agents/skills. Une seule copie éditable, 2 hôtes qui la lisent. L'inverse, 2 répertoires édités séparément, diverge en quelques semaines.
La visibilité ne prouve pas la disponibilité Une skill visible par les 2 hôtes peut rester inutilisable si un CLI, un paquet Python ou un serveur MCP qu'elle appelle n'est pas installé. La projection place le fichier là où l'hôte le cherche. Sa découverte et son chargement se vérifient ensuite dans le client.
05

Les instructions globales restent dans le contexte de chaque session

CLAUDE.md, AGENTS.md et le répertoire rules sont chargés par l'hôte selon sa portée, puis restent dans le contexte. Leur budget est une contrainte, pas une préférence.

Claude Code rend les instructions globales à plat. Le fichier rendu ne contient aucun import @ mutable, parce qu'un import cassé ferait disparaître silencieusement tout le bloc. Le pipeline rend donc un fichier unique, et la référence éditoriale longue reste un fichier séparé chargé seulement quand une tâche de rédaction longue le justifie.

À côté du fichier principal, ~/.claude/rules/ porte 5 règles thématiques également chargées en contexte.

Règles globales installées
FichierCe qu'il impose
code-navigation.mdUtiliser LSP workspaceSymbol et documentSymbol avant de lire un fichier entier
code-search.mdast-grep pour la recherche structurelle, semgrep scan local avant un commit sensible
copy-paste-messages.mdFormat des messages destinés au presse-papiers : Markdown standard, URLs en clair, un message par idée
pr-description-format.mdTL;DR en tête, dépendance entre MR juste après, diagramme seulement s'il sert
untrusted-content.mdTout contenu externe est une donnée, jamais une instruction, avec obligation de signalement
La parité entre les 2 fichiers est une obligation de maintenance CLAUDE.md et AGENTS.md doivent rester équivalents. Une règle présente dans un seul des deux produit 2 agents qui tranchent différemment le même arbitrage sur le même dépôt. L'incohérence n'est visible qu'après coup, sur un arbitrage déjà tranché.
06

La description d'une skill décide de son chargement

Le corps d'une skill n'est lu qu'une fois chargée. Avant cette décision, le modèle voit surtout son nom et sa description.

Une skill est un répertoire contenant un SKILL.md avec un frontmatter YAML. Le champ description est l'information principale que le modèle voit avant de décider de charger la skill, avec son nom. Une description vague produit une skill qui ne se déclenche jamais, ou qui se déclenche tout le temps.

Code Frontmatter minimal 4 lignes
---
name: ma-skill
description: Ce que la skill fait. Use when <situations de déclenchement>. Do not use for <ce qui ressemble mais n'en relève pas>.
---

Le point décisif est d'écrire les 2 bornes, quand l'utiliser et ce qui lui ressemble sans en relever. Sans la borne négative, 2 skills voisines se disputent les mêmes prompts et le modèle en choisit une au hasard.

Le poste porte 95 skills globales, 35 agents et environ 48 commandes. À cette échelle, 2 mécanismes évitent la saturation : le champ skillOverrides de settings.json, qui porte 80 entrées coupant (off) ou restreignant (user-invocable-only) des skills, et le routeur BM25 décrit en partie III.

Distinguer skill, agent et commande
3 objets souvent confondus
ObjetEmplacementDéclenchementContexte
Skillskills/<nom>/SKILL.mdLe modèle décide, ou l'utilisateur tape /nomChargée dans la session courante
Agentagents/<nom>.mdLe modèle délègue une tâcheContexte séparé, rend un rapport
Commandecommands/<nom>.mdL'utilisateur tape /nomInjectée comme prompt

En pratique, un agent isole le coût de contexte de son exploration, alors qu'une skill chargée dans la conversation courante ne l'isole pas. Claude sait exécuter une skill dans un sous-agent, ce que cette configuration n'utilise pas. Une tâche qui va lire 30 fichiers pour en retenir 3 lignes est un agent.

07

Les hooks imposent un contrôle là où ils sont câblés

Un hook est du code exécuté par le harness à un événement du cycle de vie. Le modèle ne choisit pas de l'éviter, mais la couverture s'arrête aux appels d'outils que son matcher reconnaît.

Un hook reçoit un JSON sur son entrée standard et décide. Sur PreToolUse, un code de sortie 2 bloque l'appel et renvoie le message au modèle pour qu'il corrige, sans faire échouer la session. L'effet de ce code dépend de l'événement : sur UserPromptSubmit il refuse le prompt, et sur PostToolUse l'outil a déjà tourné. Ce mode est préférable, parce qu'un hook qui casse une session finit désactivé, et ne vaut alors plus rien.

DIAGRAMME · Cycle de vie d'un prompt et points d'accroche
Les hooks s'intercalent entre l'utilisateur, le modèle et les outils.
Hooks câblés dans settings.json
ÉvénementHookRôle
UserPromptSubmitsmart-suggest.shSuggestion de commandes et d'agents
UserPromptSubmitskill-router/bm25-suggest.jsSuggestion de skills par score BM25
PreToolUse:Bashblock-env-reads.shRefuse la lecture de fichiers d'environnement
PreToolUse:Bashlint-commit-message.shContrôle le message de commit avant exécution
PreToolUse:Bashrtk hook claudeIntégration de l'outillage CLI
PreToolUse:Readblock-private-files.shRefuse la lecture de fichiers privés
PreToolUse:Edit|Writeclaude-adapter.shContrôles anti-marqueurs sur le texte écrit
PreToolUse:Agentmodel-usage-tracker.shTraçabilité des sous-agents lancés
PreToolUse:* et PostToolUse:*git-ai checkpointCheckpoint silencieux de l'arbre de travail
PostToolUse:Bash|Readmask-credentials.shTransforme la sortie reçue avant de la réémettre
SessionStartrtk-baseline.shCapture l'état de départ
SessionEndauto-rename-session.sh, session-summary.shRenommage et résumé de session
Codex ajoute une confiance cryptographique par hook ~/.codex/config.toml stocke un bloc [hooks.state."<fichier>:<événement>:<i>:<j>"] avec un trusted_hash pour chaque hook déclaré. Modifier le script invalide le hash et le hook reste inactif jusqu'à réapprobation explicite. C'est la protection contre la modification silencieuse d'un hook, par un tiers ou par un agent.
Le câblage Codex est séparé de sa configuration Codex lit le câblage dans ~/.codex/hooks.json, mais garde l'état de confiance dans ~/.codex/config.toml. Modifier l'un sans l'autre produit un hook déclaré et jamais exécuté, sans message d'erreur.
JSON Câblage dans ~/.claude/settings.json 23 lignes
"PreToolUse": [
  {
    "matcher": "Edit|Write",
    "hooks": [
      {
        "type": "command",
        "command": "~/.config/ai-agents/releases/d16f4a6c…866bfc84/hooks/claude-adapter.sh",
        "timeout": 5
      }
    ]
  }
],
"UserPromptSubmit": [
  {
    "hooks": [
      {
        "type": "command",
        "command": "env SKILL_ROUTER_HOST=claude SKILL_ROUTER_DATA_DIR=\"~/.local/state/ai-agents/skill-router/claude\" node \"~/.config/ai-agents/releases/d16f4a6c…866bfc84/hooks/skill-router/bm25-suggest.js\"",
        "timeout": 5
      }
    ]
  }
]
Le masquage des sorties n'est pas prouvé par l'installation mask-credentials.sh lit le JSON de l'événement, le transforme avec jq et le réémet. Il ne construit pas le champ hookSpecificOutput.updatedToolOutput, seul moyen documenté de remplacer la sortie d'un outil. Sa présence dans settings.json montre donc une intention, pas un remplacement effectif de la sortie avant qu'elle atteigne le modèle. La vérification demande une sortie factice en session réelle, qui n'a pas été faite.
08

3 barrières indépendantes filtrent avant exécution

deny, ask et sandbox opèrent à des niveaux différents. Aucune ne remplace les deux autres.

Les 3 mécanismes ne se recouvrent pas. deny refuse catégoriquement un motif de commande ou de chemin. ask interrompt pour demander confirmation. La sandbox est une isolation au niveau du système d'exploitation, appliquée à chaque commande shell prise séparément.

Ce que couvre chaque barrière
BarrièreNiveauContenu installé
permissions.denyMotif de commande ou de cheminFichiers d'environnement, *.pem, *.key, ~/.ssh, ~/.aws, git push --force, git reset --hard, suppression de dépôt distant
permissions.askConfirmation humainePush, fusion de MR ou PR, mise en ligne de paquet, déploiement en production
sandbox.networkEgress réseauListe blanche de 44 domaines, tout le reste refusé
sandbox.filesystemÉcriture disqueListe blanche de 22 répertoires d'écriture et 2 entrées de lecture
sandbox.credentialsSecrets5 chemins et 13 variables d'API refusés aux commandes
sandbox.excludedCommandsExceptions95 entrées d'exclusion au 22 septembre 2026, pour les commandes qui ont besoin du réseau ou de credentials
Les 3 barrières opèrent à des niveaux différents et ne se remplacent pas. Leur périmètre commun s'arrête à l'agent.

La liste excludedCommands sort des commandes de l'isolation, elle mérite donc une lecture attentive. C'est nécessaire pour gh, docker ou les gestionnaires de paquets, mais chaque entrée est une brèche assumée. Elle doit rester justifiable ligne par ligne.

Ces barrières protègent l'agent, pas un humain Une règle deny empêche l'agent local de lire un fichier. Elle n'empêche rien du tout à une personne qui clone le dépôt. Un dépôt contenant des données client ne se partage pas au motif qu'une configuration locale contient un deny sur ces chemins. Les 2 problèmes n'ont pas le même périmètre et pas la même solution.
JSON Extrait de ~/.claude/settings.json 26 lignes
"permissions": {
  "defaultMode": "auto",
  "deny": [
    "Bash(git push --force *)",
    "Bash(git reset --hard *)",
    "Bash(gh repo delete *)",
    "Read(**/.env)",
    "Read(**/*.pem)",
    "Read(**/.ssh/**)"
  ],
  "ask": [
    "Bash(git push *)",
    "Bash(gh pr merge *)",
    "Bash(npm publish *)",
    "Bash(vercel *--prod*)"
  ]
},
"sandbox": {
  "enabled": true,
  "autoAllowBashIfSandboxed": true,
  "allowUnsandboxedCommands": false,
  "network":     { "allowedDomains": ["<44 domaines>"] },
  "filesystem":  { "allowWrite": ["<22 répertoires>"], "allowRead": ["<2 entrées>"] },
  "credentials": { "files": ["<5 chemins>"], "envVars": ["<13 variables>"] },
  "excludedCommands": ["gh *", "glab *", "docker *", "<92 autres entrées>"]
}
III

Le routeur BM25

09

Le routage lexical évite un appel réseau par prompt

3 approches étaient possibles pour choisir quelles skills signaler. Le coût et la reproductibilité tranchent.

Un agent qui dispose de 95 skills garderait en contexte autant de descriptions, sans compter les corps. Il faut décider, avant chaque tour, lesquelles méritent d'être signalées.

3 approches du routage
ApprocheCoût par promptReproductibleDépendances
Lire les 95 descriptionsContexte constant, non nulNon, la décision se dégrade avec la tailleAucune
EmbeddingsAppel réseau ou modèle localNon entre versions de modèleModèle, réseau ou binaire
BM25 lexicalQuelques millisecondes annoncées sur index en cache, sans mesure jointeOui, à index et contexte identiquesAucune

BM25 a été retenu. Le compromis assumé est que le routeur compare des mots, pas des sens. En échange, il est débuggable, un score étant une somme lisible de contributions par terme.

10

BM25 note la pertinence avec 3 composants

Le score combine fréquence saturée, normalisation par longueur et rareté du terme, et chaque constante répond à un problème précis.

BM25, ou Okapi BM25, est le classement par défaut de Lucene, Elasticsearch et SQLite FTS5. Pour chaque terme q de la requête présent dans le document D, il calcule une contribution, et le score total est la somme sur tous les termes de la requête.

Code Formule 12 lignes
score(q, D) = IDF(q) × ( f(q,D) × (k1 + 1) )
                       ÷ ( f(q,D) + k1 × (1 - b + b × |D|/avgdl) )

IDF(q) = log( 1 + (N - n + 0.5) / (n + 0.5) )

f(q,D)  fréquence du terme q dans le document D
|D|     longueur du document
avgdl   longueur moyenne des documents du corpus
N       nombre total de documents
n       nombre de documents contenant q

Constantes retenues : k1 = 1.2, b = 0.3
Les 3 composants de la formule, et ce que chacun corrige. Le score total est la somme de ces contributions sur les mots de la requête.

`k1 = 1.2`, la saturation. C'est ce qui empêche la linéarité. Un document contenant 10 fois le mot sitemap n'est pas 10 fois plus pertinent qu'un document le contenant une fois. La fonction monte vite, puis plafonne.

Facteur de fréquence pour k1 = 1.2, à longueur de document égale à la moyenne et IDF mis de côté
Fréquence fFacteur de fréquenceGain marginal
11.00référence
21.38+0.38
31.57+0.19
51.77+0.20 sur 2 occurrences
101.96+0.19 sur 5 occurrences
infini2.20plafond théorique

`b = 0.3`, la normalisation par longueur. Sans elle, un document long gagne mécaniquement, parce qu'il contient plus de termes, donc plus de chances de correspondre. Le facteur |D|/avgdl pénalise les documents plus longs que la moyenne. b = 0 désactive la normalisation, b = 1 l'applique à fond.

La valeur 0.3 est basse, volontairement. Le corpus est fait de phrases de 5 à 15 mots, où la longueur ne porte presque pas d'information. Un b proche de 1 pénaliserait injustement une formulation détaillée face à un mot-clé isolé.

Le détail qui compte dans l'IDF

L'IDF, ou inverse document frequency, est le cœur du système. Un terme présent dans tous les documents ne discrimine rien. Un terme rare discrimine beaucoup.

Le 1 + en tête de la formule est le détail qui change le comportement. La forme classique de Robertson et Sparck Jones, sans ce 1 +, devient négative dès qu'un terme apparaît dans plus de la moitié du corpus. Un terme fréquent pénaliserait alors activement les documents qui le contiennent, ce qui n'a aucun sens pour ce cas d'usage. Le plancher à zéro évite ce comportement.

En amont, un tokenizer bilingue prépare le texte : découpage camelCase, minuscules, repli d'accents, retrait d'environ 60 stopwords français et anglais, détection de négation, puis stemming par une liste de 25 suffixes des 2 langues. Les mots outils comme le, la, de ne parviennent donc jamais au calcul.

JavaScript routing/bm25.js, le cœur du score 27 lignes
const K1 = 1.2;
const B = 0.3;

function computeIdf(docs) {
  const N = docs.length;
  const df = new Map();
  for (const doc of docs) {
    for (const t of new Set(doc.tokens)) df.set(t, (df.get(t) || 0) + 1);
  }
  const idf = {};
  for (const [t, n] of df) idf[t] = Math.log(1 + (N - n + 0.5) / (n + 0.5));
  return idf;
}

function scoreDoc(queryTokens, doc, idf, avgdl) {
  const tf = termFreq(doc.tokens);
  const dl = doc.tokens.length || 1;
  let score = 0;
  for (const q of queryTokens) {
    const f = tf.get(q);
    if (!f) continue;
    const num = f * (K1 + 1);
    const den = f + K1 * (1 - B + B * (dl / avgdl));
    score += (idf[q] || 0) * (num / den);
  }
  return score;
}
11

Du score à la suggestion, 4 conditions à franchir

BM25 seul produit un nombre. Le maximum par skill, un seuil calibré, un veto négatif et une sélection d'éligibilité produisent une décision.

Un score brut ne dit pas s'il faut suggérer. C'est là que se joue la qualité du routeur, pas dans la formule.

La chaîne complète, du prompt aux suggestions injectées. Le diagramme suivant montre chaque sortie sans suggestion.
DIAGRAMME · Du prompt à la suggestion
Chaîne de traitement, avec les 4 filtres successifs.
  1. Maximum par cible, jamais la somme

    Le score d'une skill est celui de son meilleur scénario unique. Sommer donnerait mécaniquement l'avantage à une skill de 40 scénarios sur une skill de 10, indépendamment de la pertinence.

  2. Seuil tau calibré par cible

    Chaque skill obtient son propre seuil, calculé à la construction de l'index. Une cible portant moins de 8 scénarios positifs ou moins de 2 négatifs est exclue de la calibration, parce qu'en dessous le seuil serait ajusté sur du bruit. Les positifs sont notés contre les autres positifs de leur cible, en s'excluant eux-mêmes, sinon le score est parfait par construction.

  3. Le seuil se calibre sur les négatifs déclarés de la skill

    La calibration du seuil ne regarde que les scénarios de la skill : ses positifs, notés contre ses autres positifs, et ses négatifs déclarés. La comparaison entre skills voisines arrive après, à l'évaluation croisée.

  4. Balayage de tous les seuils candidats

    Le seuil retenu est celui qui maximise F-beta sur l'ensemble des scores observés, en testant chaque point médian entre 2 scores consécutifs. La skill passe en statut ok si le F1 mesuré à ce seuil atteint 0,60, et reste en conflict sinon.

  5. L'évaluation croisée décide de l'éligibilité

    Une skill calibrée en ok n'est pas encore suggérable. Le constructeur rejoue toutes les skills ensemble, garde celles dont le F1 croisé atteint 0,55, puis retire la moins bonne tant que le F1 global reste sous 0,70. Seules les skills qui survivent à cette passe portent eligible.

L'optimisation est asymétrique. Le F-beta utilisé pose beta = 2, ce qui fait peser le rappel 4 fois la précision. Une suggestion manquée est une skill qui ne sert pas. Une suggestion de trop est une ligne que le modèle ignore. La politique retenue accepte donc des faux positifs pour réduire les suggestions manquées.

DIAGRAMME · Les 3 états d'une cible après calibration
Une cible n'est suggérable que dans l'état ok.
Le veto négatif, et ce qu'il ne couvre pas Un seul terme rare en commun suffit à franchir tau sur un texte par ailleurs hors sujet, parce qu'un IDF élevé porte à lui seul le score. Le routeur de la release répond à ce cas par le veto négatif : si le meilleur score obtenu par un scénario négatif atteint le score positif, la candidate est écartée. Le plancher de 2 tokens partagés, lui, appartient au routeur historique, où MIN_SHARED_TOKENS vaut 2.
Inspecter et recalibrer l'index
Terminal Terminal 12 lignes
# résumé de l'index, sans rien écrire
SKILL_ROUTER_HOST=claude node \
  ~/.config/ai-agents/current/hooks/skill-router/routing/build-index.js --dry-run

# reconstruire l'index
SKILL_ROUTER_HOST=claude node \
  ~/.config/ai-agents/current/hooks/skill-router/routing/build-index.js

# tester une suggestion de bout en bout
echo '{"prompt":"prépare un message pour l équipe"}' \
  | SKILL_ROUTER_HOST=claude node \
      ~/.config/ai-agents/current/hooks/skill-router/bm25-suggest.js

Le hook reconstruit l'index tout seul en tâche de fond quand un fichier de scénarios change. Le --dry-run calcule tout sans écrire le cache, et imprime un résumé JSON : périmètre, nombre de scénarios, de skills couvertes, d'éligibles, de conflits et d'exclusions. Il n'affiche ni les tau ni les F1 par skill, qui se lisent dans les fichiers de cache après une vraie construction. Ces commandes visent l'installation privée de l'auteur ; sur une autre machine, les chemins changent.

JavaScript Maximum par skill, puis éligibilité 28 lignes
function scoreSkills(queryTokens, scenarios, index) {
  const bySkill = new Map();
  const negativeBySkill = new Map();
  for (const s of scenarios) {
    const raw = scoreDoc(queryTokens, s, index.idf, index.avgdl);
    if (s.polarity === 'neg') {
      if (raw > (negativeBySkill.get(s.skill) || 0)) negativeBySkill.set(s.skill, raw);
      continue;
    }
    if (s.polarity !== 'pos') continue;
    if (raw > (bySkill.get(s.skill) || 0)) bySkill.set(s.skill, raw);
  }
  // Un score par skill : son meilleur scénario, jamais la somme.
  return [...bySkill]
    .map(([skill, score]) => ({ skill, score, negativeScore: negativeBySkill.get(skill) || 0 }))
    .sort((a, b) => b.score - a.score);
}

function filterEligible(scored, thresholds, activeSkills) {
  return scored.flatMap((candidate) => {
    const threshold = thresholds[candidate.skill];
    const active = activeSkills[candidate.skill];
    if (!threshold || threshold.status !== 'ok' || threshold.eligible !== true || !Number.isFinite(threshold.tau)) return [];
    if (Number.isFinite(candidate.negativeScore) && candidate.negativeScore >= candidate.score) return [];
    if (candidate.score < threshold.tau || !active || !active.skillMd) return [];
    return [{ ...candidate, skillMd: active.skillMd }];
  });
}
12

BM25 est lexical et le corpus est un artefact à maintenir

3 limites structurelles, dont 2 se corrigent par le corpus et une seulement par un changement d'approche.

2 formulations du même besoin, sans un seul mot en commun. Le routeur n'a rien à mesurer, et se tait.
Limite sémantique « rends ce texte moins bavard » ne correspondra pas à une skill dont le corpus ne contient que « concise » et « lean ». Le remède est d'écrire les 2 formulations dans le corpus, pas de changer d'algorithme.
Le corpus vieillit comme du code Une skill dont les déclencheurs réels ont dérivé garde un corpus qui décrit l'ancien usage, et se met à manquer les vrais prompts. Rien ne le signale, puisque le routeur continue de fonctionner et route simplement vers le passé.
La calibration ne vaut que sur son propre corpus Les métriques affichées par --dry-run sont mesurées sur les données qui ont servi à calibrer. Elles détectent une régression entre 2 versions du corpus. Elles ne prouvent pas une qualité en usage réel. Un jeu de test rédigé indépendamment du corpus de calibration, puis gelé, mesure autre chose. Un jeu tenu à l'écart supposerait de journaliser les vrais prompts, ce que la journalisation actuelle ne fait pas par choix de confidentialité.
JSON Extrait du corpus de flow-lean, 6 positifs et 2 négatifs sur les 40 scénarios du fichier 15 lignes
{
  "skill": "flow-lean",
  "positive": [
    "mode lean activé",
    "passe en mode léger",
    "brevity mode on",
    "trim the fat",
    "donne-moi le TLDR de cette session",
    "récapitule les décisions prises dans cet échange"
  ],
  "negative": [
    "écris-moi un article complet",
    "donne moi tous les détails"
  ]
}
IV

Réutiliser et approfondir

13

Le squelette reprend les 4 couches sans l'inventaire personnel

agent-config-starter reprend la structure, pas le contenu. Il existe sur le poste inspecté et n'est pas publié à ce jour.

Non publié au 18 septembre 2026 Le squelette n'existe que sur le poste inspecté. Aucun dépôt public ne le distribue à ce jour. Ce chapitre décrit sa structure et ses choix d'installation, pour qui veut reconstruire l'équivalent.

Transmettre cette configuration telle quelle n'aurait pas de sens, parce qu'elle contient des chemins de projets, des serveurs MCP avec leurs clés, et 35 agents dont la plupart ne servent qu'à un usage précis. Le squelette reprend la structure et un routeur BM25 autonome, avec 2 skills d'exemple pour que la mécanique soit vérifiable dès l'installation.

Code Arborescence du squelette 11 lignes
agent-config-starter/
├── README.md                     les 4 couches, quand utiliser laquelle
├── install.sh                    simulation par défaut, --apply pour écrire
├── docs/bm25.md                  l'algorithme complet
├── claude/CLAUDE.md              instructions globales génériques
├── claude/settings.json          permissions + hooks
├── codex/AGENTS.md               miroir
├── codex/config.toml.example     commenté, sans secrets
├── hooks/anti-ai-markers.sh      hook PreToolUse, exit 2 = corrige-toi
├── hooks/skill-router/           routeur BM25 autonome, 4 fichiers
└── skills/<nom>/                 skills d'exemple avec corpus

Son script d'installation fonctionne en simulation par défaut et n'écrit qu'avec --apply. Il sauvegarde en .bak.<horodatage> avant tout écrasement, et ne fusionne jamais un settings.json existant. Il dépose un fichier .new à côté et laisse la fusion à la main. Fusionner automatiquement du JSON de configuration écrase un champ existant ou tranche mal un conflit, sans le dire.

Vérifications effectuées le 16 septembre 2026 Les 2 skills d'exemple atteignent le statut ok à la calibration (F1 0.88 et 0.92). Le routeur suggère la bonne skill sur 2 prompts ciblés et se tait sur un prompt hors sujet comme sur un prompt portant la formule d'opt-out. Le hook anti-marqueurs sort en 2 sur un marqueur en prose, et en 0 sur un texte propre comme sur un marqueur situé dans un bloc de code. install.sh a tourné de bout en bout en simulation.
Ce qui est volontairement absent Les serveurs MCP, les wrappers de bascule de modèle, les agents spécialisés, les chemins de projets et le pipeline de releases. Le destinataire part sur une structure qu'il remplit, pas sur un inventaire qu'il devrait comprendre avant de pouvoir modifier quoi que ce soit.
14

3 écarts relevés à l'inspection du 16 septembre 2026

Un piège armé, une redondance coûteuse, et un garde-fou trop large dans le routeur historique.

Les 3 écarts relevés à l'inspection. Aucun n'a levé d'erreur.
Les hooks sont épinglés sur une release antérieure au pointeur courant current pointe sur une release du 15 septembre, mais settings.json et hooks.json référencent en dur une release du 6 septembre. diff -rq entre les 2 répertoires hooks/ ne retourne rien, donc le code est byte-identique et aucun écart fonctionnel n'existe aujourd'hui. Le jour où un hook change, la release courante portera le nouveau code et les 2 hôtes continueront d'exécuter l'ancien, silencieusement.

Ce comportement est cohérent avec la conception, où l'activation des hooks est une transaction séparée, distincte de l'installation des instructions et des skills. Elle demande une approbation explicite liée à un condensat. La conséquence pratique est qu'il faut rejouer cette transaction après toute modification de hook, et que rien ne le rappelle.

Le routeur historique est installé, mais le câblage du 22 septembre ne l'exécute pas L'ancien routeur, sous ~/.claude/hooks/routing/, cible les agents et les commandes. Il porte le plancher de 2 tokens partagés et le garde-fou anti-doublon décrits plus bas. Au 22 septembre 2026, settings.json câble sur UserPromptSubmit le routeur BM25 de la release et smart-suggest.sh, un script Bash à expressions régulières qui n'appelle pas Node. Le fichier historique est donc présent sans être exécuté par ce câblage. Le guide d'origine décrivait 2 routeurs BM25 actifs le 16 septembre ; aucun instantané de settings.json daté de ce jour n'accompagne ce rapport, donc cette observation reste déclarée et non revérifiée.

Les deux couvrent des objets différents et ne sont donc pas strictement redondants. La question à trancher est de savoir si le routage des agents et des commandes mérite encore un moteur séparé, ou si les 2 corpus peuvent fusionner dans le routeur host-aware.

Le garde-fou anti-doublon du routeur historique est trop large ~/.claude/hooks/routing/bm25-suggest.js écarte une cible dont le nom apparaît n'importe où dans le prompt, par simple test de sous-chaîne. L'intention est juste, puisque le garde-fou ne doit pas resuggérer un outil que l'utilisateur vient de nommer. La mise en œuvre l'est moins, car une cible nommée d'après un mot du domaine se disqualifie elle-même. Un prompt contenant le mot debugger écarte l'agent debugger.

Le routeur issu de la release ne porte pas ce défaut, car son garde-fou exige le nom précédé du sigil, /nom pour Claude et $nom pour Codex, ce qui distingue une invocation d'un mot de vocabulaire. C'est cette version qu'il faut reprendre si le routeur historique est conservé. Le squelette réutilisable a été corrigé dans ce sens, et un cas de test le couvre.

15

11 contenus publiés détaillent chaque couche

Ce rapport décrit une installation à une date donnée. Les articles et guides ci-dessous détaillent le raisonnement couche par couche.

Ce rapport montre une configuration, telle qu'inspectée le 16 septembre 2026. Les contenus suivants, publiés sur le même site et rédigés en anglais, prennent chaque couche séparément et exposent le raisonnement, les mesures et les erreurs qui ont conduit à cette forme.

Contenus liés, par couche
Contenus liés, par couche
CoucheContenuCe qu'il ajoute
Pipeline et releasesPortable agent configuration is a release system, not a shared folderLa carte stable du système : source, build, installation, runtime, audit, et ce qui varie encore entre les 2 hôtes
PortabilitéPortability becomes a Scale concernPourquoi les primitives natives ne suffisent pas : sources neutres, sorties générées, contrôle de release, tests de comportement
Instructions globalesYour CLAUDE.md is too longCe qui relève du fichier d'instructions, d'une procédure ou d'une préférence de réponse, et comment vérifier le chargement
SkillsWhy I combined three Claude Code skillsLa fusion de 3 skills de réponse en une seule, et l'évaluation qui l'a départagée
Output StylesClaude selected my output style. Then ignored itInstallation, sélection et comportement demandent 3 preuves distinctes
Hooks et MCPClaude Code security: the attack surface nobody auditsUn hook s'exécute avec les permissions de l'utilisateur, un serveur MCP est du code tiers
Coût des MCPMCP servers: what they actually cost and when to use themChargement immédiat ou différé des outils, contexte consommé et tokens facturés
DémarrerClaude Code setup, level by level3 niveaux de configuration, avec ce qu'il faut vérifier à chacun
DiagnostiquerContext engineering: the L0-to-L5 playbookChoisir un contrôle de contexte d'après la défaillance observée
En équipeThe AI instruction system is a product, not a config fileLe passage d'un CLAUDE.md personnel à un système partagé par 6 développeurs
Dans un projetFrom afterthought to infrastructure9 mois de configuration IA dans un projet en production
Par où commencer Pour une vue d'ensemble, lire d'abord l'article sur la configuration portable. Pour une première installation, partir du guide niveau par niveau. Pour un problème précis, le guide L0 à L5 part du symptôme.

Une question sur cette configuration ?

Une question, un retour d’expérience ou une correction à partager ? Écrivez-moi sur LinkedIn.

Me contacter sur LinkedIn
16

Glossaire

Les termes utilisés dans ce dossier.

BM25 / Okapi BM25
Fonction de classement lexical qui note la pertinence d'un document pour une requête, à partir de la fréquence des termes, de leur rareté et de la longueur du document. Classement par défaut de Lucene, Elasticsearch et SQLite FTS5.
Hook
Script exécuté par le harness à un événement du cycle de vie d'une session. Reçoit un JSON sur son entrée standard. Une sortie 2 renvoie un message de correction au modèle sans faire échouer la session.
Projection
Arborescence de skills rendue par une release pour un hôte donné. Claude reçoit une copie réelle, Codex un lien symbolique.
Skill
Répertoire contenant un fichier SKILL.md avec un frontmatter. Chargée à la demande. Seule sa description est vue avant la décision de charger.
Les autres termes du glossaire
Agent
Sous-agent lancé par le modèle principal dans un contexte séparé, qui rend un rapport. Isole le coût de contexte de son exploration, contrairement à une skill.
avgdl
Longueur moyenne des documents d'un corpus, en nombre de tokens. Sert de référence à la normalisation par longueur dans BM25.
Corpus
Ensemble des phrases d'exemple déclarées par une skill dans son fichier de scénarios, réparties en positifs et négatifs.
F-beta
Moyenne harmonique pondérée de la précision et du rappel. Avec beta = 2, le rappel pèse 4 fois la précision.
F1
Moyenne harmonique de la précision et du rappel. Une valeur de 1 signifie aucun faux positif et aucun faux négatif sur le jeu mesuré.
Harness
Logiciel client qui organise la session, charge les instructions, appelle le modèle et exécute les outils. Claude Code et Codex sont 2 harnesses. C'est lui qui impose hooks et permissions, pas le modèle.
IDF
Inverse document frequency. Poids donné à un terme selon sa rareté dans le corpus. Un terme présent partout ne discrimine rien, un terme rare discrimine beaucoup.
Manifeste
Fichier qui lie chaque octet rendu d'une release à son empreinte. Son condensat normalisé sert d'identifiant à la release.
MCP
Model Context Protocol. Protocole par lequel un serveur externe expose des outils à un agent. Le coût en contexte dépend des métadonnées réellement chargées et du mode de découverte des outils, immédiat ou différé.
Permission
Filtre appliqué avant l'exécution d'un outil. 3 modes : allow qui autorise sans demander, ask qui interrompt pour confirmation, deny qui refuse catégoriquement.
Précision
Part des suggestions émises qui étaient pertinentes. Une précision faible produit du bruit.
Rappel
Part des cas pertinents effectivement suggérés. Un rappel faible produit des skills qui ne se déclenchent jamais.
Release immuable
Répertoire produit par le rendu, identifié par le SHA-256 de son manifeste normalisé, et jamais modifié après création.
Sandbox
Isolation au niveau du système d'exploitation, appliquée à chaque commande shell prise séparément. Limite le réseau, l'écriture disque et l'accès aux secrets.
SHA-256
Fonction de condensat produisant une empreinte de 256 bits. Deux contenus identiques donnent la même empreinte ; un octet de différence en donne une autre.
Stemming
Réduction d'un mot à sa racine par suppression de suffixes, pour que « déclenchement » et « déclencher » produisent le même token.
Stopword
Mot outil retiré avant le calcul du score parce qu'il ne discrimine rien. Ici environ 60 mots français et anglais.
Tau
Seuil de score propre à chaque skill, calculé à la construction de l'index. En dessous, aucune suggestion n'est émise.
trusted_hash
Empreinte d'un script de hook enregistrée par Codex. Modifier le script invalide l'empreinte et désactive le hook jusqu'à réapprobation explicite.
17

Conclusion, sources et mises à jour

Périmètre et méthode du dossier

Décrit la configuration globale installée sur le poste de travail au 16 septembre 2026, telle qu'observée par inspection directe des fichiers. Les comptes et vérifications datés de ce jour viennent du guide d'origine et n'ont pas été revérifiés depuis ; les extraits de code et de configuration, eux, ont été relus dans les fichiers. Couvre le pipeline ai-agents, les 4 couches d'exécution, le routeur BM25 et le squelette réutilisable. Ne couvre pas les configurations propres à un dépôt, les serveurs MCP un par un, ni les plugins tiers.

Conclusion

La valeur de cette configuration ne tient pas au nombre de skills ni à la finesse du routeur. Elle tient à une séparation nette entre ce qui est suggéré au modèle et ce qui lui est imposé, et à un pipeline qui rend les 2 hôtes cohérents tant que la release installée est contrôlée.

Les 3 écarts appellent chacun une disposition. L'épinglage des hooks sur une release antérieure au pointeur courant est sans effet aujourd'hui, mais il se périmera sans signal : il faut rejouer la transaction d'activation après chaque modification de hook. Le routeur historique reste sur le disque sans être câblé : soit il retrouve un câblage assumé, soit il est retiré. Son garde-fou anti-doublon doit être corrigé avant tout nouveau câblage, comme il l'a été dans le squelette.

Pour transmettre la méthode sans transmettre l'inventaire, la structure du squelette suffit : les 4 couches, un routeur vérifiable et 2 skills d'exemple, que le destinataire remplit au rythme de ses besoins réels. Le squelette lui-même n'est pas publié à ce jour.

Sources datées

La mention « consulté le » indique la date de lecture. Une source peut avoir été publiée plus tôt, puis modifiée.

Historique des mises à jour

DateVersionAjouts
22 septembre 20263.0.0Corrections issues d'une relecture indépendante. La chaîne de décision du routeur est réécrite sur le code de la release : opt-out explicite au lieu de coupure sur négation, veto négatif au lieu du plancher de 2 tokens partagés, et évaluation croisée avant éligibilité. Le plancher de 2 tokens et le garde-fou anti-doublon sont rendus au routeur historique, décrit comme installé mais non câblé au 22 septembre 2026. La garantie des hooks est ramenée aux actions interceptées, le masquage des sorties passe d'effet acquis à intention non prouvée, et exit 2 dépend de l'événement. L'extrait scoreSkills retrouve son tri, le corpus cité reprend 2 négatifs réels, les comptages de la sandbox passent aux valeurs du jour, et le chiffre « une clé sur 10 » est retiré faute de mesure. 3 infographies régénérées.
20 septembre 20262.3.0Ajout de 6 extraits tirés des fichiers réels : le manifeste d'une release, le câblage des hooks, les permissions et la sandbox, le cœur du score BM25, le maximum par skill avec le filtre d'éligibilité, et un extrait du corpus de flow-lean. Les chemins personnels sont réduits à ~, les condensats tronqués, et les listes de domaines, de répertoires et de secrets remplacées par leur décompte.
19 septembre 20262.2.0Les infographies passent au design system BoldGuy et forment une série numérotée de 01/08 à 08/08. Les 4 existantes sont refaites (4 couches, 3 barrières, 3 composants de BM25, limite lexicale) et 4 nouvelles s'ajoutent : le pipeline de la source aux 2 agents, la copie réelle face au lien symbolique, la chaîne du prompt à la suggestion et les 3 écarts relevés. Les diagrammes Mermaid restent en place. 17 appels à Gemini 3 Pro Image, dont 1 rejeté pour une espace manquante dans un titre.
19 septembre 20262.1.4Les nombres passent en chiffres dans tout le texte, titres, légendes, diagrammes et historique compris : « 4 couches » au lieu de « quatre couches ». Les lettres restent pour les pronoms (« les deux sont câblés »), l'expression « tous les deux », les textes recopiés des infographies et les titres d'articles cités.
19 septembre 20262.1.3Passe sur la ponctuation d'annonce. Les deux-points qui introduisaient une explication passent en connecteur causal ou en 2 phrases, la question du TL;DR devient une affirmation, et le titre du chapitre sur les 4 couches nomme le fait au lieu d'annoncer une question. Un début de paragraphe répété corrigé.
19 septembre 20262.1.2Passe sur la cadence. Les intitulés sans verbe du TL;DR deviennent des phrases, les énumérations nominales posées comme phrases sont réécrites avec un verbe, et la note de vérification du squelette passe du style journal à des phrases complètes.
19 septembre 20262.1.1Relecture éditoriale. Le TL;DR annonçait 2 écarts alors que le chapitre en décrit 3 : le garde-fou trop large du routeur historique y est ajouté. Une transition mécanique et une formule d'insistance retirées.
18 septembre 20262.1.0Édition publique. Le squelette agent-config-starter est décrit comme non publié et le bloc de commandes d'installation est retiré, puisqu'aucun dépôt ne le distribue. Ajout d'un chapitre qui relie chaque couche aux 11 articles et guides publiés sur le sujet, et de liens vers le portfolio, le guide Claude Code et GitHub. Nouvel habillage aux couleurs du portfolio.
16 septembre 20262.0.0Remplacement des illustrations décoratives par 4 infographies construites, générées avec Gemini 3 Pro Image : les 4 couches, les 3 composants du score BM25, les 3 barrières avant exécution, et la limite lexicale du routeur. Chacune porte un titre, des cartes légendées et une phrase de synthèse. Les 5 diagrammes structurels restent en Mermaid. Les 4 fichiers sont livrés en WebP, 90 % plus légers que le JPEG natif à qualité visuelle équivalente ; les originaux restent dans images/originals/.
16 septembre 20261.4.0Ajout de 2 illustrations générées avec Gemini 3 Pro Image : une ouverture abstraite aux couleurs de la marque, et une mise en image de la limite lexicale du routeur. Les 5 diagrammes structurels restent en Mermaid, parce que leurs arêtes portent du sens et qu'un rendu génératif les déforme.
16 septembre 20261.3.0Ajout d'une infographie des 4 couches, générée avec Gemini 3 Pro Image. Elle complète l'arbre de décision sans le remplacer : les diagrammes dont chaque arête porte du sens restent en Mermaid.
16 septembre 20261.2.0Le glossaire passe en tête du document, avant le résumé et les chapitres, pour que le vocabulaire soit disponible dès la première lecture.
16 septembre 20261.1.0Ajout d'un 3e écart constaté : le garde-fou anti-doublon du routeur historique teste une simple sous-chaîne, ce qui fait qu'une cible nommée d'après un mot du domaine se disqualifie elle-même. Le routeur de la release exige le sigil et n'a pas ce défaut.
16 septembre 20261.0.0Création. Architecture du pipeline ai-agents, 4 couches d'exécution, routeur BM25 et squelette réutilisable, d'après une inspection directe du poste. 2 écarts relevés et documentés : épinglage des hooks sur une release antérieure au pointeur courant, coexistence de 2 routeurs BM25.

Copier le dossier en JSON

La copie automatique est indisponible. Le texte est sélectionné : utilisez ⌘C ou Ctrl+C, puis collez-le dans votre assistant.

Partager ce dossier

Télécharger le dossier HTML