AI and Automation

Hook di Claude Code: PreToolUse, PostToolUse e i pattern che scalano

Come configurare gli hook di Claude Code per gate di sicurezza, format-on-save e guardrail di team senza rallentare l'agente.

3 maggio 20269 min di lettura
Hook di Claude Code: PreToolUse, PostToolUse e i pattern che scalano

Un hook di Claude Code è un comando shell che si attiva in un punto fisso del ciclo di vita dell'agente, riceve un evento JSON su stdin e decide tramite exit code se il passo successivo procede. Gli hook sono l'unico meccanismo che Claude Code espone per un controllo deterministico, a livello di codice, sull'agente: i prompt possono essere ignorati, le system instruction possono essere sovrascritte dal contesto, ma un hook PreToolUse che esce 2 cancella sempre la chiamata al tool.

Nella release attuale sono disponibili dodici eventi del ciclo di vita, tra cui PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStop, SessionStart, SessionEnd e Notification. Vivono in settings.json, girano come processi normali e viaggiano con il resto del repo. È così che un team trasforma Claude Code da strumento individuale ingegnoso a infrastruttura su cui altri ingegneri possono contare.

Cosa avrai alla fine

Tre hook production-grade committati in .claude/settings.json nel tuo repo:

  • Una guardia PreToolUse che blocca comandi shell pericolosi prima che girino.
  • Un formatter PostToolUse che lancia Prettier o Ruff dopo ogni edit di file.
  • Un hook Stop che scrive nel log di sessione quando Claude termina un turno.

Più il modello mentale per aggiungere i nove successivi senza rompere l'agente.

Prerequisiti

  • Claude Code v2.0.10 o successivo (le versioni precedenti non possono modificare l'input del tool da un hook PreToolUse).
  • Un repo con .claude/ nella root.
  • Bash o Python nel PATH. Usiamo Bash per gli hook brevi e Python quando il parsing del JSON diventa non banale.
  • Un formatter installato localmente (Prettier, Ruff, gofmt, qualsiasi cosa esca 0 in caso di successo).

Step 1 · Capire i tre scope degli hook

Le settings vengono caricate in questo ordine, con quelle successive che sovrascrivono le precedenti per lo stesso matcher:

  1. ~/.claude/settings.json: user-wide. Vive nella tua home, si applica a ogni progetto. Buono per preferenze personali (suoni di notifica, percorsi di log).
  2. .claude/settings.json nella root del repo: project-wide. Committato in git. È qui che vivono i guardrail di team.
  3. .claude/settings.local.json: override per developer, gitignorato. Da usare con parsimonia, principalmente per debug locale.

Il riferimento ufficiale su code.claude.com/docs/en/hooks è la fonte di verità per lo schema. Lo consultiamo ogni volta che un hook si comporta male; la versione docs si aggiorna più velocemente delle guide di terze parti.

Step 2 · Configurare una guardia PreToolUse per Bash

L'hook a leva più alta in qualsiasi setup di team. PreToolUse si attiva prima che un tool giri, vede il nome del tool e gli argomenti JSON su stdin, e può uscire 2 per bloccare. Anthropic spedisce un'implementazione di riferimento in examples/hooks/bash_command_validator_example.py nel repo pubblico di Claude Code. L'abbiamo copiata, snellita e messa in .claude/hooks/bash-guard.sh:

#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

if echo "$CMD" | grep -qE '\brm\s+-rf\s+/'; then
  echo "Blocked: rm -rf / refused by PreToolUse guard." >&2
  exit 2
fi

if echo "$CMD" | grep -qE '\b(curl|wget)\b.*\|.*\bsh\b'; then
  echo "Blocked: piping remote content to a shell is refused." >&2
  exit 2
fi

if echo "$CMD" | grep -qE '\.env(\s|$|:)'; then
  echo "Blocked: .env access. Add the var to settings.local.json instead." >&2
  exit 2
fi

exit 0

Il blocco corrispondente in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/bash-guard.sh" }
        ]
      }
    ]
  }
}

