← Retour à la documentation

Guide 08 Méta, le stack du site lecture, 18 min

Mon stack Claude Code, en éprouve.

Ce site a un dossier .claude/ et un fichier CLAUDE.md à sa racine. Ils ne servent à rien aux visiteurs, mais ils font tourner la maintenance éditoriale. Cette page expose chaque pièce du stack, dans son code source. Si tu veux reproduire un mécanisme dans ton propre projet, recopie le bloc qui t'intéresse et adapte. C'est fait pour ça.

Stack monté le 18 mai 2026, en phase d'éprouve pendant 30 jours. Le 18 juin, je révise : ce qui n'aura pas été appelé sort. Cette page est une photo de l'état initial.

01

Pourquoi Claude Code, pas un chat dans un onglet

Claude Code est un agent en ligne de commande : il lit les fichiers du projet, modifie ceux que tu autorises, exécute des commandes shell, et observe le résultat avant de continuer. Trois différences avec un chat dans un onglet :

  • Le contexte du projet est natif. Pas besoin de copier-coller des fichiers, l'agent les ouvre lui-même. Il sait ce qui a changé entre deux commits, ce qui est dans .gitignore, quels tests passent.
  • Les boucles de feedback sont courtes. Il modifie un fichier, lance les tests, voit l'erreur, corrige. Ce qui prendrait quinze allers-retours dans un chat se fait en deux minutes.
  • Le cadre est explicite. Le fichier CLAUDE.md du projet décrit les conventions, le .claude/settings.json liste ce qui est autorisé, demandé ou refusé. L'agent reste dans ces bornes par défaut.

Pour le détail des concepts (skills, MCP, hooks, sous-agents), voir le guide Claude Code pour débutants. Cette page-ci montre comment je les utilise tous, ensemble, sur ce site précis.

02

Architecture du .claude/ de ce repo

Six couches, chacune avec un rôle clair. Plus c'est haut, plus c'est abstrait ; plus c'est bas, plus c'est atomique.

CLAUDE.md                 ← les règles du projet (lues à chaque session)
├── .claude/
│   ├── settings.json     ← permissions allow/ask/deny + hooks
│   ├── commands/         ← raccourcis : /serve, /verifier, /journal-nouveau
│   ├── skills/           ← workflows : journal-entree, audit-page, ...
│   ├── agents/           ← rôles spécialisés : relecteur-tonalite, ...
│   ├── hooks/            ← garde-fous : validate-journal.sh
│   └── mcp/              ← outils atomiques exposés par un serveur Python
│       └── ia-simplement-tools/
│           ├── server.py        ← 7 outils domaine (FastMCP)
│           ├── pyproject.toml
│           └── tests/test_outils.py
└── .mcp.json             ← Claude Code lance le MCP au démarrage

Comment ça s'enchaîne quand je tape /journal-nouveau :

  1. La slash command /journal-nouveau charge la skill journal-entree.
  2. La skill délègue l'interview au sous-agent redacteur-journal.
  3. Le sous-agent pose 5 questions à Pierre, produit un brouillon.
  4. La skill appelle l'outil MCP chercher_superlatifs pour valider le titre et le résumé.
  5. La skill appelle l'outil MCP creer_entree_journal qui écrit le fichier.
  6. Pierre lance git commit, le hook validate-journal.sh vérifie que l'index est cohérent. Bloque sinon.
03

Le CLAUDE.md de ce repo, commenté

Le fichier CLAUDE.md à la racine est lu automatiquement à chaque session. Il décrit les conventions et les invariants du projet. Voici le mien (extraits), suivi d'annotations.

# CLAUDE.md, brief Claude Code pour ce projet

## Ce que ce repo est

Site statique vanilla HTML/CSS/JS. Aucun build, aucune dépendance npm
en production. Servi tel quel par Vercel via vercel.json.

Cadrage en deux phrases. Claude sait qu'il ne doit pas chercher de package.json ni proposer npm install.

## Avant tout commit

1. Valider que journal/entries/index.json est du JSON :
   python3 -m json.tool < journal/entries/index.json > /dev/null
2. Pour chaque slug dans entries, vérifier que
   journal/entries/<slug>.md existe.

Les invariants critiques. Le hook validate-journal.sh automatise la vérification, mais avoir la règle écrite ici sert d'abord à informer l'agent, ensuite à documenter pour Pierre.

## Conventions de commit (en français)

- Impératif présent, minuscule en tête, ≤72 caractères.
- Pas de prefix conventional commits (feat:/fix:).
- Exemples : "ajoute entrée journal du 18 mai", "corrige lien sitemap".

