In breve e verdetto
I quattro lavori che continui a scrivere in Claude Code appartengono ognuno a un file diverso. Una checklist di release è uno skill. Una regola che non deve mai rompersi è un hook PreToolUse. Un audit che legge molti file è un subagent. I dati che Claude non vede sono un server MCP.
Li abbiamo eseguiti tutti e quattro su un repo vero, un'API di fatturazione con 34 test, sbagliando ogni volta la prima scelta. Le scelte sbagliate sono fallite sullo schermo:
- La checklist in CLAUDE.md ha aggiunto a /context una riga
Memory files: 210 tokens, a ogni turno. - Il primo hook è partito, ha stampato il suo avviso e la scrittura è passata (unitPrice da 8000 a 12000).
- L'audit eseguito inline ha riempito il thread: Messages a 19.3k token, contro 7.2k con un subagent nominato.
- Senza un server di docs, Claude ha risposto "I couldn't find that setting... I won't guess one."
Verdetto: scegli il tipo dalla tabella della documentazione, poi fai in modo che il file risponda, non che esista soltanto. Un hook che registra soltanto è solo consultivo, come la riga di CLAUDE.md che ha sostituito. Dei quattro, solo l'hook non chiede mai.
Copia i file qui sotto, fai il commit e parti da quello che riscrivi più spesso.
Sintesi con fonti
Cosa carica la tua sessione prima della tua prima parola. Il nostro /context di partenza leggeva 26.2k/1m token, senza riga di memoria e senza riga MCP. Ogni lavoro che aggiungi mette una riga in quella tabella oppure no. Leggila prima, poi misura rispetto a lei.
Una regola in CLAUDE.md è una richiesta, un hook è un'imposizione. La documentazione dice che un'istruzione come "never edit .env" in CLAUDE.md o in uno skill è una richiesta, e un hook PreToolUse che blocca la modifica è un'imposizione s1. La pagina Memory definisce CLAUDE.md contesto, non imposizione s8. Un thread su Hacker News ha riportato la stessa cosa dal lato degli hook: un hook che esce con 0 non blocca nulla s10.
La tabella dei trigger ti dice quale file. Terza volta che incolli una procedura: uno skill. Output che inonda la chat: un subagent. Una scheda che Claude non vede: un server MCP. Qualcosa che deve succedere ogni volta: un hook s1.
Lavoro 1, la checklist di release, è uno skill.
- Una procedura cresciuta dentro CLAUDE.md appartiene a uno skill, e il suo corpo viene caricato solo quando serve s3.
- Nella nostra run la checklist incollata in CLAUDE.md ha aggiunto una riga
Memory files: 210 tokens. Come skill condisable-model-invocation: true, /context è tornato alla base di partenza. /releaseha eseguito sei passaggi (34 test, da 0.4.0 a 0.4.1, tag v0.4.1). Ha anche notato chegit commit -amsalta i file non tracciati, quindi ha messo prima in staging il nuovo changelog.
Lavoro 2, la regola da rispettare, è un hook, e la garanzia è la risposta che restituisce.
- Exit 0 senza output non è una decisione. Exit 2 blocca. Blocca anche il JSON deny s2.
- Il nostro primo hook ha registrato, è uscito con 0 e la scrittura è passata (unitPrice da 8000 a 12000). La risposta di Claude non ha mai menzionato l'hook.
- Lo stesso hook, restituendo il JSON deny, ha rifiutato la modifica con
PreToolUse:Edit hook error: Stored data is never edited by hand: write a migration in src/store/migrations.ts (README § Conventions).Claude poi ha chiesto: scavalcare, o scrivere la migrazione. - La documentazione pubblica una ricetta PreToolUse per i file protetti s23.
- Il limite: questo hook controlla Edit e Write. Copri anche la shell, oppure tieni la shell fuori dalla tua allow list.
onFailure: "block"fa bloccare un hook fallito invece di lasciarlo passare. È arrivato nella v2.1.295 s2 s15.
Lavoro 3, l'audit che inonda, è un subagent.
- Un subagent parte alla cieca e restituisce un riassunto s4.
- Inline, Messages leggeva 19.3k. Con il subagent nominato leggeva 7.2k.
- Il solo file agent ha delegato 0 run su 2. Nominarlo nel prompt ha delegato. Il file lo fa esistere, il tuo prompt fa sì che Claude lo usi.
- Fissa
model:nel file. Il conto non si è mosso: la lettura avviene comunque, solo non nella tua finestra.
Lavoro 4, i dati irraggiungibili, sono un server MCP.
claude mcp add --scope projectscrive.mcp.json, e la tool search carica solo i nomi degli strumenti s5.- Esiste un server HTTP gratuito, senza login, per la documentazione di Claude Code stesso, con
search_claude_code_docsequery_docs_filesystem_claude_code_docss24. - Senza, Claude ha risposto "I couldn't find that setting... I won't guess one." Con, Claude ha risposto
"onFailure": "block", aggiunto nella v2.1.295, mentre la nostra installazione era la 2.1.294. - Un server di docs per una libreria che il modello già conosce non ha cambiato nulla per noi. Collega ciò che il modello non può avere.
Segnalazioni dal campo. Un thread su Reddit ha riportato Claude che ignorava una riga "MUST use agent" in CLAUDE.md s9. La citiamo come segnalazione, non come misura. I post di ingegneria di Anthropic trattano la stessa distinzione s6 s7.
Codice
Quattro file della run, più due helper. I percorsi indicano dove vive ciascuno nel tuo repo.
1. Lo skill di release, in .claude/skills/release/SKILL.md. Scrivi /release. disable-model-invocation: true lo tiene da avviare solo tu, ed è quello che vuoi per qualcosa che fa commit e tag.
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.
2. L'hook che protegge i dati, in .claude/settings.json. Restituisce la decisione deny con un motivo che Claude legge. Richiede jq nel tuo PATH e sta inline, quindi l'hook non ha bisogno di un file script proprio. Cambia il pattern data/.*\.json$ e il motivo secondo la tua regola.
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'"
}
]
}
]
}
}
3. L'auditor delle convenzioni, in .claude/agents/conventions-auditor.md. Gli strumenti sono limitati a quelli in sola lettura e il modello è fissato. Modifica l'elenco delle convenzioni in base al tuo 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 configurazione MCP, salvata come .mcp.json alla radice del repo. È ciò che scrive claude mcp add --transport http claude-docs --scope project https://code.claude.com/docs/mcp. Claude Code elenca un server di progetto come in attesa finché non lo approvi nella sessione successiva.
mcp.json
{
"mcpServers": {
"claude-docs": {
"type": "http",
"url": "https://code.claude.com/docs/mcp"
}
}
}
Helper. ctx.sh esegue /context in una sessione headless e stampa le righe dei token, così puoi confrontare prima e dopo. mcp-probe.sh invia initialize e tools/list a un url MCP HTTP, per verificare che un server risponda prima di aggiungerlo.
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"
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"
Da fare lunedì
- Apri una sessione nuova ed esegui /context. Annota il totale. Quella è la tua base di partenza.
- Scegli la checklist che incolli più spesso. Spostala in
.claude/skills/<name>/SKILL.md, aggiungidisable-model-invocation: truese fa commit o tag, ed eseguila con/<name>. - Prendi la regola che non deve mai rompersi e mettila in un hook PreToolUse che restituisce
permissionDecision: "deny"oppure esce con 2. Chiedi a Claude di violare la regola e verifica che la modifica venga rifiutata. - Se il tuo hook registra soltanto, cambialo. Un hook che esce con 0 non blocca nulla.
- Sposta un compito che legge molti file in
.claude/agents/<name>.mdcon unmodel:fissato. Nominalo nel prompt finché Claude non lo sceglie da solo. - Controlla un fatto che Claude ha indovinato per te di recente. Se è più nuovo del modello, aggiungi un server di docs con
claude mcp add --transport http <name> --scope project <url>e fai il commit di.mcp.json. - Controlla la versione di Claude Code prima di fare affidamento su un'impostazione.
onFailure: "block"richiede la 2.1.295. - Esegui
git add -A && git statuse fai il commit dei nuovi file. La nostra run ne mostrava esattamente quattro.
Per approfondire
La scheda di scelta. Una pagina. Sono le righe della documentazione s1 con i nostri quattro lavori scritti sopra. L'eroe di questo pacchetto sono i quattro file qui sopra più questa scheda.
| Tipo | Cosa fa | Usalo quando | Il nostro lavoro | Il nostro file |
|---|---|---|---|---|
| CLAUDE.md | Contesto persistente caricato a ogni conversazione | Una convenzione che Claude sbaglia due volte | Una riga, non una procedura | nessuno |
| Skill | Istruzioni e flussi di lavoro che Claude può usare | Incolli lo stesso playbook per la terza volta | La checklist di release | .claude/skills/release/SKILL.md |
| Hook | Uno script attivato da eventi | Vuoi che succeda ogni volta, senza chiedere | Non modificare mai a mano i dati salvati | .claude/settings.json |
| Subagent | Contesto isolato che restituisce risultati riassunti | Un lavoro secondario inonda la conversazione | L'audit delle convenzioni | .claude/agents/conventions-auditor.md |
| MCP | Collega a servizi esterni | Continui a copiare dati da una scheda che Claude non vede | La documentazione di Claude Code | .mcp.json |
| Plugin | Raggruppa i quattro in un'unica unità installabile | Un secondo repository ha bisogno dello stesso setup | Non usato | nessuno |
La scheda decide il tipo, non la qualità. Un hook che registra soltanto, un subagent che non nomini mai e un server per docs che il modello conosce stavano ognuno nel tipo giusto, e hanno fallito. Scegli il tipo, poi fai in modo che il file risponda.
Quando non usarli.
- Una convenzione che Claude continua a sbagliare resta in CLAUDE.md come una riga, non come una procedura.
- Un lavoro che guidi avanti e indietro, con la tua cronologia sotto gli occhi, resta nel thread principale, non in un subagent.
- Un server di docs per una libreria che il modello già conosce costa contesto e non cambia nulla.
pnpm testè una CLI che hai già. Non è un lavoro da MCP.
Plugin. Un plugin è la scatola che spedisce i quattro insieme. Usalo quando un altro repo ha bisogno dello stesso setup.
Skill più MCP. La documentazione li abbina: il server fornisce la connessione, e lo skill insegna a Claude a usarla bene.
Skill con context: fork. Uno skill può girare in un subagent con fork, così la sua lettura resta fuori dalla tua finestra. Provalo su un lavoro prima di fare affidamento su di lui.
Tipi di hook oltre a command. Command è quello che abbiamo usato. Il riferimento degli hook elenca altri tipi di handler, inclusi gli hook http e prompt. Vale la stessa regola: decide ciò che l'hook restituisce, e un hook silenzioso non decide nulla.
MCP contro CLI. C'è un dibattito aperto sui server MCP che si mangiano il contesto, con un post che sostiene la CLI come prima scelta s11 e un altro sul costo in contesto dei server MCP s12. La nostra regola: se una CLI fa già il lavoro, usala. Usa MCP per ciò che non ne ha una. La tool search ora carica solo i nomi, quindi misura con /context sul tuo setup prima di decidere s5.
Tieni gli skill brevi e verificati. Uno skill breve per lavoro, controllato su una run vera. Vedi il paper SkillsBench s13 e il punto di vista di un praticante su perché gli skill costano poco finché non vengono usati s14.
FAQ
Perché il mio hook è partito ma la modifica è passata comunque? Un hook decide solo attraverso ciò che il suo comando restituisce. Exit 0 senza output non è una decisione s2. Restituisci il JSON deny, oppure esci con 2.
Perché Claude legge i file da solo anche se ho un subagent? Nella nostra run il solo file agent ha delegato 0 run su 2. Nominalo nel prompt, per esempio "use the conventions auditor subagent".
L'hook copre tutto quello che Claude può fare? No. Il nostro intercetta Edit e Write. In una run successiva Claude si è offerto di fare il cambiamento tramite la shell, quindi copri la shell oppure tienila fuori dalla tua allow list.
Devo aggiungere un server MCP per ogni libreria che uso? No. Se il modello conosce già la libreria, un server non cambia nulla e costa contesto. Aggiungine uno per dati più nuovi del modello o fuori dal tuo disco, e confronta la versione installata con quella che ha aggiunto la funzione.
Posso fare il commit di tutto questo?
Sì. Lo skill, il file delle impostazioni, il file agent e .mcp.json vivono tutti nel repo, quindi chiunque lo cloni li riceve. Approva il server MCP di progetto una volta nella sessione successiva.
AIDive