Tre cose da interiorizzare sugli exit code:

  • Exit 0: il tool procede. Quello che hai scritto su stdout viene mostrato nel transcript e può essere parsato come JSON per controllo avanzato.
  • Exit 2: il tool viene cancellato. Il testo su stderr torna a Claude come errore di tool-result, così il modello può autocorreggere al turno successivo.
  • Exit 1 (o qualsiasi altro non-zero): errore non bloccante dell'hook. Il tool procede comunque. Riservato ai fallimenti interni dell'hook che vuoi loggare ma non far valere.

Questa distinzione è la causa più comune di hook rotti che vediamo: un team intende bloccare, esce 1, e il comando pericoloso gira lo stesso. Ogni hook di sicurezza deve usare exit 2.

Step 3 · Configurare un formatter PostToolUse

PostToolUse si attiva dopo che un tool è andato a buon fine. Non può annullare l'azione, ma può lanciare un comando di follow-up e rimandarne l'output a Claude. L'uso canonico è auto-format dopo ogni Write o Edit, così l'agente non committa mai codice non formattato:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/format-on-save.sh"
          }
        ]
      }
    ]
  }
}

Lo script estrae il path del file dall'evento, fa branching sull'estensione e lancia il formatter giusto:

#!/usr/bin/env bash
set -euo pipefail
FILE=$(jq -r '.tool_input.file_path // .tool_input.path // ""')
[ -z "$FILE" ] && exit 0

case "$FILE" in
  *.ts|*.tsx|*.js|*.jsx|*.json|*.md) npx --no -y prettier --write "$FILE" ;;
  *.py) ruff format "$FILE" ;;
  *.go)  gofmt -w "$FILE" ;;
esac

exit 0

Due regole testate sul campo: tieni il formatter idempotente e silenzioso in caso di successo, e non lasciarlo mai uscire 2. Un exit non-zero su PostToolUse manda a Claude un errore rumoroso che spesso innesca un retry ridondante.

Step 4 · Configurare un hook Stop per la telemetria di sessione

Stop si attiva quando l'agente principale finisce una risposta (non sull'interrupt utente). Lo usiamo per stampare una riga in un log giornaliero di sessione, così il team può fare audit di quali feature Claude ha toccato senza setacciare il transcript:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": ".claude/hooks/log-stop.sh" }
        ]
      }
    ]
  }
}

L'evento Stop accetta anche una risposta JSON {"decision": "block", "reason": "..."} su stdout (con exit 0) per rifiutare lo stop e forzare Claude a continuare. L'abbiamo usato due volte in produzione: una per imporre un giro di test prima del fine turno, una per richiedere una bozza di commit message. Utile, ma facile da abusare, perché un block su Stop brucia contesto. Di default, logga; passa al blocking solo quando il costo di fermarsi prima è reale.

Step 5 · Verificare che funzioni

Gli hook falliscono in silenzio più spesso di quanto falliscano in modo evidente. Tre check prima di fidarti in produzione:

  1. Lancia claude --debug nel progetto. Ogni invocazione di hook stampa su stderr con timing ed exit code. Se il tuo hook manca, il matcher è sbagliato.
  2. Triggera il matcher a mano. Chiedi a Claude di girare rm -rf /tmp/test-dir e conferma che blocchi. Chiedigli di scrivere un file TS e conferma che il formatter sia partito (controlla git diff).
  3. Misura i tempi degli hook. Qualsiasi cosa sopra i 200ms su PreToolUse si accumula nella sessione. Profiliamo con time e mettiamo qualsiasi cosa più pesante su PostToolUse, dove la latenza è nascosta.

Failure comuni e fix

L'hook gira ma non blocca

Sei uscito 1 invece che 2. Oppure hai scritto JSON con "decision": "block" ma sei uscito 2; in quel caso il JSON viene ignorato e viene letto solo stderr. Scegli una strada: exit 2 con messaggio su stderr, o exit 0 con un blocco decision in JSON su stdout.

L'hook non gira mai

Mismatch del matcher. I tool built-in sono Bash, Edit, Write, MultiEdit, Read, Grep, Glob, WebSearch, WebFetch, Task. I tool MCP seguono mcp__<server>__<tool>. Per matchare tutti i tool MCP di scrittura su tutti i server, usa mcp__.*__write.*.

L'hook si blocca

Hai dimenticato di leggere stdin. Anche se non ti serve il JSON, consumalo: cat > /dev/null in cima a un hook Bash basta. Altrimenti l'agente resta in attesa sulla pipe.

