Un agente non diventa affidabile in produzione scrivendo un system prompt migliore: diventa affidabile mettendo del codice fra la decisione del modello e l’effetto sul mondo. Nell’Agent SDK quel codice sono gli hook, definiti come «callback functions that run your code in response to agent events», con cinque impieghi dichiarati: bloccare operazioni pericolose, registrare e verificare ogni chiamata, trasformare input e output, richiedere approvazione umana, seguire il ciclo di vita della sessione. Le prime tre sono il mestiere dell’architetto.

Il punto di aggancio decide che cosa puoi fare

Il ciclo è sempre lo stesso: scatta un evento, l’SDK raccoglie gli hook registrati per quell’evento, i matcher filtrano quali eseguire, la callback riceve i dettagli della chiamata e restituisce un oggetto di decisione. Il punto di aggancio non è un dettaglio di comodità: PreToolUse scatta prima dell’esecuzione e può bloccare o modificare, PostToolUse scatta sul risultato e non impedisce più nulla. Se il requisito è «questa scrittura non deve avvenire», l’unico posto possibile è PreToolUse.

Il matcher si valuta sul nome del tool, mai sugli argomenti. Un matcher assente esegue la callback su ogni occorrenza dell’evento, ed è la scelta giusta solo quando vuoi davvero registrare tutto.

Write|Edit                -> solo i tool di scrittura file
^mcp__                    -> tutti i tool MCP
mcp__<server>__<action>   -> forma del nome di un tool MCP

Per filtrare su un percorso devi guardare dentro tool_input nella callback: i matcher «only match tool names, not file paths or other arguments». Il tool use ID passato come secondo argomento correla PreToolUse e PostToolUse della stessa chiamata, ed è quello che ti permette di chiudere una traccia di audit invece di produrre due flussi scollegati.

Riscrivere l’input prima che il tool parta

L’intercettazione vera vive in hookSpecificOutput. Un oggetto di ritorno vuoto lascia passare l’operazione; per riscriverla si restituisce updatedInput insieme a una decisione di permesso.

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": { "file_path": "/sandbox/etc/config.yml" }
  }
}

Le regole di combinazione vanno sapute a memoria, perché sbagliarle non produce un errore: produce un comportamento diverso da quello che credi. updatedInput va accoppiato ad allow per auto-approvare l’input modificato, oppure ad ask per mostrarlo all’utente; se ometti del tutto la decisione «the modified input still applies and flows through the normal permission evaluation»; con defer l’input modificato viene ignorato. E la regola che chiude: «Always return a new object rather than mutating the original», riferita a tool_input.

Quando più hook e più regole di permesso si pronunciano sulla stessa chiamata, l’ordine di precedenza è documentato: deny batte defer, defer batte ask, ask batte allow, e un solo deny blocca l’operazione a prescindere dagli altri. È questo che rende gli hook componibili senza sorprese: puoi impilare un hook di policy sopra a uno di normalizzazione e sapere già chi vince.

Normalizzare l’output prima che Claude lo legga

Su PostToolUse hai due leve che vengono confuse spesso. additionalContext aggiunge informazione al risultato del tool. updatedToolOutput sostituisce l’output del tool prima che Claude lo veda, e funziona «for any tool in both SDKs»; il vecchio updatedMCPToolOutput, limitato ai soli tool MCP, è deprecato.

È qui che si fa la normalizzazione, ed è quasi sempre necessaria appena si esce dai tool interni: un server MCP di terze parti che restituisce timestamp in tre formati, un gestionale che espone codici interni, un endpoint che ogni tanto annega un errore dentro una risposta 200. Normalizzare nell’hook invece che nel prompt ha due vantaggi misurabili: la forma canonica è testabile senza chiamare il modello, e il modello smette di spendere token e ragionamento per riconciliare formati. Attenzione a systemMessage, che mostra un messaggio all’utente e non al modello: per passare contesto al modello serve additionalContext.

Il prezzo: ogni hook è latenza sul percorso critico

Per impostazione predefinita l’agente aspetta il ritorno della callback. Un hook che interroga un servizio di policy remoto aggiunge quel round trip a ogni chiamata di tool che il matcher intercetta, e su un loop agentico lungo il conto si vede. Se l’hook serve solo per un effetto collaterale — log, metriche, webhook — esiste l’output asincrono, con cui l’agente prosegue senza aspettare. Il prezzo è dichiarato ed è totale: «Async outputs can’t block, modify, or inject context into the operation since the agent has already moved on».

I timeout predefiniti sono 600 secondi per la maggior parte degli eventi, 30 per l’invio del prompt utente e per i due eventi di cambio modello, 10 per la visualizzazione dei messaggi. Il modo di rompersi di PreToolUse va progettato esplicitamente, perché non è né un fallimento silenzioso né un blocco duro: alla scadenza «Claude Code doesn’t run the tool call, Claude receives a tool result stating the hook didn’t respond before its timeout, and the turn continues». Il turno va avanti con il tool non eseguito, e il tuo agente deve saperci convivere.

Un ultimo errore, banale e frequentissimo: updatedInput deve stare dentro hookSpecificOutput, non al livello superiore dell’oggetto restituito. Al livello superiore viene semplicemente ignorato, e l’agente scrive esattamente dove voleva scrivere.