Cos'è e perché conta
Sono i due hook che uso nei miei progetti. Un hook è un comando che Claude Code lancia da solo in un momento preciso della sessione. La differenza con un'istruzione scritta in chat o nel CLAUDE.md è tutta qui: un'istruzione Claude la può perdere per strada, dopo due ore di lavoro. Un hook no. Lo esegue Claude Code ogni volta, che Claude se lo ricordi o no.
Il guardiano serve perché alcuni comandi non si annullano: cancellano file senza passare dal cestino, riscrivono la storia di git, svuotano una tabella del database. Basta un percorso sbagliato o una richiesta capita male.
Il mio guardiano fa due cose. Se un comando cancella file, ti chiede conferma, anche in modalità auto, quella in cui Claude lavora senza chiederti niente. Se è irreparabile, tipo cancellare tutto il disco o forzare un push, lo blocca e basta.
I test dopo ogni modifica servono per l'altro problema classico: Claude cambia un file, ti dice «fatto», e intanto ha rotto qualcosa da un'altra parte. Con l'hook il risultato dei test gli arriva subito, mentre ha ancora in mente cosa ha toccato.
1. Il guardiano: prima di ogni comando
Il primo hook usa l'evento PreToolUse, che scatta prima che Claude usi uno strumento. Con il filtro «Bash» guarda solo i comandi del terminale. Crea il file .claude/hooks/guardiano.sh nella cartella del progetto e incollaci questo:
#!/bin/bash
# guardiano.sh: chiede prima di cancellare, blocca le cose irreparabili
command -v jq >/dev/null || { echo "Bloccato: manca jq (brew install jq)" >&2; exit 2; }
CMD=$(jq -er '.tool_input.command') || { echo "Bloccato: input non leggibile" >&2; exit 2; }
NO='rm[[:space:]]+-[a-z]*r[a-z]*[[:space:]]+(/|~/?|\$HOME/?)\*?([[:space:]]|$)|git[[:space:]]+push([[:space:]].*)?[[:space:]](-f|--force)|git[[:space:]]+reset[[:space:]]+--hard|drop[[:space:]]+(table|database)'
if grep -Eiq "$NO" <<<"$CMD"; then echo "Bloccato: comando irreparabile ($CMD)" >&2; exit 2; fi
ASK='^[[:space:]]*((sudo|xargs)[[:space:]]+)*(rm|rmdir|unlink)([[:space:]]|$)|^[[:space:]]*find[[:space:]].*[[:space:]]-delete'
if tr ';&|()`' '\n\n\n\n\n\n' <<<"$CMD" | grep -Eq "$ASK"; then
jq -n --arg r "Il guardiano: questo comando cancella file. Confermi?" \
'{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: $r}}'
fi
exit 0Come funziona: prima di ogni comando Claude Code passa allo script il comando in JSON, nel campo tool_input.command. Lo script può rispondere in tre modi:
- Esce con codice 2: il comando non parte e Claude legge il motivo. Succede con i comandi della lista NO.
- Scrive un piccolo JSON con permissionDecision «ask» ed esce con 0: Claude Code ti mostra la richiesta di permesso, con il motivo scritto dallo script, e il comando parte solo se dici sì. Succede con i comandi della lista ASK.
- Esce con 0 senza scrivere niente: il comando va avanti con i permessi normali.
Le due righe dopo il commento bloccano tutto se manca jq, il programma che legge il JSON, o se l'input non si legge. Sul Mac jq lo installi con brew install jq.
Cosa chiede, cosa blocca, cosa lascia passare
- ASK, chiede conferma: rm, rmdir, unlink e find … -delete, anche con sudo o xargs davanti. Prima di cercare, lo script spezza il comando su ; & | ( ) e backtick, così becca anche il rm in fondo a cd cartella && rm -r build.
- NO, blocca e basta: rm -r su /, ~ o $HOME, cioè tutto il disco o tutta la cartella utente; git push con -f o --force; git reset --hard; DROP TABLE e DROP DATABASE, maiuscole o minuscole.
- Lascia passare: git rm --cached, docker rm e tutto il resto, perché lì il comando è git o docker, non rm.
Le due liste allungale tu. Per esempio git clean, che cancella i file nuovi che git non conosce ancora: nella mia versione non c'è. Se lo usi, mettilo in ASK o in NO, insieme ai comandi che nel tuo progetto fanno danni.
Cosa non vede
Il guardiano guarda solo i comandi Bash. Se Claude svuota un file riscrivendolo con lo strumento Write, non lo ferma. Per i file che non devono cambiare mai, tipo .env, serve una regola deny nei permessi, per esempio Edit(./.env), che vale per tutti gli strumenti che modificano file. Oppure un secondo hook su Write.
2. I test: dopo ogni modifica
Il secondo hook usa PostToolUse, che scatta dopo che uno strumento ha finito il suo lavoro. Con il filtro «Edit|Write» parte ogni volta che Claude modifica o crea un file. Crea .claude/hooks/test-dopo-modifica.sh:
#!/bin/bash cd "$CLAUDE_PROJECT_DIR" || exit 0 OUT=$(npm test 2>&1) && exit 0 echo "Test falliti dopo la modifica:" >&2 echo "$OUT" | tail -n 30 >&2 exit 2
Metti il tuo comando dei test al posto di npm test. Se i test passano non vedi niente. Se falliscono, lo script manda a Claude le ultime 30 righe dell'errore ed esce con codice 2.
Qui il codice 2 non annulla la modifica: è già fatta. Fa leggere a Claude l'errore, così lo sistema subito. È la differenza tra i due hook: il guardiano può dire di no prima, questo può solo avvisare dopo. E Claude aspetta la fine dei test a ogni modifica: se i tuoi sono lenti, nello script metti solo quelli veloci.
3. Attivali
Rendi eseguibili tutti e due gli script. Dal terminale, nella cartella del progetto:
chmod +x .claude/hooks/*.sh
Poi in .claude/settings.json, sempre nella cartella del progetto, registri i due hook:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guardiano.sh" }] }
],
"PostToolUse": [
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/test-dopo-modifica.sh" }] }
]
}
}Se il file c'è già, non sostituirlo: aggiungi la parte «hooks» accanto a quello che contiene, e controlla le virgole. $CLAUDE_PROJECT_DIR è la cartella da cui hai aperto la sessione, così Claude Code trova gli script anche quando Claude lavora in una sottocartella.
Se non ti va di fare tutto a mano, chiedilo a Claude:
Crea in questo progetto i due hook di questa guida: un PreToolUse su Bash che mi chiede conferma prima di cancellare file e blocca con codice 2 i comandi irreparabili, e un PostToolUse su Edit e Write che lancia [il comando dei test] dopo ogni modifica. Registrali in .claude/settings.json senza togliere niente di quello che c'è già. Poi fammi vedere i due script e spiegami cosa fa ogni riga.
4. La prova
Prima provo il guardiano da solo, senza Claude. Dal terminale, nella cartella del progetto, gli passo un comando finto come farebbe Claude Code:
echo '{"tool_input":{"command":"rm -rf prova"}}' | .claude/hooks/guardiano.sh; echo $?Deve comparire un piccolo JSON con «ask», e poi 0. Con git reset --hard al posto di rm -rf prova deve comparire «Bloccato» e poi 2. Con ls, solo 0. Lo script legge solo il testo: qui non viene eseguito niente. Questa prova conta perché, se il percorso nel settings.json è sbagliato o manca chmod +x, Claude Code mostra solo un avviso «hook error» e il comando passa senza domande.
Poi la prova vera. Apri una nuova sessione: di solito Claude Code si accorge da solo delle modifiche al settings.json, ma così sei sicuro. Scrivi /hooks e controlla che ci siano tutti e due, uno sotto PreToolUse e uno sotto PostToolUse. Poi chiedi a Claude:
Crea una cartella vuota chiamata prova-guardiano e poi cancellala.
Deve comparire la richiesta di permesso, con il motivo «Il guardiano: questo comando cancella file» e l'etichetta [settings], che ti dice da dove arriva la domanda. La prima volta rispondi no e controlla che la cartella sia ancora lì.
Per l'hook dei test:
Cambia una riga in un file del progetto in modo che un test fallisca, poi dimmi cosa ti è arrivato dopo la modifica. Alla fine rimetti tutto com'era e controlla che i test passino.
Gli errori più comuni
- Dire sì per abitudine: la domanda del guardiano è lì apposta. Leggi il comando prima di rispondere.
- Fidarsi del guardiano senza averlo visto lavorare: con un percorso sbagliato il comando passa con un semplice avviso. Fai sempre la prova a mano con echo.
- Partire con i test già rotti: ogni modifica riceve un errore che non c'entra niente. Prima di attivare l'hook fai passare i test, o lancia solo quelli che passano.
Checklist
- guardiano.sh in .claude/hooks, con le tue liste ASK e NO
- test-dopo-modifica.sh con il tuo comando dei test
- jq installato
- chmod +x su tutti e due gli script
- Hook registrati in .claude/settings.json
- Prova a mano con echo: ask per rm, 2 per git reset --hard
- Prova in Claude Code: richiesta di permesso per la cartella, test fallito riportato
Domande frequenti
E se di notte nessuno risponde?
Il comando resta fermo sulla richiesta finché non torni, e non cancella niente. Vale anche in modalità auto. Se invece lanci Claude Code senza nessuno davanti, per esempio con claude -p, la richiesta non può comparire: Claude Code nega il comando e Claude legge il motivo.
Il guardiano mi protegge da tutto?
No. Guarda il testo dei comandi Bash, e lo stesso danno si può fare in altri modi, per esempio con un piccolo script in Python o riscrivendo un file con Write. Riduce il rischio. La rete vera resta git: un commit prima dei lavori lunghi e una copia del progetto online.