AIDive

Pack vidéo

Skill, hook, subagent ou MCP : les 4 fichiers et la carte de choix

7 min de lecture

TL;DR et verdict

Quatre tâches que tu tapes sans cesse dans Claude Code appartiennent chacune à un fichier différent. Une checklist de release est un skill. Une règle qui ne doit jamais être enfreinte est un hook PreToolUse. Un audit qui lit beaucoup de fichiers est un subagent. Des données que Claude ne peut pas voir sont un serveur MCP.

Nous avons fait tourner les quatre sur un vrai repo, une API de facturation avec 34 tests, en commençant à chaque fois par le mauvais choix. Les mauvais choix ont échoué à l'écran :

  • La checklist dans CLAUDE.md a ajouté une ligne Memory files: 210 tokens à /context, à chaque tour.
  • Le premier hook s'est exécuté, a affiché son avertissement, et l'écriture est passée (unitPrice de 8000 à 12000).
  • L'audit lancé en ligne a rempli le fil : Messages à 19.3k tokens, contre 7.2k avec un subagent nommé.
  • Sans serveur de docs, Claude a répondu « I couldn't find that setting... I won't guess one. »

Verdict : choisis le type avec le tableau de la doc, puis fais en sorte que le fichier réponde, pas seulement qu'il existe. Un hook qui ne fait que logger est consultatif, comme la ligne de CLAUDE.md qu'il remplaçait. Des quatre, seul le hook ne demande jamais.

Copie les fichiers ci-dessous, commite-les, et commence par celui que tu retapes le plus.

Synthèse sourcée

Ce que ta session charge avant ton premier mot. Notre /context de référence affichait 26.2k/1m tokens, sans ligne mémoire ni ligne MCP. Chaque tâche que tu ajoutes met une ligne dans ce tableau ou non. Lis-le d'abord, puis mesure par rapport à lui.

Une règle dans CLAUDE.md est une demande, un hook est une application stricte. La doc dit qu'une instruction comme « never edit .env » dans CLAUDE.md ou dans un skill est une demande, et qu'un hook PreToolUse qui bloque l'édition est une application stricte s1. La page Memory décrit CLAUDE.md comme du contexte, pas comme une application stricte s8. Un fil sur Hacker News rapporte la même chose côté hook : un hook qui se termine avec 0 ne bloque rien s10.

Le tableau des déclencheurs te dit quel fichier. Troisième collage d'une procédure : un skill. Une sortie qui inonde le chat : un subagent. Un onglet que Claude ne voit pas : un serveur MCP. Quelque chose qui doit arriver à chaque fois : un hook s1.

Tâche 1, la checklist de release, est un skill.

  • Une procédure qui a grossi dans CLAUDE.md a sa place dans un skill, dont le corps n'est chargé qu'à l'usage s3.
  • Dans notre essai, la checklist collée dans CLAUDE.md a ajouté une ligne Memory files: 210 tokens. En skill avec disable-model-invocation: true, /context est revenu à la référence.
  • /release a exécuté six étapes (34 tests, 0.4.0 vers 0.4.1, tag v0.4.1). Il a aussi remarqué que git commit -am ignore les fichiers non suivis, et a donc d'abord ajouté le nouveau changelog à l'index.

Tâche 2, la règle à tenir, est un hook, et la garantie est la réponse qu'il renvoie.

  • Exit 0 sans sortie n'est pas une décision. Exit 2 bloque. Le JSON deny bloque aussi s2.
  • Notre premier hook loggait, se terminait avec 0 et l'écriture est passée (unitPrice de 8000 à 12000). La réponse de Claude n'a jamais mentionné le hook.
  • Le même hook renvoyant le JSON deny a refusé l'édition avec PreToolUse:Edit hook error: Stored data is never edited by hand: write a migration in src/store/migrations.ts (README § Conventions). Claude a ensuite demandé : contourner, ou écrire la migration.
  • La doc publie une recette PreToolUse pour les fichiers protégés s23.
  • La limite : ce hook surveille Edit et Write. Couvre aussi le shell, ou garde le shell hors de ta liste d'autorisations.
  • onFailure: "block" fait bloquer un hook en échec au lieu de laisser passer. Il est arrivé en v2.1.295 s2 s15.