Sans ces lignes, Claude propose par défaut du feat: add journal entry à l'anglaise. Trois lignes de cadrage suffisent à le ramener au style maison.

## Ton et rédaction

Bannir : "expert", "révolutionnaire", "unique", "incroyable",
"magique", "secret", "ultime", "parfait", "extraordinaire",
"best practices", "game changer". Pas d'emoji décoratif.

Préférer toujours [À COMPLÉTER PAR PIERRE] à une demi-vérité.

La règle de sobriété, en clair. Le sous-agent relecteur-tonalite et l'outil MCP chercher_superlatifs appliquent cette liste mécaniquement.

04

Mon MCP custom : ia-simplement-tools

MCP, pour Model Context Protocol, c'est le protocole qui permet de connecter à Claude des outils maison écrits dans le langage de son choix. Pour ce site, j'ai écrit un serveur Python qui expose sept outils domaine.

Liste des outils (chacun est un endpoint MCP que Claude peut appeler pendant une session) :

  • lister_entrees_journal() — renvoie le journal trié par date.
  • creer_entree_journal(date, titre, resume, contenu_markdown) — écrit un .md et met à jour index.json.
  • valider_index_json() — vérifie validité JSON et correspondance slugs ↔ .md.
  • auditer_page_html(chemin) — audit a11y, SEO, sobriété sur une page.
  • lister_pages_documentation() — renvoie les pages doc avec titre/description.
  • verifier_sitemap() — croise sitemap.xml avec les .html présents.
  • chercher_superlatifs(chemin) — traque les mots bannis.

Le code du serveur, en extrait, avec FastMCP :

from fastmcp import FastMCP
from bs4 import BeautifulSoup

mcp = FastMCP("ia-simplement-tools")

@mcp.tool()
def valider_index_json() -> dict:
    """Vérifie validité JSON + correspondance 1-pour-1 slugs/fichiers."""
    data = json.loads(JOURNAL_INDEX.read_text(encoding="utf-8"))
    entries = data.get("entries", [])
    slugs_index = {e.get("slug", "") for e in entries}
    fichiers_md = {p.stem for p in JOURNAL_DIR.glob("*.md")
                   if p.name != "_MODELE.md"}
    erreurs = []
    for slug in slugs_index - fichiers_md:
        erreurs.append(f"slug sans .md : {slug}")
    for nom in fichiers_md - slugs_index:
        erreurs.append(f".md sans entrée dans index.json : {nom}")
    return {"ok": not erreurs, "erreurs": erreurs}

if __name__ == "__main__":
    mcp.run()

Le serveur est déclaré dans .mcp.json à la racine du projet :

{
  "mcpServers": {
    "ia-simplement-tools": {
      "command": ".claude/mcp/ia-simplement-tools/.venv/bin/python",
      "args": [".claude/mcp/ia-simplement-tools/server.py"]
    }
  }
}

Le serveur tourne dans un venv Python local (zéro dépendance globale). Au démarrage de Claude Code, le serveur est lancé automatiquement et les sept outils sont disponibles dans la session.

Côté sécurité, deux garde-fous : validation stricte des chemins (refus des chemins absolus et de tout ce qui sort de la racine du site), et écriture restreinte au seul dossier journal/entries/. Le reste est en lecture seule.

Le serveur est testé par pytest sur un mini-site jouet en tmp_path, douze tests qui passent en moins d'une seconde.

Pourquoi un MCP, et pas juste un script Python en CLI

C'est la question la plus probable. Trois raisons :

  • Composabilité native avec Claude Code. L'agent appelle l'outil sans passer par un sous-shell ni un script bash intermédiaire. Une skill peut chaîner trois outils (valider_index_jsonverifier_sitemapauditer_page_html) sans gymnastique de parsing de sortie.
  • Description machine-lisible des outils. Le MCP expose à Claude la signature de chaque fonction, le type de chaque argument, la docstring. L'agent sait quoi appeler et comment, sans que je doive le décrire dans un prompt.
  • Réutilisabilité hors de ce projet. Si je monte un deuxième site avec la même logique éditoriale, je copie le dossier .claude/mcp/, j'ajuste deux constantes (SITE_ROOT, SUPERLATIFS_BANNIS), c'est fini. Là où un script bash maison demande de re-réfléchir à chaque fois.

Ce qu'un MCP n'apporte pas ici : il n'accélère pas l'exécution (le coût dominant est le LLM lui-même), et il n'apporte rien si on n'utilise pas Claude Code. C'est un choix qui se justifie par le contexte agent, pas dans l'absolu.

