# Claude Code A series of 2 article(s), in reading order. # Claude Code - Optimiser la consommation de tokens > Un guide pratique pour réduire la consommation de tokens dans Claude Code; commandes CLI, optimisation de CLAUDE.md et autre gestion du contexte. --- ![Claude Code - Optimiser la consommation de tokens](/img/v2/claude-code-tokens.webp) **TL;DR** Les tokens sont la monnaie des conversations avec l'IA : chaque mot que vous envoyez et recevez a un coût. Cet article parcourt des techniques concrètes pour réduire la consommation de tokens dans Claude Code — des commandes slash comme `/clear` et `/compact` aux optimisations de CLAUDE.md, en passant par des règles de récupération de données plus intelligentes et des conseils d'utilisation en heures creuses. Si vous débutez avec Claude Code, vous ne savez peut-être pas encore que chaque interaction a un coût mesuré en **tokens**. Grosso modo, un token est un petit morceau de texte (environ 4 caractères ou ¾ d'un mot). Plus Claude a de contexte à traiter — votre historique de conversation, les fichiers ouverts, les sorties des outils — plus de tokens sont consommés. Sur les plans payants, cela affecte votre quota ; sur les plans gratuits, cela limite directement ce que vous pouvez faire dans une session. Cet article rassemble des conseils pratiques pour tirer le maximum de votre budget de tokens. ## Réduire le contexte {#reducing-context} **Astuces pour économiser des tokens** * **`/clear`** — Démarrez une nouvelle conversation quand l'historique actuel n'est plus pertinent. Plus la conversation est longue, plus chaque nouveau message coûte de tokens en entrée. * **`/compact`** — Quand le quota atteint \~40% et que vous voulez garder l'historique, `/compact` le résume à l'aide de l'IA. Lancez-le quand la conversation est longue et va se poursuivre sur plusieurs échanges. * **Committez vos changements** — Après avoir terminé une tâche, lancez `git add` + `git commit`. Claude Code relit en continu le diff pour détecter les régressions ; un `git diff` vide signifie moins de contexte consommé à chaque message. * **`/caveman`** ([plugin](https://github.com/JuliusBrussee/caveman)) — Force Claude à répondre le plus compactement possible, ce qui réduit les tokens en sortie. Compromis : une qualité de réponse moindre. * **`/caveman-compress CLAUDE.md`** — Réécrit votre CLAUDE.md dans une syntaxe ultra-compacte, en remplaçant les phrases verbeuses par des abréviations comme `SOLID` ou `SRP`, sans perdre la moindre instruction. * **`/context`** — Montre ce qui est chargé et combien de tokens chaque élément consomme, y compris chaque serveur MCP. Utilisez **`/.mcp`** pour activer ou désactiver les MCP. * **`/usage-credits`** — Ouvre claude.ai pour afficher immédiatement votre pourcentage d'utilisation actuel. * **Taille de `CLAUDE.md`** — Ce fichier est chargé à chaque démarrage et consomme des tokens en entrée avant que vous ayez tapé le moindre mot. Gardez-le minimal — uniquement ce qui est vraiment nécessaire. * **Invalidation du cache** — Changer de modèle ou ajouter un serveur MCP invalide le cache de prompt de Claude et le force à tout relire. Faites ces changements au début d'une nouvelle conversation (après `/clear`). * **Fichiers et images** — Attacher un PDF, un document Word ou une image coûte nettement plus de tokens en entrée. Collez directement le texte pertinent dans le prompt quand c'est possible. * **`/btw`** — Ajoute une note éphémère à la conversation sans l'inclure dans l'historique complet du contexte. À noter : `/btw` empêche les agents de lancer le moindre outil sur votre codebase. ### Choix du modèle {#model-choice} Le choix du modèle a un impact direct sur la consommation de tokens — plus le modèle est capable, plus il coûte cher : **Coût du modèle vs cas d'usage** * **Claude Opus** — Coût en tokens le plus élevé. Idéal pour le raisonnement complexe et les décisions d'architecture. * **Claude Sonnet** — Coût en tokens moyen. Idéal pour les tâches de développement au quotidien. * **Claude Haiku** — Coût en tokens le plus faible. Idéal pour la rapidité, les questions simples et les petites modifications. ## Fichier CLAUDE.md {#claudemd-file} Chargé à chaque démarrage — chaque octet ici coûte des tokens avant votre premier message. Écrivez en style télégraphique ; `SOLID` et `SRP` sont compris sans phrases complètes. Si une instruction n'arrête pas de grossir parce qu'elle doit expliquer le *pourquoi*, et pas seulement énoncer le *quoi*, sa place n'est peut-être pas ici du tout — [un article ultérieur](/blog/claude-code-workflow) explique comment déplacer ce genre de règle permanente dans un skill, un agent ou un fichier de règles plutôt que de la réécrire dans `CLAUDE.md` chaque fois qu'elle grossit. ### Règles de récupération des données {#data-fetching-rules} Indique à Claude d'utiliser des appels ciblés à [`grep`](/blog/ripgrep)/[`jq`](/blog/linux-jq)/`yq` au lieu de charger des fichiers entiers. **Restrictif** — économie de tokens maximale : ```markdown ## Data Fetching Rules **Fetch only what each check requires — never pull full metadata upfront.** ``` **Équilibré** — lecture complète pour les fichiers source courts, ciblée pour l'infra : ```markdown ## Data Fetching Rules **Distinguish between code and metadata:** - **Source Code:** Because files are strictly max 200 lines, you may read full source files to grasp the complete import/dependency context and avoid breaking SRP. - **Infrastructure & Logs:** Never read full logs, `.lock` files, or raw JSON/YAML metadata. You MUST use targeted shell tools (`grep`, `jq`, `yq`) for Docker configs, `compose.yaml`, JSON files or CI/CD files. ``` > **Note:** > > La version restrictive peut mener à des hallucinations ; la version équilibrée échange quelques tokens supplémentaires contre un contexte de fichier exact sur les fichiers source courts. ### Mode de sortie {#output-mode} Ajoute \~25 tokens en entrée au démarrage mais raccourcit chaque réponse de l'IA, ce qui cumule les économies sur les longues sessions. **Agressif** — réponses les plus courtes, raisonnement moins visible : ```markdown ## Output mode **Ultra-concise.** No filler words, no repetition, no introductions, no conclusions. Density over readability. Skip pleasantries. Go straight to findings. ``` **Modéré** — bref, avec une justification en une ligne par changement : ```markdown ## Output mode **High-Density Technical.** No filler, pleasantries, or generic introductions. When proposing code changes, you MUST provide a 1-sentence technical rationale explaining your architectural choice (e.g., how it serves SOLID, DRY, or a specific PHP 8.4/Python 3.14 feature). Be a highly critical peer reviewer; flag flaws bluntly. ``` > **Note:** > > Le mode agressif économise plus de tokens mais masque le raisonnement de l'IA. Utilisez le mode modéré si vous voulez comprendre et apprendre de chaque suggestion. ### Génération de code {#code-generation} Empêche Claude de renvoyer des fichiers entiers — seules les lignes modifiées, avec un contexte environnant minimal. ```markdown ## Code generation Do not output unchanged code blocks. Only display the specific lines modified, providing minimal surrounding context. Rely strictly on tools for full file rewrites. ``` ## Heures creuses et heures de pointe {#off-peak-and-peak-hours} Anthropic a confirmé qu'en journée, pendant les heures de bureau, la **limite de débit de messages** est différente. Le nombre de tokens reste le même, mais pas leur pondération : vous épuiserez votre quota plus ou moins vite selon l'heure de la journée. Si votre budget est serré, travailler pendant les heures creuses (le soir ou le week-end) fera durer votre quota plus longtemps. ## Bonus {#extra} ### Si vous ne voulez pas installer caveman {#if-you-dont-want-to-install-caveman} Créez simplement une nouvelle conversation et collez ce prompt, en remplaçant `[YOUR-CLAUDE.MD-CONTENT]` par le contenu réel de votre fichier : > **Prompt de compression façon caveman:** > > ```text > I want you to act as a token compressor for an LLM system prompt (in a "Caveman" style). Your goal is to drastically reduce the size of the following text while retaining 100% of the rules, technical constraints, and architectural guidelines. Compression rules: 1. Remove all connecting words, articles, polite language, and human grammar. 2. Use extreme telegraphic syntax, keywords, and standard abbreviations. 3. Group concepts by tags (e.g., [Rules], [Stack], [Lint]). 4. Do NOT omit ANY technical constraints (e.g., tool names, versions, strict limits). 5. Generate only the compressed result, with no introduction or conclusion. Here is the file to be compressed: > > [YOUR-CLAUDE.MD-CONTENT] > ``` --- # Claude Code - Des slash commands à un workflow en couches > Comment le dossier .claude/ de ce blog est passé d'une poignée de slash commands à un ensemble de commands, hooks, skills, agents et rules qui travaillent ensemble — un parcours d'apprentissage concret, et les cas où un script déterministe bat une question posée à l'IA. --- ![Claude Code - From Slash Commands to a Layered Workflow](/img/v2/vibe_coding_claude.webp) **TL;DR** Le dossier `.claude/` de ce blog est passé, en quelques mois, d'une poignée de slash commands à un système à cinq couches : des commands que vous tapez, des hooks qui bloquent ou approuvent automatiquement de façon déterministe, des skills qui encodent ce que « bon » veut dire, des agents qui le vérifient dans un contexte isolé, et des rules qui imposent la version courte automatiquement. Cet article suit l'ordre dans lequel tout a réellement été construit, à quoi sert chaque couche, un exemple réel de chacune, et pourquoi un simple script Bash — sans la moindre IA — est parfois la meilleure brique possible. Comme tout débutant, mes premières sessions Claude Code sur ce blog commençaient toutes de la même façon, jour après jour : je réexpliquais, dans le chat, à quoi ressemble un bon script Bash ici, ce qu'un Dockerfile ne doit jamais faire, ou comment numéroter un nouveau fichier TODO sans entrer en collision avec un existant. Claude faisait le travail, mais c'était moi qui portais le règlement — retapé, session après session. Mon dossier `.claude/` ne ressemble plus du tout à ce qu'il était. Aujourd'hui, il contient 15 slash commands, 4 agents reviewers en lecture seule, 10 skills, 3 rules toujours actives, et quelques scripts Bash qui ne touchent jamais à l'IA. Rien n'est arrivé d'un coup — chaque couche est apparue pour résoudre un problème que la précédente ne savait pas traiter. ## À quoi ressemblent ces cinq couches en pratique {#what-five-layers-look-like-in-practice} Cinq ingrédients rendent tout cela possible, et chacun a sa propre section plus bas — voici juste la version courte, pour que la démo qui suit ne ressemble pas à une soupe d'acronymes : **Cinq mots à garder en tête** * **Command** — ce que vous tapez vous-même (`/quelquechose`). * **Skill** — une checklist que Claude lit de lui-même, sans qu'on la lui demande. * **Agent** — exécute cette checklist dans sa propre fenêtre séparée et restreinte, loin de votre conversation principale. * **Rule** — se charge automatiquement dès qu'un fichier correspondant est ouvert. * **Hook** — peut bloquer une action net, sans négociation possible. Deux de ces cinq n'attendent jamais un humain : une **rule** se charge à l'instant où un fichier correspondant s'ouvre, et un **hook** se déclenche à chaque appel d'outil correspondant — rien à taper, et Claude n'a pas voix au chapitre. Les trois autres se déclenchent à la main, et s'enchaînent comme le montre la démo ci-dessous : vous tapez une **command**, la command appelle un **agent**, et l'agent applique une **skill** pour rendre le verdict — sachant, comme expliqué plus loin, qu'une skill peut aussi se charger toute seule quand sa description colle à la tâche en cours, sans aucune command dans la boucle. Voici à quoi ressemble aujourd'hui une lecture critique d'un article de blog, une fois les cinq couches en place, avant même sa publication : > */review:reader git-precommit* On n'a plus besoin de réexpliquer à Claude ce que « bon » veut dire pour un article de ce blog — la skill `reader-first-docs` le détaille déjà, et `/review:reader` l'applique directement : quelle part de l'article se trouve entre `` et la première vraie preuve — une sortie de commande réelle, pas seulement une étape d'installation ? > `git-precommit` — sortie réelle dix lignes après ``, soit environ 7 % du corps > de l'article. Largement sous le seuil vert de 15 %, rien d'installé ni d'expliqué avant. > Verdict : OK, rien à restructurer. Cette checklist — mettre la valeur en avant, marquer les approfondissements comme optionnels, garder l'installation hors de la première moitié — n'a jamais eu à être retapée, des mois après avoir été écrite une première fois. Et un détail à souligner avant tout le reste : toutes les commands de ce workflow ne reposent pas sur Claude. `/todos:add`, la command qui ajoute une nouvelle entrée au backlog, a d'abord besoin du prochain ID libre — un pur problème de comptage. Plutôt que de demander au modèle de parcourir `.todos/` et de compter, elle appelle un simple script : ```bash $ .claude/scripts/todo_next_id.sh .todos 0105 ``` > **Bonne pratique : un script vaut mieux qu'un prompt:** > > Dès qu'une étape relève de la pure logique — compter des fichiers, incrémenter un ID, parser une > date, vérifier un verrou — écrivez-la comme un simple script et faites-le appeler par la command. > C'est déterministe (la même entrée donne toujours la même sortie), ça coûte zéro token, et ça se > teste tout seul, sans modèle dans la boucle. Gardez l'IA pour les étapes qui demandent vraiment du > jugement. ## Pourquoi une organisation en couches fonctionne {#why-a-layered-setup-works} * **Une seule couche se tape.** Les slash commands sont le seul élément auto-découvrable — l'autocomplétion les trouve. Skills, rules, agents et hooks restent invisibles jusqu'à ce que quelque chose les déclenche ; ils façonnent *la manière* dont Claude travaille sans alourdir ce que vous devez penser à invoquer. * **Chaque couche répond à une question différente.** Une skill décide *ce que* « bon » veut dire. Un agent décide *qui* le vérifie, et dans quel isolement. Une rule décide *quand* la version courte s'applique automatiquement. Un hook décide *ce qui est tout simplement interdit* — aucun jugement, aucune façon de faire changer le modèle d'avis. * **Les agents en lecture seule gardent le bruit de review hors de la conversation qui compte.** C'est une économie de tokens qui vient de l'architecture, pas de la discipline de prompting — un bon complément aux astuces de gestion de session de [une précédente série de conseils pour économiser des tokens](/blog/claude-ia-spare-tokens), centrée sur `/clear`, `/compact` et la taille de `CLAUDE.md`. * **Une rule n'est volontairement pas une explication complète.** C'est un extrait compressé DO/DON'T de la skill sœur, chargé uniquement quand un fichier correspondant est réellement ouvert — le raisonnement complet reste à un clic, pas dans chaque fenêtre de contexte. * **La couche la moins chère de toutes n'appelle pas le modèle.** Un simple script, comme ci-dessus, est le socle sur lequel tout le reste se construit. ## Commencez ici : votre première slash command {#start-here-your-first-slash-command} Les slash commands ont été le point d'entrée facile, et elles le restent — un fichier Markdown avec un petit en-tête frontmatter, détecté automatiquement dès qu'il atterrit dans `.claude/commands/`. Par exemple, créez simplement le fichier `.claude/commands/pr-description.md` dans votre projet avec ce contenu : **.claude/commands/pr-description.md** ```markdown --- description: Draft a pull-request description from the current branch's diff against main argument-hint: "[base-branch] (optional — defaults to main)" allowed-tools: Bash(git diff*), Bash(git log*), Read --- # PR description Look at `git diff $ARGUMENTS...HEAD` and `git log $ARGUMENTS...HEAD --oneline`. Draft a pull-request description: a one-paragraph summary of the "why", then a bulleted "what changed". Do not include a test plan unless the diff touches test files. ``` Tapez maintenant `/pr-description` dans une session Claude Code, et ce prompt s'exécute avec le diff courant déjà sous les yeux — rien à retenir, rien à réexpliquer. Deux détails du fichier ci-dessus comptent plus qu'ils n'y paraissent : * **`argument-hint`** est ce que l'autocomplétion affiche avant même que vous ayez fini de taper la command — de la documentation à bas prix pour votre futur vous. * **`allowed-tools`** limite le rayon d'action avant que la command ne s'exécute. Celle-ci peut lire l'historique Git et des fichiers, et rien d'autre — elle ne peut rien modifier, même par erreur. Trois commands réelles issues du dossier `.claude/commands/` de ce blog montrent la même forme à différentes tailles. `/review:bash` est à peine plus qu'un pointeur vers l'agent traité plus loin dans cet article : **.claude/commands/review/bash.md** ```markdown --- description: Audit this project's Bash scripts against the bash-best-practices skill (read-only) argument-hint: "[path] (optional — defaults to the whole project)" allowed-tools: Agent --- # Bash review Invoke the `bash-best-practices-reviewer` agent to audit **$ARGUMENTS** (or the whole project if no argument is given) against the `bash-best-practices` skill, and against this project's own bash-conventions rule/skill too, if it ships one. Read-only: the agent reports violations with file:line, it never edits. ``` `/links` maintient la convention de maillage interne de ce blog — 2 à 4 liens inline par article, plus un lien réciproque dans l'article plus ancien vers lequel il pointe : **.claude/commands/links.md** ````markdown --- description: Add missing internal links to an article (inline, in the prose) and the reciprocal link in the older post. argument-hint: "[path or slug of an article — omit to use the articles changed in the working tree]" allowed-tools: Read, Glob, Grep, Bash, Edit --- # Add internal links to an article The blog's cross-linking rule lives in `AGENTS.md` ("Blog Content Guidelines" → Internal links): every article carries **2 to 4 internal links, inline in the prose**, plus a reciprocal link in the older article when it makes sense. The `internal-links` job of `.github/workflows/quality.yml` flags articles that link nowhere; this command is the fix. ## 1. Decide which articles to work on `$ARGUMENTS` may be an article folder (`blog/2026/07/27/reactions`), an `index.md`, a slug (`reactions`), or a `.unpublished/` draft. A slug has to be resolved first: ```bash grep -rl "^slug: $ARGUMENTS$" blog .unpublished --include=index.md --include=index.mdx ``` If `$ARGUMENTS` is empty, work on the articles currently modified or added in the working tree: ```bash git status --porcelain -- 'blog/**/index.md*' '.unpublished/**/index.md*' | awk '{print $NF}' ``` If that is empty too, fall back to the last commit (`git diff --name-only --diff-filter=ACM HEAD~1 HEAD -- 'blog/**/index.md*' '.unpublished/**/index.md*'`) and, before editing anything, tell the user which articles you picked. ## 2. Run the check ```bash yarn links:check ``` It prints the internal links already present, marks with `!!` any link pointing to no published article (a wrong slug — fix those), and lists candidate targets ranked by score. When the local AnythingLLM instance answers, the output carries a second signal, **meaning**: - each word-based candidate gets a `meaning: close (0.xx)` or `meaning: not close` line, and the close ones are listed first — a `not close` candidate matched on words only ("command line", "file") and is usually noise; - a final list, *Close in meaning, not linked*, names articles about the same topic that the word-based pass missed, with the section of this article they relate to — the place a link would go. If that block is missing, AnythingLLM is off: the lexical list alone is still valid. A stale index (`ai-index` not run since the latest posts) can miss recent articles as targets. ## 3. Choose the links — judgment, not the score **The candidate list is a hint, not a spec.** It ranks on shared words and tags, so it happily proposes four unrelated posts that merely say "docker image", while missing the one link the prose is begging for. The meaning signal narrows that gap; it does not close it. So: - Read the article. Look for the moments where it *names* something covered elsewhere: a tool, a prerequisite, a concept explained in an earlier post, a natural follow-up. - Cross-check the memory blog map (`/home/node/.claude/projects/-opt-docusaurus/memory/project_blog_map.md`) for same-`mainTag` and same-series posts the script may have scored too low. - Aim for 2 to 4 links. Prefer few and relevant over many and weak — a link the reader does not need is noise, and this rule exists to help readers, not to satisfy a counter. - If the article genuinely has no relevant neighbour, say so instead of forcing a link. ## 4. Insert them - Inline, **at the first place the term appears in the prose**. Never a "See also" list at the end: the `RelatedPosts` grid already covers the bottom of the page. - Use the globally registered component, no import needed: `the natural words already in the sentence`. - Do not rewrite the sentence around the link, and do not add a sentence just to host one. If no sentence can host it naturally, that link was not the right one. - Never link inside a fenced code block, a heading, or frontmatter. - Add the **reciprocal link** in the older article when it fits its prose — that is what keeps old posts alive. Same rules apply there. ## 5. Verify ```bash yarn links:check # for each article touched, including the reciprocal ones ``` Every link must point to an existing slug (no `!!` lines). If several articles were edited, or if any doubt remains on a slug, run `npx docusaurus build` — it fails on broken internal links. ## 6. Report For each article: the links added, with the anchor text and the target slug, and where the reciprocal link went. Mention explicitly any article you deliberately left with fewer than two links, and why. ```` Et `/todos:add` est la command derrière le script `todo_next_id.sh` vu plus haut — sa ligne `allowed-tools` nomme explicitement le script, si bien que la command peut l'appeler mais rien d'autre : **.claude/commands/todos/add.md** ````markdown --- description: Capture a new task into the .todos/ backlog as one well-formed NNNN-slug.md file argument-hint: allowed-tools: Bash(ls*), Bash(find*), Bash(.claude/scripts/todo_next_id.sh*), Read, Write --- # Add a TODO to the backlog Capture the task described in **$ARGUMENTS** as a single, well-formed backlog file. Do NOT implement anything — this only creates the entry. The header bullets below follow the **`todo-authoring`** skill's contract — load it if any field's meaning is unclear (e.g. what counts as a valid `Depends` value). ## Procedure 1. **Resolve the description.** - If `$ARGUMENTS` is empty, ask the user for a one-line task description and stop until you have it. - Derive a concise Title (imperative, ≤ ~10 words) and a kebab-case `slug` (lowercase, alphanumerics + dashes, no leading article, ≤ ~6 words). 2. **Locate the backlog.** - Use `.todos/` at the repository root. If it doesn't exist, create it. 3. **Dedupe (cheap guard).** - List existing `.todos/*.md` and read their Titles (first `# ` heading). - If the new task clearly duplicates an existing entry, STOP and tell the user which file already covers it — don't create a second one. 4. **Compute the next ID.** - Run `.claude/scripts/todo_next_id.sh .todos` — it scans every `.todos/*.md` file (including status subfolders and any numeric-prefixed name like `000_...`) and prints `max + 1`, zero-padded to 4 digits (`NNNN`). Never compute this by hand, and never reuse an existing ID. (Legacy 3-digit IDs from before this width change stay as they are — never rename them.) 5. **Write `.todos/NNNN-.md`** with the skeleton below. The filename is an **identity**: never encode priority, status, or batch in it. ```markdown # NNNN — - **Priority**: medium - **Batch**: unassigned - **Depends**: — - **Files**: TBD ## Context <1–3 sentences: what and why, drawn from the description and the current conversation.> ## Acceptance - [ ] <concrete, verifiable outcome> ``` Fill Context/Acceptance from `$ARGUMENTS` and the conversation. Leave `Priority` / `Batch` / `Depends` / `Files` at their defaults for the user to adjust — these header bullets are what `/todos:plan` parses. 6. **Report and hand off.** - Print the created path and the Title. - Remind the user that `.todos/plan.md` is a generated projection — run `/todos:plan` to regenerate it. Offer to run it now. ```` ## Tracer une limite infranchissable : les hooks {#draw-a-hard-line-hooks} Le prompt d'une slash command reste une instruction que Claude lit et dont il peut, dans une session suffisamment longue, discrètement s'écarter — comme une personne oublie une consigne donnée une heure et de nombreux messages plus tôt. Un **hook** fonctionne autrement : c'est une commande shell que le harness Claude Code (le programme qui exécute la session, distinct de Claude lui-même) lance automatiquement à un point fixe — avant que Claude lise un fichier ou exécute une commande (ce que Claude Code appelle un *tool call*), juste après, ou à la fin de la session — et son code de sortie décide de la suite. Claude n'a aucun mot à dire dans cette décision. C'est la même idée qu'un hook Git `pre-commit` dans [un article précédent](/blog/git-precommit), une couche plus bas : celui-là intercepte un commit, celui-ci intercepte un seul tool call. Le fichier `.claude/settings.json` de ce blog n'en contient encore aucun — il ne porte qu'une liste `permissions.allow`. Les hooks ont gagné leur place sur un autre projet, en Python, qui lance `ruff` et `pytest` en permanence pendant une session. Réduit à la mécanique de base : **.claude/settings.json (illustrative — Python/Bash project)** ```json { "permissions": { "deny": ["Read(./.env)", "Read(./**/*.key)", "Read(./secrets/**)"] }, "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "cmd=$(jq -r '.tool_input.command'); if [[ \"$cmd\" == pytest* ]]; then echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\"}}'; fi" } ] } ], "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "f=$(jq -r '.tool_input.file_path'); if [[ \"$f\" == *.py ]]; then ruff check \"$f\"; fi" } ] } ], "Stop": [ { "hooks": [{ "type": "command", "command": "pytest -q" }] } ] } } ``` Quatre idées à reprendre, quel que soit le langage du projet : * **`permissions.deny`** bloque net `Read` sur les secrets — un hook n'est même pas sollicité, le tool call n'a jamais lieu. * **`PreToolUse`** approuve ici automatiquement `pytest` en particulier, pour qu'une session longue et soignée ne s'arrête pas sur une demande de confirmation chaque fois que la suite de tests tourne. * **`PostToolUse`** réagit dès qu'un fichier Python est écrit ou modifié : il lance `ruff check` sur ce seul fichier, immédiatement, au lieu d'attendre que le lecteur — ou un job de CI — s'en aperçoive plus tard. * **`Stop`** se déclenche une fois, quand Claude considère une tâche terminée et s'apprête à rendre la main — le dernier point de contrôle, pas un par édition. Ici, il relance toute la suite de tests ; si `pytest` échoue, le code de sortie non nul du hook indique à Claude que son tour n'est pas vraiment fini, et pourquoi. C'est le seul hook capable de contredire un « j'ai terminé ». > **Un hook est un plancher, pas un substitut:** > > Un hook ne se déclenche que sur la forme de tool call à laquelle il est associé — il ne comprend pas > l'intention comme le fait un modèle qui suit une skill. Réservez-le à la poignée de règles qui ne > doivent jamais être négociables (secrets, lint ou tests obligatoires) ; laissez tout ce qui demande > du jugement à une skill. ## Encoder vos standards : les skills, vérifiés par des agents {#encode-your-standards-skills-checked-by-agents} Une **skill** est l'endroit où « du bon Bash » ou « un bon Dockerfile » est réellement écrit — une fois, comme source unique de vérité, au lieu d'être retapé dans le chat à chaque session. Voici la vraie skill `bash-best-practices` de ce blog, en intégralité — elle commence par un champ `description` et un flag `disable-model-invocation`, puis les conventions elles-mêmes : **.claude/skills/bash-best-practices/SKILL.md** ````markdown --- name: bash-best-practices description: Generic Bash best practices — quoting, set -o nounset/errexit, [[ ]] vs [ ], $() vs backticks, explicit error handling, shellcheck-clean patterns, and common pitfalls. Language-level rules, not any one project's conventions. disable-model-invocation: false --- # Bash Best Practices Generic Bash rules applicable to any project. This skill covers only what's true of Bash itself — check whether the current project has its own conventions skill (logging system, sourcing pattern, function naming, status files, etc.) and defer to it for anything project-specific. ## Recommended script setup ​```bash #!/usr/bin/env bash set -o nounset set -o errexit ​``` A widely-used default for scripts that should fail fast and never silently operate on an unset variable. Not universal law — some scripts deliberately omit `errexit` because they must keep going despite a failing step. Check what the current project actually does before assuming. `pipefail` (`set -o pipefail`) is a separate opt-in: without it, a failing command in the middle of a pipe does not by itself abort the script. Check whether the current project sets it before relying on that behavior. ## Variables and scoping **Local scope, `readonly` unless reassigned:** ​```bash function my_function() { local -r arg1="${1}" ... } ​``` **Undefined variable error (under `set -o nounset`):** ​```bash echo "${UNDEFINED_VAR}" # Error under nounset value="${UNDEFINED_VAR:-default}" # OK: provides a default ​``` ## Error handling — principle Propagate, log, and return/exit explicitly — never swallow an error silently: ​```bash # Bad: swallowed error result=$(some_command) || echo "Error" # Error not propagated # Good: propagate, log, and return/exit explicitly result=$(some_command) || { echo "Command failed" >&2; return 1; } ​``` Guard-clause functions (argument-count/non-empty checks at the top of a function, returning or exiting early on failure) are a robust, common pattern. Check whether the current project already has its own assertion helpers before writing ad hoc `[[ ]]` checks everywhere. ## Bash patterns (shellcheck-clean) **Variable quoting (SC2086):** ​```bash # Bad: unquoted variable splits on whitespace and globs files=$file_list for f in $files; do ... # a filename with a space becomes two iterations rm $file # if $file="a b", removes two files # Good: quote scalars; use a real array to iterate a list rm "${file}" files=(a b "c d") # array, not a space-joined string for f in "${files[@]}"; do ... # each element stays intact, even with spaces ​``` Note: `for f in ${var}` (braces but no quotes) still word-splits — braces are not quoting. Quote (`"${var}"`) or use an array (`"${arr[@]}"`). **Command substitution:** ​```bash # Preferred result=$(command) # Avoid (deprecated) result=`command` ​``` **Conditional:** ​```bash # Good if [[ -f "${file}" ]]; then ... if [[ "${var}" == "value" ]]; then ... # Avoid if [ -f ${file} ]; then ... # Unquoted, uses [ instead of [[ ​``` **Checking `$?` explicitly (SC2181)** — prefer checking a command's exit status directly: ​```bash # Preferred if command; then ... # If you must defer the check (e.g. after an intermediate command that consumes it), # justify the shellcheck disable inline # shellcheck disable=SC2181 if [[ $? -ne 0 ]]; then ... ​``` ## Shellcheck directives Use sparingly, and always justify inline with a comment — a disable without a reason invites someone to copy it somewhere it doesn't apply. ## Common pitfalls | Pitfall | Fix | | --- | --- | | `if $? -eq 0` (checking exit after other commands ran in between) | Check immediately: `if command; then ...`, or `# shellcheck disable=SC2181` right before the check if you must defer it | | Unquoted variables | Always quote: `"${var}"` | | Assuming `pipefail` is set | It's an explicit opt-in (`set -o pipefail`), not a Bash default — check the current project | | `` result=`command` `` (backticks) | Prefer `result=$(command)` — nests cleanly, easier to read | ## Pipes (SC2094) ```bash # Good grep pattern < file | sort # Bad (sometimes) sort < file | grep pattern > file # Overwrites input ``` ## String escaping for generated scripts When Bash writes shell code (e.g., via Jinja2 template), escape carefully: ```bash # Template generating a shell script cat > "${output_script}" << 'EOF' #!/bin/bash log::info "Running ${job_name}" EOF # Single-quote the heredoc (EOF) to prevent variable expansion ``` --- Remember: Bash is fragile. Defensive programming (quoting, guard clauses, explicit error handling) prevents silent failures. ```` Le champ `description` a un double rôle : c'est ce qu'un humain lit pour savoir ce que couvre la skill, et c'est ce que Claude compare à la tâche courante pour décider de la charger *sans qu'on le lui demande*. Un flag contrôle cela : `disable-model-invocation: false` garde le chargement automatique actif ; passez-le à `true` et la skill ne se charge que si une command l'appelle explicitement. Une skill seule n'est qu'une mémoire plus stricte. Ce qui l'a transformée en véritable audit, c'est un **agent** assorti — un sous-agent avec un nom, un jeu d'outils fixe et étroit, et une seule mission : **.claude/agents/bash-best-practices-reviewer.md** ````markdown --- name: bash-best-practices-reviewer description: Read-only audit of a project's Bash scripts against the bash-best-practices skill and a configurable size ceiling. Checks set -euo pipefail, grep-under-pipefail safety, heredoc placement, and file size; reports violations with file:line, never edits. tools: Read, Grep, Glob, Bash model: sonnet --- # Bash Best-Practices Reviewer Read-only sweep of a project's shell layer against the `bash-best-practices` skill — read it first, it is the source of truth; this agent only says where to look and how to report. **Detection only: report violations, never edit.** A project may also ship its own bash-conventions rule/skill with extra gates (exit-code contract, sourcing/loader pattern, project-specific traps) — read it too if present and fold its gates in. ## Why an isolated agent The sweep reads many shell files that are irrelevant to the rest of the work. Running it in a bubble keeps that noise out of the main context — only the findings return. ## Scope Default to every tracked `*.sh` under the project's shell directories. If the project names specific roots (a bash layer that ships into containers, a scripts dir), restrict to those. ## Methodology Read `bash-best-practices` (and any project bash rule), then check each script: 1. **Mandatory safety header.** The project's shebang + `set -euo pipefail`. Flag any executable script missing `set -euo pipefail`. ```bash for f in $(grep -rl '' . --include='*.sh'); do grep -q 'set -euo pipefail' "$f" || echo "MISSING pipefail: $f"; done ``` 2. **`grep` under pipefail.** A `grep` whose no-match exit (1) would abort the script must end with `|| true`. Flag bare `grep` in assignments / conditionals that can legitimately match nothing. 3. **Heredocs / inline multiline text.** Long heredoc prose belongs in a templates directory, referenced by path — not inline. Flag large heredocs. 4. **Size ceiling.** Report any `.sh` over the project's soft ceiling (default ~300 lines) as a split candidate along function seams (soft, not a hard failure). ```bash find . -name '*.sh' -exec wc -l {} + | awk '$1>300' ``` 5. **Project gates.** If the project's bash rule defines an exit-code contract, a sourcing/loader pattern, or known traps, check those too and cite the rule. ## Output format Group by severity. ### Summary Scripts swept; blocking vs soft violations. ### Blocking violations ``` ❌ path/to/foo.sh:12 missing `set -euo pipefail` ``` ### Anomalies (grep||true, heredoc, project traps) ``` ⚠️ path/to/baz.sh:70 grep without `|| true` under pipefail ``` ### Size overruns (soft) ``` 📏 path/to/qux.sh 341 lines (>300) — split candidate along function seams ``` ### Final confidence Sweep complete? Zero violations is a valid outcome. ```` Deux choix de conception pèsent plus lourd que la longueur du fichier ne le suggère : * **`tools: Read, Grep, Glob, Bash`** — pas d'`Edit`, pas de `Write`. Cet agent ne peut physiquement pas modifier un fichier, ce qui veut dire qu'une review ne peut jamais dériver accidentellement en réécriture, et qu'une chaîne hostile enfouie dans un fichier relu ne peut pas l'y pousser non plus. * **Un agent n'est accessible qu'à travers une slash command correspondante** — `/review:bash` est la seule porte d'entrée. Il n'y a aucune entrée d'autocomplétion pour l'agent lui-même ; la découvrabilité vit entièrement dans la couche command, la logique de review entièrement dans la couche agent. Le même motif se répète pour `python-best-practices` (couplée à `python-best-practices-reviewer`, surtout utile sur les points que [`ruff` et `mypy` n'attrapent pas déjà](/blog/python-qa)) et pour `dockerfile-best-practices`. Une skill, un agent étroit, une command — une forme qui passe à l'échelle pour n'importe quel langage ajouté ensuite. ### Un cas concret : le contrôle de Time-to-Value de ce blog {#a-concrete-case-this-blogs-own-time-to-value-check} Tous les couplages vus jusqu'ici relisent du code. Sur ce blog, avant de publier un article, je lance `/review:reader` — il charge la skill `reader-first-docs` et vérifie la structure du post. L'un des critères mesurés est le Time-to-Value : dans les 30 premières secondes de lecture environ, un lecteur doit déjà comprendre ce que couvre l'article et s'il vaut la peine de continuer. Un score TTV *élevé* signifie l'inverse — le lecteur a dû traverser trop de texte avant d'arriver au moment « ah, *voilà* de quoi parle l'article ». Voici la skill en intégralité : **.claude/skills/reader-first-docs/SKILL.md** ````markdown --- name: reader-first-docs description: Time-to-value audit methodology for Docusaurus blog articles — front-load proof, keep install/code before the first result visible, measure TTV mechanically from line positions. Defines the four passes (mechanical, repetition, author's-journey noise, qualitative), the metric thresholds, and the OK/MINOR/RESTRUCTURE verdict. Docusaurus-specific (MDX components, <!-- truncate --> anchor, AlertBox, Terminal, Snippet). For the audit workflow (journal, batch selection, TODO filing), invoke via /review:reader, not directly. disable-model-invocation: false --- # Reader-First Docs — Docusaurus Blog Load before auditing an article under `blog/` or `.unpublished/`. This skill defines the **methodology** only — what to measure and how to judge. The `/review:reader` command owns the workflow: which articles to audit, in what order, and how to track progress across sessions. The target structure every article should converge toward is defined in `blog-post-structure` — load it too if the nature of a required restructure is unclear. While reading an article's `<Terminal>`/`<Snippet>` blocks for Pass 1 below, also note a value the reader would change (a port, a container name, a version) hardcoded and repeated across 2+ commands instead of declared once with `<Vars>` — `blog-post-structure`'s self-check names this explicitly; a `/review:reader` pass is a natural place to catch an older article that predates the component. ## The principle: Time-to-Value (TTV) A reader arrives with one question: *should I keep reading, and can I get value fast?* Every line they scan before seeing a result is a line they might leave on. Structure the article so the **result comes first** — literally, in reading order — not after the prerequisites, the theory, or the install section. A Docusaurus article has a natural dividing line: `<!-- truncate -->`. Everything before it is the excerpt the reader sees on the blog index page. Everything after is the body the committed reader opens. The body must prove its value **early** — not after a long install section, not after a code dump the reader has no context for, not after a recap of what they just read in the TLDR. ## Article structure anchor points ```bash grep -n '<!-- truncate -->\|^## \|<Terminal\|<Snippet\|<Prerequisite\|apt install\|^!\[\|```mermaid\|```plaintext\|<ProjectSetup\|<StepsCard\|<QuickJump' <path> wc -l <path> ``` Let `T` = line of `<!-- truncate -->`, `E` = last line of the file, `BODY = E − T`. **`<QuickJump>` is scaffolding, not content.** If a `<QuickJump ... />` sits between `T` and the first real content line, move `T` to the line right after it before computing anything — it never counts as install/abstraction/proof and never inflates `BODY` or the TTV percentage. Treat it the same way the excerpt before `<!-- truncate -->` is treated: present, useful to the reader, outside the measurement. All three passes below work from `T` as the zero point. The excerpt before `T` is not evaluated — it is already the reader's entry point and has its own quality signal (the 30-second test reads it). ## Pass 1 — Mechanical (position-based, not a full read) Compute from line positions only. This is what keeps a batch cheap. **"Proof"** is the first of these strictly after `T`: - a `<Terminal>` showing **output** — not one whose content is only an install command (`apt install`, `yarn add`, `docker pull`); - an image of a result (`![…](./images/…)`), excluding the banner image above the TLDR; - a `plaintext` / `mermaid` block showing the outcome; - a before/after pair. When in doubt about a `<Terminal source="./files/x.txt">`, read that one file — it is short. If no proof exists anywhere in the article body, `TTV = 100%` — say so explicitly. | Metric | Computation | Threshold | | --- | --- | --- | | **Time to value (TTV)** | `(proof_line − T) / BODY` | 🟢 < 15% · 🟠 < 30% · 🔴 ≥ 30% | | **Install-before-proof** | a `<Prerequisite>`, `apt install`, or `## Prerequisites` heading sits before `proof_line` | red flag (binary) | | **Abstraction-before-proof** | a `<Snippet>` of implementation sits before `proof_line` | red flag (binary) | | **Unmarked deep-dive** | a section covering internals whose title carries no "optional / skip / under the hood" signal | count | | **Landing** | do the last three paragraphs recap *and* point somewhere next? | yes / no | ## Pass 2 — Repetition ("is this information new?") TTV gets the reader in; repetition drives them back out. An article that says the same thing four times reads as padded, and it is usually 15–20% longer than it needs to be. Method — mechanical, no full read required: 1. Extract the article's **core claims**: take the `<TLDR>`, the `## ` headings, and any takeaways card. That is typically 4 to 6 distinct facts. 2. `grep -in` a distinctive phrase for each claim across the file, and count where it lands. 3. Measure the weight of each section: ```bash awk -v t=$T 'NR>=t' <path> | awk '/^## /{if(s)print s" | "w" words"; s=$0; w=0; next} {w+=NF} END{print s" | "w" words"}' ``` | Metric | Computation | Threshold | | --- | --- | --- | | **Redundancy** | occurrences of the most-repeated core claim | 🟢 ≤ 3 · 🟠 4-5 · 🔴 ≥ 6 | | **Dead-weight block** | a summary/takeaways block whose every bullet restates a fact already in the body | yes / no | | **Duplicated warning** | two `<AlertBox>` carrying the same caution in different sections | count | Frequent shapes worth naming in the report: - a takeaways card mirroring the body one-for-one; - a bullet list whose items rephrase the section headings (adds no new clause); - an intro sentence that restates its own heading; - a step-by-step list followed by one subsection per step (the list was the summary, the sections are the depth — the list is redundant, cut it or turn it into a `<StepsCard>`); - the same caution in two `<AlertBox>` — a classic side effect of a past restructuring. **Count facts, not words.** A short section that adds nothing is worse than a long one that teaches something. Say what a further cut would *cost* rather than chasing a percentage — if the remaining length is all information, say so plainly. A section being long is not by itself a finding. ## Pass 2b — Author's-journey noise Repetition pads an article; the author's journey *discourages* the reader. A guide that keeps saying "this surprised us" reads as "this is fragile, you will suffer too" — and the reader leaves before the payoff. Hunt it separately, because pass 2 will not catch it: each sentence is unique, so redundancy scores 🟢 while the article still bleeds readers. Mechanical sweep: ```bash grep -niE "found out|turns out|I tried|hard way|bit me|at first|I expected|I only know|to my surprise|my first attempt|worth knowing you" <path> ``` Then classify each hit — the grep finds candidates, it does not judge them: | Shape | Verdict | Fix | | --- | --- | --- | | First-person **judgment** — "I reach for the light one by reflex" | keep | this is the blog's voice | | First-person **chronology** — "I found out the hard way while building it" | cut | keep the fact, drop the story | | A bug the article already works around in its own script | cut from prose | the explanation belongs in the script's comment | | Engineering choices justified to the reader — why a layer sits where it sits, what was rejected | cut | that is a code-review conversation | | A demonstrated result then hedged — "that is a stronger claim than it looks" | cut the hedge | state the limitation once, flatly, as a property of the tool | | Metric | Computation | Threshold | | --- | --- | --- | | **Journey noise** | passages classified *cut* above | 🟢 0 · 🟠 1-2 · 🔴 ≥ 3 | | **Failure stories** | `<AlertBox>` whose subject is something that went wrong | 🟢 ≤ 1 · 🔴 ≥ 2 | Full rationale: `writing_style` memory (*No Trial-and-Error Narrative*), [[feedback_article_weight]], and `.claude/rules/blog-prose.md`. ## Pass 3 — The 30-second test (qualitative) Read **only** the title, the frontmatter `description`, and lines `T` to `T + 40`. Nothing else. Then answer as someone who has never heard of this tool: - **Do I keep reading, or do I leave?** - **Why**, in one sentence. - What is the article asking me to do before it has shown me anything? This is the heart of the audit. The mechanical metrics of passes 1 and 2 only make the judgment reproducible from one article to the next — they never override it. An article can score 12% TTV and still lose the reader if the opening never says what the thing is *for*. ## Verdict Exactly one per article: | Verdict | Condition | Action | | --- | --- | --- | | `OK` | proof within the first screen, no red flag, redundancy 🟢, the 30-second test says "keep reading" | journal only | | `MINOR` | sound structure, one weakness (unmarked deep-dive, flat landing, proof slightly late but 🟠, redundancy 🟠, one dead-weight block, journey noise 🟠) | journal + one-line note, **no TODO** | | `RESTRUCTURE` | TTV 🔴, **or** install-before-proof, **or** abstraction-before-proof, **or** redundancy 🔴, **or** journey noise 🔴, **or** failure stories 🔴, **or** the 30-second test says "I leave" | one TODO | `MINOR` never produces a TODO: a batch sweep over 40+ articles would bury real findings under one-line files for every article with a single flat ending. For `RESTRUCTURE`, file one TODO (format and language: see `/review:reader`). The proposed fix is always a **reorder**, never a cut — name which existing section/line range each reordered block comes from, so nothing reads as discarded. **One exception: journey noise.** There the fix *is* a cut, because the passage carries no reader value to relocate. Quote each offending sentence in the TODO with its line number and the fact it was carrying, so the cut is reviewable rather than a blanket "trim the prose". ## What this does NOT mean - **Don't delete reference material.** TTV is about *order*, not *completeness*. Advanced sections still have a place — they just aren't the first thing every reader sees. - **Don't compress the opening into a one-liner.** A stranger must understand what the article teaches without already knowing the tool — not just recognize it from the title. - **A long section is not a finding.** A deep-dive covering internals is legitimately the bulk of a good technical article; flag it only when its title carries no signal that it is optional. ```` La métrique centrale, le Time-to-Value, est mécanique, pas une impression : on trouve `T`, la ligne de `<!-- truncate -->` ; on trouve la première vraie *preuve* après elle — une sortie de commande réelle, jamais une simple étape d'installation ; on calcule `TTV = (proof_line − T) / BODY`, où `BODY` est tout ce qui suit `T`. C'est exactement le calcul derrière la démo d'ouverture : sur [l'article sur le pre-commit](/blog/git-precommit), la sortie réelle arrive dix lignes après `<!-- truncate -->`, `BODY` fait 148 lignes, donc `TTV ≈ 7 %` — largement sous le seuil vert de 15 % défini par la skill elle-même. Trois paliers transforment ce pourcentage en verdict : 🟢 `OK` sous 15 %, 🟠 `MINOR` sous 30 % (une note de journal, jamais un TODO, et c'est voulu — sinon un passage sur 40 articles enterrerait les vraies trouvailles sous des one-liners pour chaque post à la conclusion simplement plate), 🔴 `RESTRUCTURE` à 30 % ou plus, le seul palier qui crée un TODO. > **Toutes les skills ne vont pas de pair avec un agent:** > > `/review:reader` ne délègue pas à un agent isolé comme le fait `/review:bash` — il a besoin d'un > accès `Write` pour journaliser l'avancement d'un lot entier dans > `.todos/0000-reader-review-journal.md`, donc il tourne inline, même skill, sans couche d'isolement. > Un agent `reader-first-docs-reviewer` existe bien dans ce projet, mais il audite un autre périmètre > (de la documentation longue comme un README, pas des articles de blog) et n'est encore relié à > aucune command. La forme décrite dans la section précédente est courante, pas universelle. ## Toujours actives, sans invocation : les rules {#always-on-no-invocation-needed-rules} Les skills et les agents attendent tous les deux un déclencheur — une tâche qui correspond, ou une command tapée. Une **rule** n'attend rien : elle se charge à l'instant où un fichier correspondant est ouvert ou modifié, par glob sur `paths`, sans aucune invocation. Voici le vrai fichier — sa première ligne renvoie déjà vers l'endroit où vit réellement le raisonnement : **.claude/rules/bash.md** ```markdown --- paths: - "**/*.sh" - "**/*.bash" - "**/.bash_aliases" --- # Bash — always apply Full rationale and more patterns: `bash-best-practices` skill. Verify: `shellcheck`/`shfmt`. - ✅ DO: `set -o nounset` + `set -o errexit` (+ `pipefail` if the project relies on failing mid-pipe) at the top of every executable script. - ✅ DO: quote every variable expansion — `"${var}"`, `"${arr[@]}"` for arrays. ❌ DON'T: `rm $file` / `for f in $files` — unquoted, splits on whitespace and globs. - ✅ DO: `result=$(command)`. ❌ DON'T: `` result=`command` `` (deprecated backticks). - ✅ DO: `if [[ -f "${file}" ]]`. ❌ DON'T: `if [ -f ${file} ]` (unquoted, `[` not `[[`). - ✅ DO: propagate errors explicitly — `cmd || { echo "failed" >&2; return 1; }`. ❌ DON'T: `result=$(cmd) || echo "Error"` — swallows the failure, doesn't return/exit. - ✅ DO: check exit status immediately — `if command; then`. Only defer to `$?` with an inline `# shellcheck disable=SC2181` justification. - ✅ DO: a `grep` inside a `set -o pipefail` script that can legitimately match nothing ends with `|| true` — otherwise its exit 1 aborts the script. ``` Une rule ne répète pas le raisonnement de la skill, elle en extrait les paires DO/DON'T brutes dont un modèle a besoin en pleine édition, et renvoie vers le *pourquoi*. La même forme couvre le Markdown, en gardant le gras en `**astérisques**` et jamais en `__underscores__`, là où [un linter volontairement désactivé](/blog/markdown-lint) laisserait sinon passer les deux en silence. J'ai écrit les rules en dernier, et je me suis ensuite à moitié demandé si elles n'auraient pas dû venir en premier — une rule a l'air d'être la couche la plus « basique ». Mécaniquement, elle ne l'est pas : une rule est *définie* comme un court extrait de sa skill sœur, donc la skill doit exister avant qu'il y ait quoi que ce soit à extraire. Écrire les skills et les agents avant les rules, même par accident, s'est révélé être le seul ordre qui fonctionne vraiment. ## Tout relier ensemble {#wiring-it-all-together} Une fois que quelques langages ont partagé la même forme skill → agent → command, la carte est devenue la documentation : | Command | Agent | Skill | | ---------------- | ------------------------------------ | --------------------------- | | `/review:bash` | `bash-best-practices-reviewer` | `bash-best-practices` | | `/review:python` | `python-best-practices-reviewer` | `python-best-practices` | | `/review:docker` | `dockerfile-best-practices-reviewer` | `dockerfile-best-practices` | Chaque ligne est indépendante et peut être ajoutée seule — une quatrième ligne pour un nouveau langage coûte un fichier skill, un fichier agent et un fichier command, dont aucun ne touche aux trois autres. ## Sous le capot (passez si vous voulez juste l'utiliser) {#under-the-hood-skip-this-if-you-just-want-to-use-it} ### Pourquoi les agents reviewers ne peuvent pas éditer {#why-the-reviewer-agents-cant-edit} `Read, Grep, Glob, Bash` et rien d'autre, ce n'est pas de la prudence pour la prudence. Tout le travail d'un agent de review est de lire des fichiers qu'il n'a pas écrits et de les juger — dès qu'il peut aussi `Edit`, le « détection seulement » cesse d'être une garantie imposée par la liste d'outils pour devenir une consigne que le prompt se contente de demander. Garder les deux séparés signifie qu'on peut faire confiance au rapport d'un reviewer pour être exactement cela : un rapport, pas un diff déjà appliqué sans second regard. ### Le script à zéro token, une fois de plus {#the-zero-token-script-one-more-time} `todo_next_id.sh` mérite un deuxième coup d'œil parce qu'il se généralise bien au-delà de la numérotation des TODO : **.claude/scripts/todo\_next\_id.sh** ```bash #!/usr/bin/env bash set -euo pipefail # Prints the next .todos/ backlog ID, zero-padded. # # Scans every *.md under the given directory and its subfolders (DONE/, # PARTIAL/, BLOCKED/, UNNEEDED/, POSTPONED/, ...), extracts the leading digit # run of each filename regardless of a status prefix, and prints max + 1. # Existing 3-digit IDs and new wider ones coexist fine — only the numeric # value is compared, never the string width. # # The prefix is stripped by matching "everything up to the first digit", # not by enumerating specific prefix words/separators — so it copes with a # one-word prefix (DONE_, PARTIAL-), a multi-word one (WONT_DO_, wont-do-), # or no prefix at all, uniformly. A digit run elsewhere in the filename # (a year, an HTTP status code, ...) is never mistaken for the ID, because # only the run immediately following that first stripped prefix is captured. # # Usage: todo_next_id.sh [todos-dir] [width] # todos-dir defaults to ./.todos # width zero-padding width for the printed ID, defaults to 4 todos_dir="${1:-.todos}" width="${2:-4}" max=0 if [[ -d "${todos_dir}" ]]; then highest="$(find "${todos_dir}" -type f -name '*.md' -printf '%f\n' | sed -nE 's/^[^0-9]*0*([0-9]{3,}).*/\1/p' | sort -n | tail -1)" [[ -n "${highest}" ]] && max="${highest}" fi # 10# forces base-10 parsing, otherwise a leading zero (e.g. 020) is read as octal printf "%0${width}d\n" $((10#${max} + 1)) ``` Deux lignes de `find`/`sed`/`sort` répondent à « quel est le prochain ID libre » avec une certitude totale — aucun risque d'hallucination, puisqu'il n'y a aucun modèle sur le chemin. Son voisin `todo_lock.sh` fait la même chose pour un problème plus dur, la concurrence : deux sessions Claude Code qui courent après le même numéro de TODO le résolvent avec `mkdir` (atomique — il échoue si le dossier existe déjà), pas en demandant à l'une des sessions de « vérifier d'abord ». Chaque fois qu'une étape se réduit à *compter, parser, verrouiller, hasher, differ* — prenez un script avant de prendre un prompt. ## Conclusion {#conclusion} L'ordre qui a réellement fonctionné ici a été : commands, puis hooks, puis skills couplées à des agents, puis rules — et avec le recul, ce n'est pas un accident à corriger : une rule est mécaniquement un extrait d'une skill, donc skills-avant-rules était le seul ordre viable. Ce qui a changé, ce n'est pas que Claude est devenu plus intelligent de session en session — c'est que le règlement a cessé de vivre dans le chat pour rejoindre la couche faite pour le porter : une limite nette dans un hook, un standard dans une skill, une vérification isolée dans un agent, un réflexe dans une rule, et, partout où la tâche relevait de la pure logique, un script qui n'a jamais rien demandé au modèle. Une fois qu'un tel dispositif existe, la question suivante est ce qu'il coûte à faire tourner — et c'est précisément là que [les astuces d'économie de tokens d'un article précédent](/blog/claude-ia-spare-tokens) prennent le relais. ---