Tâche 3, l'audit qui inonde, est un subagent.

  • Un subagent démarre à l'aveugle et renvoie un résumé s4.
  • En ligne, Messages affichait 19.3k. Avec le subagent nommé, 7.2k.
  • Le fichier d'agent seul a délégué 0 fois sur 2. Le nommer dans le prompt a déclenché la délégation. Le fichier le fait exister, ton prompt fait que Claude l'utilise.
  • Fixe model: dans le fichier. La facture n'a pas bougé : la lecture a quand même lieu, simplement pas dans ta fenêtre.

Tâche 4, les données hors d'atteinte de Claude, est un serveur MCP.

  • claude mcp add --scope project écrit .mcp.json, et la recherche d'outils ne charge que les noms d'outils s5.
  • Il existe un serveur HTTP gratuit, sans connexion, pour les docs de Claude Code lui-même, avec search_claude_code_docs et query_docs_filesystem_claude_code_docs s24.
  • Sans lui, Claude a répondu « I couldn't find that setting... I won't guess one. » Avec lui, Claude a répondu "onFailure": "block", ajouté en v2.1.295, alors que notre installation était en 2.1.294.
  • Un serveur de docs pour une bibliothèque que le modèle connaît déjà n'a rien changé pour nous. Connecte ce que le modèle ne peut pas avoir.

Retours de terrain. Un fil Reddit rapporte que Claude ignore une ligne « MUST use agent » dans CLAUDE.md s9. Nous le citons comme un témoignage, pas comme une mesure. Les articles d'ingénierie d'Anthropic traitent de la même séparation s6 s7.

Code

Quatre fichiers de l'essai, plus deux utilitaires. Les chemins indiquent où chacun vit dans ton repo.

1. Le skill de release, dans .claude/skills/release/SKILL.md. Tape /release. disable-model-invocation: true fait que toi seul peux le lancer, ce qui est voulu pour quelque chose qui commite et qui tague.

SKILL.md

---
name: release
description: Cut a patch release of ledgerly (typecheck, tests, version bump, changelog line, commit, tag).
disable-model-invocation: true
---

Run these steps in order and stop at the first failure:

1. `pnpm typecheck` must exit 0.
2. `pnpm test` must pass.
3. Bump the patch version in `package.json` (e.g. 0.4.0 -> 0.4.1).
4. Add one line at the top of `CHANGELOG.md` (create it if missing): `## vX.Y.Z - YYYY-MM-DD` followed by a one-line summary of the commits since the last tag.
5. `git commit -am "release vX.Y.Z"`
6. `git tag vX.Y.Z`. Never push.

SKILL.md on GitHub

2. Le hook de protection des données, dans .claude/settings.json. Il renvoie la décision deny avec une raison que Claude lit. Il a besoin de jq dans ton PATH et tient en ligne, donc le hook n'a pas besoin de fichier script. Change le motif data/.*\.json$ et la raison pour ta règle.

settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -c 'if (.tool_input.file_path // \"\") | test(\"data/.*\\\\.json$\") then {hookSpecificOutput: {hookEventName: \"PreToolUse\", permissionDecision: \"deny\", permissionDecisionReason: \"Stored data is never edited by hand: write a migration in src/store/migrations.ts (README § Conventions).\"}} else empty end'"
          }
        ]
      }
    ]
  }
}

settings.json on GitHub

3. L'auditeur de conventions, dans .claude/agents/conventions-auditor.md. Les outils sont limités à la lecture seule et le modèle est fixé. Modifie la liste de conventions pour qu'elle corresponde à ton README.

conventions-auditor.md

---
name: conventions-auditor
description: Use when asked to audit the code against the README conventions
model: sonnet
tools: Read, Grep, Glob
---

You audit ledgerly's source against README § Conventions:

- Money is always an integer number of cents. Never floats.
- Domain functions return new objects; they never mutate their input.
- Validation failures throw `ValidationError` (400), missing records `NotFoundError` (404), illegal state transitions `ConflictError` (409).
- A change of stored shape bumps `schemaVersion` and adds a migration in `src/store/migrations.ts`.

Read every file under `src/`. Return only the findings: one line per violation as `file:line - convention - what is wrong`, then borderline cases the same way, then one line with the count of files read. No file contents, no summary of clean files.