Pourquoi FastMCP et pas le SDK MCP officiel

FastMCP est un wrapper du SDK officiel qui supprime le boilerplate. Au lieu d'enregistrer manuellement chaque outil avec son JSON Schema, on écrit @mcp.tool() au-dessus d'une fonction Python typée et FastMCP génère le schéma à partir des annotations. Pour sept outils simples, c'est ~60 % de code en moins qu'avec le SDK pur.

05

Mes six skills (workflows)

Une skill, c'est un fichier Markdown dans .claude/skills/<nom>/SKILL.md qui décrit un workflow réutilisable. Chaque skill orchestre les outils MCP comme briques de bas niveau.

journal-entree — créer une entrée de journal en interview guidée

Délègue l'interview à redacteur-journal, valide titre et résumé avec chercher_superlatifs, écrit le fichier avec creer_entree_journal, propose un message de commit en français. Ne commit jamais elle-même.

audit-page — auditer une page HTML

Appelle auditer_page_html et chercher_superlatifs, met en forme un rapport markdown trié bloquant / à corriger / à savoir, propose les corrections une à une. Pour les superlatifs, délègue à relecteur-tonalite.

nouvelle-page-doc — créer une nouvelle page de documentation

Copie le gabarit d'une page doc existante, demande titre / description / slug / eyebrow numéroté, génère head complet avec og tags et canonical, ajoute la carte dans documentation/index.html et l'URL dans sitemap.xml, valide avec verifier_sitemap.

verifier-avant-push — checklist complète avant git push

Lit git diff --name-only HEAD, orchestre valider_index_json, verifier_sitemap, auditer_page_html et chercher_superlatifs sur les fichiers modifiés. Produit un rapport prioritisé. Ne lance jamais le push.

publier-demo — ajouter une démo Claude testée sur une page

Délègue à createur-demo qui interview Pierre sur un cas réel (prompt verbatim, modèle, résultat). Refuse les démos non testées. Insère un <figure class="demo-claude"> dans la page et ajoute le CSS associé si absent.

revue-tonalite — relire une page pour la sobriété

Appelle chercher_superlatifs sur la page, délègue les reformulations à relecteur-tonalite qui propose 2-3 alternatives neutres par occurrence avec recommandation, attend validation avant d'éditer.

06

Mes quatre sous-agents (rôles spécialisés)

Un sous-agent, c'est un agent spécialisé que la session principale peut invoquer pour déléguer un travail focalisé. Le sous-agent a son propre prompt système, ses propres outils, et son propre contexte. Mes quatre :

relecteur-tonalite — propose des reformulations neutres

Pour chaque mot banni trouvé, lit le paragraphe complet, propose 2-3 reformulations, indique la recommandée, attend validation. Tools : Read, Edit.

auditeur-qualite — audit complet avec inspection runtime

Croise l'audit statique (MCP) avec une inspection au runtime via curl sur le serveur local. Classe les findings par sévérité, propose un ordre de résolution. Tools : Bash, Read.

redacteur-journal — interview structurée pour une entrée

Pose 5 questions (ce que tu as fait, ce qui a marché, ce qui a raté, ta décision, la suite), produit un brouillon Markdown au format maison. Refuse de remplir à la place de Pierre : [À COMPLÉTER PAR PIERRE]. Tools : Read, Write.

createur-demo — produit une démo Claude à partir d'un cas réel

Interview sur un cas concret (prompt verbatim, modèle, résultat), génère le brouillon HTML d'un <figure class="demo-claude"> avec snippet SDK Python idiomatique. Refuse les démos non testées. Tools : Read, Edit, Bash.

07

Mon hook validate-journal.sh

Le hook est un shell qui se déclenche avant que Claude n'exécute un outil Bash. Il filtre lui-même : il ne fait quelque chose que si la commande contient git commit. Le script complet :

#!/usr/bin/env bash
set -euo pipefail

INPUT="$(cat)"
COMMAND="$(printf '%s' "$INPUT" | python3 -c \
  'import json, sys
try:
    d = json.load(sys.stdin)
    print(d.get("tool_input", {}).get("command", ""))
except Exception:
    print("")' 2>/dev/null || echo "")"

if [[ ! "$COMMAND" =~ git[[:space:]]+commit ]]; then
    exit 0
fi

INDEX_JSON="journal/entries/index.json"

if ! python3 -m json.tool "$INDEX_JSON" > /dev/null 2>&1; then
    echo "[validate-journal] $INDEX_JSON n'est pas du JSON valide." >&2
    exit 2
fi

MISSING="$(python3 <<'PY'
import json, os
with open("journal/entries/index.json", encoding="utf-8") as f:
    data = json.load(f)