Il formatter gira su file fuori dal repo

Controlla sempre il path prima di formattare. Claude può editare file ovunque abbia accesso al filesystem, incluso ~/Library su macOS, e Prettier su un file di sistema è una brutta giornata. Proteggi con un check di prefisso: [[ "$FILE" == "$PWD"* ]] || exit 0.

L'output dell'hook compare come messaggio di Claude

Per la maggior parte degli eventi, stdout è solo per debug. Per UserPromptSubmit, UserPromptExpansion e SessionStart, stdout viene aggiunto come contesto che Claude vede. Se vuoi che un hook inietti contesto (tipo il git log recente all'inizio della sessione), scegli quegli eventi. Se vuoi enforcement silenzioso, ogni altro evento è silenzioso su stdout per design.

I pattern che scalano a un team

I tre hook qui sopra sono punti di partenza. Il pattern che distingue un setup personale da uno di team è dove vivono gli hook e come evolvono.

Committa gli hook nel repo, non nella home

Gli hook personali in ~/.claude/settings.json funzionano per una persona. Gli hook di team appartengono a .claude/settings.json nella root del repo, committati e revisionati in PR come qualsiasi altro codice. I nuovi assunti ereditano i guardrail dal primo giorno.

Tieni gli hook brevi e delega la logica a script

Il campo command dovrebbe chiamare uno script, non contenere logica. I comandi inline sono illeggibili in JSON, difficili da testare e impossibili da grepare. Ogni hook nel nostro setup è una chiamata di una riga a uno script in .claude/hooks/.

Usa gli hook di tipo prompt con parsimonia

Claude Code ora supporta un handler "type": "prompt" che manda l'evento a un modello Claude per la valutazione invece che a uno script. Funziona, ma aggiunge latenza e costo a ogni chiamata di tool. Riservalo ai casi in cui il matching basato su regole non cattura l'intento (per esempio, classificare se un comando Bash è distruttivo in base al contesto). Per tutto il resto, gli exit code da uno script shell di 20 righe sono più veloci, più economici e auditabili.

Versiona gli hook come dipendenze

Quando le release notes di Claude Code cambiano la semantica degli hook (ed è successo, tre volte dal lancio della v1.0), i tuoi hook hanno bisogno di un giro di regression. Teniamo un .claude/hooks/test.sh che spara eventi fixture a ogni script e asserisce gli exit code. Cinque minuti per scriverlo, salva ore quando arriva una v2.x con breaking change.

Fai audit di cosa vedono gli hook

Gli hook ricevono l'input completo del tool, inclusi i segreti che possono passare per i comandi Bash. Tratta gli script di hook con lo stesso rigore di qualsiasi altro codice che tocca credenziali. L'output del modello può essere sandboxato; il tuo hook no.

Fatti bene, gli hook sono il livello in cui Claude Code smette di essere uno strumento di produttività e inizia a essere parte del build system. I primi tre qui sopra si ripagano in una settimana. I nove successivi sono come trasformi un agente in infrastruttura.

Foto di Hans Westbeek su Unsplash

Domande frequenti

How do Claude Code hooks differ from MCP servers?
Hooks fire deterministically on lifecycle events (before a tool runs, after a tool runs, when a session starts) and decide via exit code whether the next step proceeds. MCP servers expose new tools and resources to the agent. They solve different problems: MCP extends what Claude can do, hooks constrain how Claude does it. Most production setups use both.
What is the latency cost of a PreToolUse hook?
The hook runs synchronously before the tool, so its wall time is added to every matched call. A 50ms shell script is invisible; a 500ms script is felt across a multi-tool session. We profile every PreToolUse hook with the time command and aim for under 200ms. Anything heavier moves to PostToolUse, where the latency is hidden behind the tool result.
Can a hook see the model's reasoning or only the tool call?
Only the tool call. Hooks receive the tool name and the JSON arguments Claude is about to pass. They do not see the model's chain of thought, the system prompt, or earlier turns. If you need semantic understanding of intent, that is what the prompt-type hook handler is for, but it adds API cost on every invocation.

Studio

Inizia un progetto.

Un partner unico per aziende, PA, startup e SaaS. Produzione più veloce, tecnologie moderne, costi ridotti. Un team, una fattura.