conventions-auditor.md on GitHub

4. La config MCP, enregistrée sous .mcp.json à la racine du repo. C'est ce qu'écrit claude mcp add --transport http claude-docs --scope project https://code.claude.com/docs/mcp. Claude Code liste un serveur de projet comme en attente jusqu'à ce que tu l'approuves à ta prochaine session.

mcp.json

{
  "mcpServers": {
    "claude-docs": {
      "type": "http",
      "url": "https://code.claude.com/docs/mcp"
    }
  }
}

mcp.json on GitHub

Utilitaires. ctx.sh lance /context dans une session sans interface et affiche les lignes de tokens, pour comparer avant et après. mcp-probe.sh envoie initialize et tools/list à une URL MCP HTTP, pour vérifier qu'un serveur répond avant de l'ajouter.

ctx.sh

#!/usr/bin/env bash
# Usage: ctx.sh <step>  -> runs "/context" through run-session.sh, writes context-<step>.txt (honours STRICT_MCP / EXTRA_ALLOW)
C="$(cd "$(dirname "$0")" && pwd)"
"$C/run-session.sh" "$C/out/context-$1.json" "/context" >/dev/null
python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print(d['result'])" "$C/out/context-$1.json" > "$C/context-$1.txt"
grep -E '^\*\*Tokens|^\| (System prompt|System tools|MCP tools|Memory files|Skills|Custom agents|Messages) ' "$C/context-$1.txt"

ctx.sh on GitHub

mcp-probe.sh