missing = [e["slug"] for e in data.get("entries", [])
           if not os.path.isfile(f'journal/entries/{e["slug"]}.md')]
if missing:
    print("\n".join(missing))
PY
)"

if [[ -n "$MISSING" ]]; then
    echo "[validate-journal] Slugs sans .md correspondant :" >&2
    echo "$MISSING" >&2
    exit 2
fi

exit 0

Pourquoi un hook plutôt qu'un test CI : pour un site personnel, je veux que la garantie soit locale, immédiate, et indépendante d'une infrastructure externe. Le hook tourne sur ma machine, en moins d'une seconde, avant chaque commit.

08

Mes trois slash commands

Une slash command, c'est un raccourci dans la session Claude Code, défini par un fichier .md dans .claude/commands/. Trois pour ce site :

  • /serve — lance python3 -m http.server 8000 en arrière-plan et donne l'URL.
  • /verifier — exécute la skill verifier-avant-push (checklist complète avant git push).
  • /journal-nouveau — lance la skill journal-entree (interview + écriture du fichier).
09

Mon settings.json

Permissions explicites, séparées en trois catégories (extrait) :

{
  "permissions": {
    "allow": [
      "Bash(python3:*)", "Bash(uv:*)",
      "Bash(git status)", "Bash(git diff:*)", "Bash(git add:*)",
      "Bash(git commit:*)", "Bash(grep:*)", "Bash(find:*)",
      "WebFetch(domain:docs.anthropic.com)",
      "WebFetch(domain:modelcontextprotocol.io)"
    ],
    "ask": [
      "Bash(git push:*)", "Bash(git checkout:*)",
      "Bash(git reset:*)", "Bash(git rebase:*)"
    ],
    "deny": [
      "Bash(rm -rf /)", "Bash(rm -rf ~)", "Bash(npm:*)",
      "Bash(curl * | sh)", "Bash(sudo:*)"
    ]
  }
}

Choix conscient : git push reste dans ask plutôt que allow. Même si j'ai confiance dans le stack, je veux confirmer chaque déploiement à la main. Trente secondes plus tard, vercel.com a remis la production à jour.

10

Workflow type : publier une entrée de journal

La séquence exacte depuis la commande jusqu'au déploiement :

  1. Je tape /journal-nouveau.
  2. La skill appelle lister_entrees_journal, m'informe que la dernière entrée date de 6 jours.
  3. Le sous-agent redacteur-journal prend la main, pose 5 questions. Je réponds.
  4. Il me propose un brouillon. Je relis, j'ajuste un paragraphe.
  5. La skill me demande titre et résumé. Je propose un titre, elle vérifie via chercher_superlatifs. Pas de mot banni, OK.
  6. La skill appelle creer_entree_journal. Le serveur MCP écrit journal/entries/2026-05-19-titre.md et insère le bloc en tête de index.json.
  7. La skill suggère un message de commit. Je tape /verifier d'abord.
  8. La skill verifier-avant-push tourne, sort « Prêt à pousser ».
  9. Je tape git commit -m "ajoute entrée journal du 19 mai". Le hook validate-journal.sh intercepte, vérifie, laisse passer.
  10. Je tape git push. Claude Code me demande confirmation (à cause de la règle ask). Je valide. Trente secondes plus tard, Vercel a redéployé.

Temps total, pour une entrée déjà claire dans la tête : 8 à 10 minutes, dont 5 d'écriture pure. Pour une entrée que je dois muscler, plutôt 20 minutes.

11

Limites assumées

Ce stack ne fait pas tout. Les manques que j'assume :

  • Pas de CI automatisée. Le hook tourne localement uniquement. Si quelqu'un poussait depuis une autre machine sans le hook, l'index pourrait casser.
  • Pas de tests HTML automatisés. Tidy ou html-validator ne sont pas branchés. auditer_page_html traite l'a11y et le SEO mais pas la conformité W3C stricte.
  • Pas de MCP tiers branchés. J'aurais pu connecter le MCP Vercel pour lire les déploiements, ou un MCP browser pour des audits Lighthouse automatiques. Ce n'est pas encore fait. À ajouter une fois le MCP maison rodé.
  • Le sous-agent auditeur-qualite a besoin du serveur local lancé. Pas de fallback si le port 8000 est occupé.

Ces manques ne m'empêchent pas de bosser. Quand ils deviendront gênants, je les comblerai. C'est l'inverse du stack « tout-CI tout-test tout-MCP » qu'on monte le premier jour et qu'on n'utilise jamais.

Pour aller plus loin