#!/usr/bin/env bash
# Usage: mcp-probe.sh <url> [tools/call json params]  -> initialize + notifications/initialized + tools/list (+ optional tools/call) over streamable HTTP
U="$1"; H=$(mktemp)
curl -s -D "$H" -m 30 -X POST "$U" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
echo; SID=$(grep -i '^mcp-session-id:' "$H" | tr -d '\r' | cut -d' ' -f2); echo "session-id: ${SID:-none}"
HS=(-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream'); [ -n "$SID" ] && HS+=(-H "Mcp-Session-Id: $SID")
curl -s -m 30 -X POST "$U" "${HS[@]}" -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
curl -s -m 30 -X POST "$U" "${HS[@]}" -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'; echo
[ -n "${2:-}" ] && { curl -s -m 90 -X POST "$U" "${HS[@]}" -d "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":$2}"; echo; }
rm -f "$H"

mcp-probe.sh on GitHub

À faire lundi

  • Ouvre une session neuve et lance /context. Note le total. C'est ta référence.
  • Prends la checklist que tu colles le plus. Déplace-la vers .claude/skills/<name>/SKILL.md, ajoute disable-model-invocation: true si elle commite ou tague, et lance-la avec /<name>.
  • Prends la règle qui ne doit jamais être enfreinte et mets-la dans un hook PreToolUse qui renvoie permissionDecision: "deny" ou se termine avec 2. Demande à Claude d'enfreindre la règle, et vérifie que l'édition est refusée.
  • Si ton hook ne fait que logger, change-le. Un hook qui se termine avec 0 ne bloque rien.
  • Déplace une tâche qui lit beaucoup de fichiers vers .claude/agents/<name>.md avec un model: fixé. Nomme-le dans ton prompt jusqu'à ce que Claude le choisisse seul.
  • Vérifie un fait que Claude a deviné récemment. S'il est plus récent que le modèle, ajoute un serveur de docs avec claude mcp add --transport http <name> --scope project <url> et commite .mcp.json.
  • Vérifie ta version de Claude Code avant de compter sur un réglage. onFailure: "block" demande la 2.1.295.
  • Lance git add -A && git status et commite les nouveaux fichiers. Notre essai en montrait exactement quatre.

Pour aller plus loin

La carte de choix. Une page. Ce sont les lignes de la doc s1 avec nos quatre tâches écrites dessus. Le cœur de ce pack, ce sont les quatre fichiers ci-dessus plus cette carte.

Type Ce qu'il fait À utiliser quand Notre tâche Notre fichier
CLAUDE.md Contexte persistant chargé à chaque conversation Une convention que Claude rate deux fois Une ligne, pas une procédure aucun
Skill Instructions et workflows que Claude peut utiliser Tu colles le même playbook pour la troisième fois La checklist de release .claude/skills/release/SKILL.md
Hook Un script déclenché par des événements Tu veux que ça arrive à chaque fois, sans demander Ne jamais modifier les données stockées à la main .claude/settings.json
Subagent Contexte isolé qui renvoie des résultats résumés Une tâche annexe inonde ta conversation L'audit des conventions .claude/agents/conventions-auditor.md
MCP Se connecter à des services externes Tu recopies sans cesse des données depuis un onglet que Claude ne voit pas Les docs de Claude Code .mcp.json
Plugin Regroupe les quatre en une unité installable Un deuxième dépôt a besoin de la même installation Non utilisé aucun

La carte décide du type, pas de la qualité. Un hook qui ne fait que logger, un subagent que tu ne nommes jamais et un serveur pour des docs que le modèle connaît étaient chacun dans le bon type et ont tous échoué. Choisis le type, puis fais en sorte que le fichier réponde.

Quand ne pas s'en servir.

  • Une convention que Claude rate sans cesse reste dans CLAUDE.md sous forme d'une ligne, pas d'une procédure.
  • Une tâche que tu pilotes dans les deux sens, avec ton historique sous les yeux, reste dans le fil principal, pas dans un subagent.
  • Un serveur de docs pour une bibliothèque que le modèle connaît déjà coûte du contexte et ne change rien.
  • pnpm test est un CLI que tu as déjà. Ce n'est pas une tâche pour MCP.

Plugins. Un plugin est la boîte qui livre les quatre ensemble. Sers-t'en quand un autre repo a besoin de la même installation.

Skill plus MCP. La doc les associe : le serveur fournit la connexion, et le skill apprend à Claude à bien s'en servir.

Skill avec context: fork. Un skill peut s'exécuter dans un subagent forké, pour que sa lecture reste hors de ta fenêtre. Teste-le sur une tâche avant de t'y fier.

Types de hook au-delà de command. Command est celui que nous avons utilisé. La référence des hooks liste d'autres types de gestionnaires, dont les hooks http et prompt. La même règle vaut : ce que renvoie le hook décide, et un hook silencieux ne décide rien.

MCP contre CLI. Il y a un débat ouvert sur les serveurs MCP qui mangent du contexte, avec un article qui plaide pour le CLI d'abord s11 et un autre sur le coût en contexte des serveurs MCP s12. Notre règle : si un CLI fait déjà le travail, utilise-le. Utilise MCP pour ce qui n'en a pas. La recherche d'outils ne charge désormais que les noms, donc mesure avec /context sur ta propre installation avant de décider s5.

Garde les skills courts et vérifiés. Un skill court par tâche, vérifié sur un vrai essai. Voir l'article SkillsBench s13 et un point de vue de praticien sur la raison pour laquelle les skills coûtent peu tant qu'on ne s'en sert pas s14.

FAQ

Pourquoi mon hook s'est-il exécuté alors que l'édition est quand même passée ? Un hook ne décide que par ce que renvoie sa commande. Exit 0 sans sortie n'est pas une décision s2. Renvoie le JSON deny, ou termine avec 2.

Pourquoi Claude lit-il les fichiers lui-même alors que j'ai un subagent ? Dans notre essai, le fichier d'agent seul a délégué 0 fois sur 2. Nomme-le dans le prompt, par exemple « use the conventions auditor subagent ».

Le hook couvre-t-il tout ce que Claude peut faire ? Non. Le nôtre cible Edit et Write. Lors d'un essai ultérieur, Claude a proposé de faire le changement par le shell, donc couvre le shell ou garde-le hors de ta liste d'autorisations.

Dois-je ajouter un serveur MCP pour chaque bibliothèque que j'utilise ? Non. Si le modèle connaît déjà la bibliothèque, un serveur ne change rien et coûte du contexte. Ajoutes-en un pour des données plus récentes que le modèle ou hors de ton disque, et compare ta version installée à celle qui a ajouté la fonctionnalité.

Puis-je tout commiter ? Oui. Le skill, le fichier de réglages, le fichier d'agent et .mcp.json vivent tous dans le repo, donc quiconque le clone les reçoit. Approuve le serveur MCP du projet une fois à ta prochaine session.

Fichiers