SpyBara
Go Premium

agent-sdk/hooks.md 2026-11-08 22:58 UTC to 2026-11-09 23:02 UTC

No changes for this document in this history range.

2026
Tue 3 23:58 Fri 6 23:01 Tue 10 21:57

Intercettare e controllare il comportamento dell'agente con hooks

Intercettare e personalizzare il comportamento dell'agente nei punti chiave di esecuzione con hooks

Gli hooks sono funzioni di callback che eseguono il vostro codice in risposta agli eventi dell'agente, come una chiamata a uno strumento, l'avvio di una sessione o l'arresto dell'esecuzione. Con gli hooks, potete:

  • Bloccare operazioni pericolose prima che vengano eseguite, come comandi shell distruttivi o accesso a file non autorizzato
  • Registrare e controllare le chiamate agli strumenti per conformità, debug o analitiche
  • Trasformare input e output per sanitizzare i dati, iniettare credenziali o reindirizzare i percorsi dei file
  • Richiedere approvazione umana per azioni sensibili come scritture su database o chiamate API
  • Tracciare il ciclo di vita della sessione per gestire lo stato, pulire le risorse o inviare notifiche

Come funzionano gli hook

1. Un evento si attiva

Qualcosa accade durante l'esecuzione dell'agente e l'SDK attiva un evento: uno strumento sta per essere chiamato (PreToolUse), uno strumento ha restituito un risultato (PostToolUse), un subagent è stato avviato o interrotto, l'agente è inattivo o l'esecuzione è terminata. Consulta l'elenco completo degli eventi.

2. L'SDK raccoglie gli hook registrati

L'SDK verifica la presenza di hook registrati per quel tipo di evento. Questo include gli hook di callback che passi in options.hooks e gli hook dei comandi shell dai file di impostazioni quando la voce settingSources o setting_sources corrispondente è abilitata, come avviene per le opzioni predefinite di query().

3. I matcher filtrano quali hook vengono eseguiti

Se un hook ha un modello matcher (come "Write|Edit"), l'SDK lo testa rispetto al target dell'evento (ad esempio, il nome dello strumento). Gli hook senza un matcher vengono eseguiti per ogni evento di quel tipo.

4. Le funzioni di callback vengono eseguite

La funzione di callback di ogni hook corrispondente riceve input su ciò che sta accadendo: il nome dello strumento, i suoi argomenti, l'ID della sessione e altri dettagli specifici dell'evento.

5. Il tuo callback restituisce una decisione

Dopo aver eseguito qualsiasi operazione (log, chiamate API, convalida), il tuo callback restituisce un oggetto di output che dice all'agente cosa fare: consentire l'operazione, bloccarla, modificare l'input o iniettare contesto nella conversazione.

Questo esempio registra un hook PreToolUse (passaggio 1) con un matcher "Write|Edit" (passaggio 3) in modo che il callback si attivi per gli strumenti Write e Edit. Quando attivato, il callback riceve l'input dello strumento (passaggio 4), verifica se il percorso del file è destinato a un file .env e restituisce permissionDecision: "deny" per bloccare l'operazione (passaggio 5):

import asyncio
from claude_agent_sdk import (
    AssistantMessage,
    ClaudeSDKClient,
    ClaudeAgentOptions,
    HookMatcher,
    ResultMessage,
)


# Define a hook callback that receives tool call details
async def protect_env_files(input_data, tool_use_id, context):
    # Extract the file path from the tool's input arguments
    file_path = input_data["tool_input"].get("file_path", "")
    file_name = file_path.split("/")[-1]

    # Block the operation if targeting a .env file
    if file_name.lower() == ".env":
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": "Cannot modify .env files",
            }
        }

    # Return empty object to allow the operation
    return {}


async def main():
    options = ClaudeAgentOptions(
        hooks={
            # Register the hook for PreToolUse events
            # The matcher filters to only Write and Edit tool calls
            "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
        }
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Create a .env file with the standard local development database configuration")
        async for message in client.receive_response():
            # Filter for assistant and result messages
            if isinstance(message, (AssistantMessage, ResultMessage)):
                print(message)


asyncio.run(main())
import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";

// Define a hook callback with the HookCallback type
const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
  // Cast input to the specific hook type for type safety
  const preInput = input as PreToolUseHookInput;

  // Cast tool_input to access its properties (typed as unknown in the SDK)
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = toolInput?.file_path as string;
  const fileName = filePath?.split("/").pop();

  // Block the operation if targeting a .env file
  if (fileName?.toLowerCase() === ".env") {
    return {
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "deny",
        permissionDecisionReason: "Cannot modify .env files"
      }
    };
  }

  // Return empty object to allow the operation
  return {};
};

for await (const message of query({
  prompt: "Create a .env file with the standard local development database configuration",
  options: {
    hooks: {
      // Register the hook for PreToolUse events
      // The matcher filters to only Write and Edit tool calls
      PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
    }
  }
})) {
  // Filter for assistant and result messages
  if (message.type === "assistant" || message.type === "result") {
    console.log(message);
  }
}

L'hook nega una chiamata a Write o Edit il cui file_path termina con /.env, indipendentemente da maiuscole e minuscole. Su Windows, file_path usa le barre rovesciate, come in C:\project\.env, e questo hook consente una chiamata a Write o Edit verso C:\project\.env.

[!WARNING]

Il matcher Write|Edit limita questo hook agli strumenti Write e Edit. Se Claude scrive .env con un comando Bash, l'hook non viene eseguito. Per bloccare le scritture tramite Bash su un file in cui è attiva la sandbox, consulta la configurazione della sandbox per Python o TypeScript.

Hook disponibili

L'SDK fornisce hooks per diverse fasi dell'esecuzione dell'agente. Alcuni hooks sono disponibili in entrambi gli SDK, mentre altri sono solo per TypeScript.

Hook Event Python SDK TypeScript SDK Cosa lo attiva Caso d'uso di esempio
PreToolUse Sì Sì Richiesta di chiamata a uno strumento (può bloccare o modificare) Bloccare comandi shell pericolosi
PostToolUse Sì Sì Risultato dell'esecuzione dello strumento Registrare le modifiche ai file in un log di controllo
PostToolUseFailure Sì Sì Errore di esecuzione dello strumento Gestire o registrare errori dello strumento
PostToolBatch No Sì Un intero batch di chiamate a strumenti si risolve, una volta per batch prima della prossima chiamata al modello Iniettare convenzioni una volta per l'intero batch
UserPromptSubmit Sì Sì Viene inviato un prompt, incluso un turno che Claude Code avvia da solo Iniettare contesto aggiuntivo nei prompt
UserPromptExpansion No Sì Un comando digitato dall'utente, o un prompt MCP, si espande in un prompt prima di raggiungere Claude. Non si attiva quando Claude invoca una skill da solo Bloccare un comando dall'invocazione diretta o aggiungere contesto quando viene digitata una skill
MessageDisplay No Sì Un messaggio dell'assistente con testo si completa, una volta per messaggio con il testo completo del messaggio Oscurare o riformattare il testo visualizzato senza modificare la trascrizione
Stop Sì Sì Arresto dell'esecuzione dell'agente Salvare lo stato della sessione prima dell'uscita
StopFailure No Sì Il turno termina con un errore API invece di un arresto normale Registrare i guasti o inviare avvisi
SubagentStart Sì Sì Inizializzazione del subagente Tracciare l'avvio di attività parallele
SubagentStop Sì Sì Completamento del subagente Aggregare i risultati dalle attività parallele
PreCompact Sì Sì Richiesta di compattazione della conversazione Archiviare la trascrizione completa prima del riepilogo
PostCompact No Sì La compattazione della conversazione si completa Registrare il riepilogo generato
PreModelSwitch No Sì Un cambio di modello richiesto, prima che accada (può bloccare) Bloccare il passaggio a un modello specifico
PostModelSwitch No Sì Il modello della sessione cambia, incluso un fallback automatico Fornire a Claude una guida specifica del modello per il nuovo modello
PermissionRequest Sì Sì Una chiamata a uno strumento necessita di una decisione di autorizzazione Gestione personalizzata delle autorizzazioni
PermissionDenied No Sì La modalità automatica nega una chiamata a uno strumento, inclusi i rifiuti senza un verdetto del classificatore Registrare i rifiuti, o dire al modello che può riprovare; Claude Code ignora retry: true per i rifiuti senza verdetto. Vedere PermissionDenied
SessionStart No Sì Inizializzazione della sessione Inizializzare la registrazione e la telemetria
SessionEnd No Sì Terminazione della sessione Pulire le risorse temporanee
Notification Sì Sì Messaggi di stato dell'agente Inviare aggiornamenti dello stato dell'agente a Slack o PagerDuty
Setup No Sì Configurazione/manutenzione della sessione Eseguire attività di inizializzazione
TeammateIdle No Sì Il compagno di squadra diventa inattivo Riassegnare il lavoro o notificare
TaskCreated No Sì Un'attività viene creata tramite lo strumento TaskCreate Applicare convenzioni di denominazione delle attività
TaskCompleted No Sì Un'attività è contrassegnata come completata Richiedere il superamento dei test prima che un'attività si chiuda
Elicitation No Sì Un server MCP richiede l'input dell'utente a metà attività Rispondere alle richieste di input MCP a livello di programmazione
ElicitationResult No Sì Un utente risponde a un'elicitazione MCP Modificare o bloccare la risposta prima che ritorni al server
ConfigChange No Sì Il file di configurazione cambia Ricaricare le impostazioni dinamicamente
InstructionsLoaded No Sì Un file CLAUDE.md o di regole viene caricato nel contesto Controllare quali file di istruzioni vengono caricati
WorktreeCreate No Sì Git worktree creato Tracciare gli spazi di lavoro isolati
WorktreeRemove No Sì Un worktree creato da un hook WorktreeCreate viene rimosso Pulire le risorse del workspace
CwdChanged No Sì La directory di lavoro cambia durante una sessione Ricaricare le variabili di ambiente per directory
FileChanged No Sì Un file monitorato viene modificato, creato o eliminato Ricaricare la configurazione quando i file del progetto cambiano
DirectoryAdded No Sì Una directory di lavoro viene aggiunta durante una sessione Installare le dipendenze per un repository aggiunto a metà sessione

Configurare gli hook

Per configurare un hook, passalo nel campo hooks delle opzioni dell'agente (ClaudeAgentOptions in Python, l'oggetto options in TypeScript). Questo snippet presuppone che tu abbia già definito un callback hook, come protect_env_files in Python o protectEnvFiles in TypeScript dall'esempio precedente:

options = ClaudeAgentOptions(
    hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[my_callback])]}
)

async with ClaudeSDKClient(options=options) as client:
    await client.query("Your prompt")
    async for message in client.receive_response():
        print(message)
for await (const message of query({
  prompt: "Your prompt",
  options: {
    hooks: {
      PreToolUse: [{ matcher: "Bash", hooks: [myCallback] }]
    }
  }
})) {
  console.log(message);
}

L'opzione hooks è un dizionario in Python o un oggetto in TypeScript, dove:

Matcher

Utilizza i matcher per filtrare quando i tuoi callback si attivano. Il campo matcher corrisponde a un valore diverso a seconda del tipo di evento hook. Ad esempio, gli hook basati su strumenti corrispondono al nome dello strumento, mentre gli hook Notification corrispondono al tipo di notifica.

I matcher SDK seguono le stesse regole dei matcher nei file di impostazioni. Quella sezione documenta i percorsi di valutazione di stringa esatta e espressione regolare, i loro requisiti di versione e i valori di matcher per ogni tipo di evento.

Opzione Tipo Predefinito Descrizione
matcher string undefined Modello abbinato al campo di filtro dell'evento, seguendo le regole per i matcher nei file di impostazioni. Per gli hook degli strumenti, quel campo è il nome dello strumento. Gli strumenti incorporati includono Bash, Read, Write, Edit, Glob, Grep, WebFetch, Agent e altri. Consulta Tipi di input degli strumenti per l'elenco completo. Gli strumenti MCP utilizzano il modello mcp__<server>__<action>, dove <server> è la chiave che utilizzi nella configurazione mcpServers, scritta come descrive la convenzione di denominazione degli strumenti.
hooks HookCallback[] - Obbligatorio. Array di funzioni di callback da eseguire quando il modello corrisponde
timeout number undefined Timeout in secondi. Quando omesso, Claude Code applica il timeout predefinito dell'evento. I tuoi callback SDK seguono i valori predefiniti dell'hook command
onFailure "continue" | "block" undefined Cosa fa un callback non riuscito. Imposta "block" per bloccare l'azione quando un callback non riesce. In Python, utilizza on_failure

Utilizza il modello matcher per indirizzare strumenti specifici quando possibile. Con un matcher 'Bash', i tuoi callback vengono eseguiti per i comandi Bash. Su Claude Code v2.1.314 o successivo, vengono eseguiti anche su PreToolUse e PermissionRequest per i comandi Monitor, che possono negare ma non approvare. Senza matcher, vengono eseguiti per ogni occorrenza dell'evento.

Funzioni di callback

Input

Ogni callback hook riceve tre argomenti:

  • Dati di input: un oggetto tipizzato contenente i dettagli dell'evento. Ogni tipo di hook ha la sua forma di input. Ad esempio, PreToolUseHookInput include tool_name e tool_input, mentre NotificationHookInput include message. Consulta le definizioni di tipo complete nei riferimenti SDK TypeScript e Python.
    • Tutti gli input hook condividono session_id, cwd e hook_event_name.
    • agent_id e agent_type vengono popolati quando l'hook si attiva all'interno di un subagent. In TypeScript, questi si trovano sull'input hook di base e sono disponibili per tutti i tipi di hook. In Python, sono campi opzionali su PreToolUse, PostToolUse, PostToolUseFailure e PermissionRequest, e campi obbligatori su SubagentStart e SubagentStop.
  • ID di utilizzo dello strumento (str | None / string | undefined): correla gli eventi PreToolUse e PostToolUse per la stessa chiamata a uno strumento.
  • Contesto: in TypeScript, contiene una proprietà signal (AbortSignal) per l'annullamento. In Python, questo argomento è riservato per uso futuro.

Output

Il tuo callback restituisce un oggetto con due categorie di campi:

  • Campi di livello superiore sono accettati su ogni evento: systemMessage mostra un messaggio all'utente, e continue (continue_ in Python) determina se l'agente continua a funzionare dopo questo hook. Alcuni eventi li scartano o li consegnano altrove. La sezione di ogni evento sulla pagina degli hook dice dove finiscono.
  • hookSpecificOutput controlla l'operazione corrente. I campi che imposti all'interno dipendono dal tipo di evento hook:
    • Per gli hook PreToolUse, è qui che imposti permissionDecision ("allow", "deny", "ask" o "defer"), permissionDecisionReason e updatedInput. Se restituisci "defer", il turno termina con un messaggio di risultato il cui stop_reason è "tool_deferred", in modo da poter riprendere la chiamata in seguito.
    • Per gli hook PostToolUse, puoi impostare additionalContext per aggiungere informazioni al risultato dello strumento. Per sostituire l'output dello strumento prima che Claude lo veda, imposta updatedToolOutput, che funziona per qualsiasi strumento in entrambi gli SDK. Il campo più vecchio updatedMCPToolOutput sostituisce solo l'output dello strumento MCP.
    • Nel TypeScript SDK, un callback PostToolUse può anche restituire classifierContext, una breve nota sul risultato della chiamata allo strumento per il classificatore dei permessi della modalità auto. Poiché il tuo callback viene eseguito nel processo della tua applicazione, il classificatore può pesare una dichiarazione dell'utente che inoltri nella nota come intenzione dell'utente. Il campo richiede TypeScript Agent SDK v0.3.236 o successivo. Annotare un risultato per il classificatore della modalità auto copre il limite di lunghezza, la regola solo sincrona e cosa non mettere nella nota.

Restituisci {} per consentire l'operazione senza modifiche. Gli hook di callback SDK utilizzano lo stesso formato di output JSON degli hook dei comandi shell di Claude Code, che documenta ogni campo e opzione specifica dell'evento. Per le definizioni di tipo SDK, consulta i riferimenti SDK TypeScript e Python.

[!NOTE]

Quando più hook PreToolUse restituiscono decisioni diverse, deny ha priorità su defer, che ha priorità su ask, che ha priorità su allow.

Output asincrono

Per impostazione predefinita, l'agente attende che il tuo hook restituisca prima di procedere. Se il tuo hook esegue un effetto collaterale, come la registrazione di log o l'invio di un webhook, e non ha bisogno di influenzare il comportamento dell'agente, puoi restituire un output asincrono. Questo dice all'agente di continuare immediatamente senza attendere il completamento dell'hook. In questo snippet, send_to_logging_service in Python e sendToLoggingService in TypeScript rappresentano qualsiasi funzione di logging che definisci:

async def async_hook(input_data, tool_use_id, context):
    # Start a background task, then return immediately
    asyncio.create_task(send_to_logging_service(input_data))
    return {"async_": True, "asyncTimeout": 30000}
const asyncHook: HookCallback = async (input, toolUseID, { signal }) => {
  // Start a background task, then return immediately
  sendToLoggingService(input).catch(console.error);
  return { async: true, asyncTimeout: 30000 };
};
Campo Tipo Descrizione
async true Segnala la modalità asincrona. L'agente procede senza attendere. In Python, utilizza async_ per evitare la parola chiave riservata.
asyncTimeout number Timeout opzionale in millisecondi per l'operazione in background

[!NOTE]

Gli output asincroni non possono bloccare, modificare o iniettare contesto nell'operazione poiché l'agente ha già proseguito. Utilizzali solo per effetti collaterali come logging, metriche o notifiche.

Bloccare l'azione quando un callback non riesce

Nella maggior parte degli eventi, se un callback genera un'eccezione o restituisce un output che non supera la validazione, l'azione procede e la tua app riceve un avviso SDKInformationalMessage il cui tag è hook_callback_failed. Un controllo PreToolUse che si arresta in modo anomalo lascia quindi che la chiamata allo strumento raggiunga il normale flusso dei permessi. Anche un callback che non restituisce nulla, come un percorso senza return, conta come non riuscito. In Python, leggi tag da SystemMessage.data.

Per bloccare invece l'azione, imposta onFailure: "block" in TypeScript o on_failure="block" in Python sul matcher. Richiede TypeScript Agent SDK v0.3.317 o successivo, oppure Python Agent SDK v0.2.177 o successivo. Questo esempio blocca una chiamata Bash quando check_policy in Python o checkPolicy in TypeScript non riesce:

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Bash", hooks=[check_policy], on_failure="block")
        ]
    }
)
import type { Options } from "@anthropic-ai/claude-agent-sdk";

const options: Options = {
  hooks: {
    PreToolUse: [{ matcher: "Bash", hooks: [checkPolicy], onFailure: "block" }]
  }
};

Per testarlo, fai in modo che il callback generi un errore e chiedi a Claude di eseguire un comando Bash. Il risultato dello strumento include failed; blocking because onFailure is "block", e Claude non vede il testo del tuo errore.

Restituisci {} quando il callback non ha un'opinione. Con "block" impostato, un percorso senza return blocca l'azione. Un callback che intercetta il proprio errore e restituisce {} non è considerato non riuscito, quindi lascia che l'errore si propaghi da un controllo.

Eventi che si comportano diversamente

Questi eventi non sempre bloccano con "block" impostato:

  • Stop, SubagentStop, TaskCompleted e TeammateIdle: questi eventi ignorano l'opzione, quindi l'azione procede.
  • PermissionRequest: il blocco conta solo se arriva prima che il tuo callback canUseTool risponda.
  • PostToolUse: lo strumento è già stato eseguito, quindi Claude riceve l'errore come feedback.

Un callback che supera il proprio timeout segue Timeout dell'hook. Con "block" impostato, un timeout blocca anche PreCompact, Elicitation e WorktreeCreate.

Errori di avvio per onFailure

Con "block" impostato, la query non riesce all'avvio e non invia alcun prompt quando non può rispettare l'opzione:

  • Claude Code non supporta l'opzione: l'errore include does not honor. Aggiorna Claude Code o rimuovi l'opzione.
  • La sessione era già in esecuzione e non ha registrato i tuoi hook: l'errore include did not register your hooks. Avvia una nuova sessione.

Esempi

Diversi esempi in questa sezione mostrano solo la funzione di callback. Per eseguirne uno, registrate il callback sotto l'evento corrispondente nel campo hooks delle vostre opzioni, come mostrato in Configurare gli hooks.

Modificare l'input dello strumento

Questo esempio intercetta le chiamate allo strumento Write e riscrive l'argomento file_path per anteporre /sandbox. Il callback restituisce updatedInput con il percorso modificato e permissionDecision: 'allow' per approvare automaticamente l'operazione riscritta:

async def redirect_to_sandbox(input_data, tool_use_id, context):
    if input_data["hook_event_name"] != "PreToolUse":
        return {}

    if input_data["tool_name"] == "Write":
        original_path = input_data["tool_input"].get("file_path", "")
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "allow",
                "updatedInput": {
                    **input_data["tool_input"],
                    "file_path": f"/sandbox{original_path}",
                },
            }
        }
    return {}
const redirectToSandbox: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name !== "PreToolUse") return {};

  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  if (preInput.tool_name === "Write") {
    const originalPath = toolInput.file_path as string;
    return {
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "allow",
        updatedInput: {
          ...toolInput,
          file_path: `/sandbox${originalPath}`
        }
      }
    };
  }
  return {};
};

[!NOTE]

Abbinate updatedInput con permissionDecision: 'allow' per approvare automaticamente l'input modificato, oppure permissionDecision: 'ask' per mostrarlo all'utente. Se omettete permissionDecision, l'input modificato si applica comunque e scorre attraverso la valutazione normale dei permessi. Con 'defer', updatedInput viene ignorato. Restituite sempre un nuovo oggetto piuttosto che mutare l'originale tool_input.

Per confermare il reindirizzamento, impostate il prefisso a un percorso in cui potete scrivere, come ./sandbox o /tmp/sandbox (macOS non consente di creare una directory /sandbox a livello di root), quindi chiedete all'agente di scrivere un file: il risultato dello strumento Write nel flusso di messaggi nomina il percorso con il vostro prefisso sandbox piuttosto che quello che Claude ha richiesto.

Aggiungere contesto e bloccare uno strumento

Questo esempio nega le chiamate agli strumenti il cui file_path inizia con /etc e spiega il motivo sia al modello sia all'utente:

  • permissionDecision: 'deny' interrompe la chiamata dello strumento.
  • permissionDecisionReason comunica al modello il motivo.
  • systemMessage mostra all'utente cosa è accaduto.
async def block_etc_writes(input_data, tool_use_id, context):
    file_path = input_data["tool_input"].get("file_path", "")

    if file_path.startswith("/etc"):
        return {
            # Top-level field: message shown to the user
            "systemMessage": "Remember: system directories like /etc are protected.",
            # hookSpecificOutput: block the operation
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": "Writing to /etc is not allowed",
            },
        }
    return {}
const blockEtcWrites: HookCallback = async (input, toolUseID, { signal }) => {
  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = toolInput?.file_path as string;

  if (filePath?.startsWith("/etc")) {
    return {
      // Top-level field: message shown to the user
      systemMessage: "Remember: system directories like /etc are protected.",
      // hookSpecificOutput: block the operation
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "deny",
        permissionDecisionReason: "Writing to /etc is not allowed"
      }
    };
  }
  return {};
};

Per confermare il blocco, registrate il callback sotto PreToolUse con un matcher Write|Edit e chiedete all'agente di creare un file sotto /etc: il risultato dello strumento Write nel flusso di messaggi contiene Writing to /etc is not allowed, e nessun file viene creato.

Approvare automaticamente strumenti specifici

Per impostazione predefinita, l'agente potrebbe chiedere il permesso prima di utilizzare determinati strumenti. Questo esempio approva automaticamente gli strumenti del file system di sola lettura (Read, Glob, Grep) restituendo permissionDecision: 'allow', consentendo loro di essere eseguiti senza conferma dell'utente, fatta eccezione per le letture da percorsi di rete, mentre lascia tutti gli altri strumenti soggetti ai normali controlli dei permessi:

async def auto_approve_read_only(input_data, tool_use_id, context):
    if input_data["hook_event_name"] != "PreToolUse":
        return {}

    read_only_tools = ["Read", "Glob", "Grep"]
    if input_data["tool_name"] in read_only_tools:
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "allow",
                "permissionDecisionReason": "Read-only tool auto-approved",
            }
        }
    return {}
const autoApproveReadOnly: HookCallback = async (input, toolUseID, { signal }) => {
  if (input.hook_event_name !== "PreToolUse") return {};

  const preInput = input as PreToolUseHookInput;
  const readOnlyTools = ["Read", "Glob", "Grep"];
  if (readOnlyTools.includes(preInput.tool_name)) {
    return {
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "allow",
        permissionDecisionReason: "Read-only tool auto-approved"
      }
    };
  }
  return {};
};

Registrare più hooks

Quando un evento si attiva, gli hook corrispondenti vengono eseguiti in parallelo. Per le decisioni sui permessi, si applica il risultato più restrittivo. Il deny di un singolo hook blocca la chiamata allo strumento. Poiché l'ordine di completamento è non deterministico, scrivi ogni hook in modo che agisca in modo indipendente, invece di fare affidamento sul fatto che un altro hook sia stato eseguito per primo.

L'esempio seguente registra tre controlli indipendenti, ciascuno con un matcher omesso. I nomi degli hook al suo interno, come audit_logger in Python o auditLogger in TypeScript, stanno al posto di callback che definisci tu:

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(hooks=[authorization_check]),
            HookMatcher(hooks=[input_validator]),
            HookMatcher(hooks=[audit_logger]),
        ]
    }
)
const options = {
  hooks: {
    PreToolUse: [
      { hooks: [authorizationCheck] },
      { hooks: [inputValidator] },
      { hooks: [auditLogger] }
    ]
  }
};

Filtrare con matcher multi-strumento

Utilizzate matcher multi-strumento per condividere un callback tra strumenti correlati. Questo esempio registra tre matcher con ambiti diversi, e ogni hook che nomina sta al posto di un callback che voi definite:

  • Un elenco esatto separato da pipe (Write|Edit|NotebookEdit) attiva file_security_hook per gli strumenti Write, Edit e NotebookEdit.
  • Un'espressione regolare (^mcp__) attiva mcp_audit_hook per qualsiasi strumento MCP il cui nome inizia con mcp__.
  • Un matcher omesso attiva global_logger senza filtrare per nome dello strumento.
options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            # Match file modification tools
            HookMatcher(matcher="Write|Edit|NotebookEdit", hooks=[file_security_hook]),
            # Match all MCP tools
            HookMatcher(matcher="^mcp__", hooks=[mcp_audit_hook]),
            # Match everything (no matcher)
            HookMatcher(hooks=[global_logger]),
        ]
    }
)
const options = {
  hooks: {
    PreToolUse: [
      // Match file modification tools
      { matcher: "Write|Edit|NotebookEdit", hooks: [fileSecurityHook] },

      // Match all MCP tools
      { matcher: "^mcp__", hooks: [mcpAuditHook] },

      // Match everything (no matcher)
      { hooks: [globalLogger] }
    ]
  }
};

Tracciare l'attività dei subagenti

Utilizzate gli hooks SubagentStop per monitorare quando i subagenti completano il loro lavoro. Consultate il tipo di input completo nei riferimenti SDK TypeScript e Python. Questo esempio registra un riepilogo ogni volta che un subagente si completa:

async def subagent_tracker(input_data, tool_use_id, context):
    # Log subagent details when it finishes
    print(f"[SUBAGENT] Completed: {input_data['agent_id']}")
    print(f"  Transcript: {input_data['agent_transcript_path']}")
    print(f"  Tool use ID: {tool_use_id}")
    print(f"  Stop hook active: {input_data.get('stop_hook_active')}")
    return {}


options = ClaudeAgentOptions(
    hooks={"SubagentStop": [HookMatcher(hooks=[subagent_tracker])]}
)
import { HookCallback, SubagentStopHookInput } from "@anthropic-ai/claude-agent-sdk";

const subagentTracker: HookCallback = async (input, toolUseID, { signal }) => {
  // Cast to SubagentStopHookInput to access subagent-specific fields
  const subInput = input as SubagentStopHookInput;

  // Log subagent details when it finishes
  console.log(`[SUBAGENT] Completed: ${subInput.agent_id}`);
  console.log(`  Transcript: ${subInput.agent_transcript_path}`);
  console.log(`  Tool use ID: ${toolUseID}`);
  console.log(`  Stop hook active: ${subInput.stop_hook_active}`);
  return {};
};

const options = {
  hooks: {
    SubagentStop: [{ hooks: [subagentTracker] }]
  }
};

Per confermare che l'hook si attiva, registrate il callback e chiedete all'agente di delegare un piccolo compito a un subagente, come elencare i file nella directory corrente: quando il subagente finisce, il callback stampa le righe [SUBAGENT] Completed: con l'ID del subagente e il percorso della trascrizione.

Effettuare richieste HTTP dagli hooks

Gli hooks possono eseguire operazioni asincrone come richieste HTTP. Catturate gli errori all'interno del vostro hook invece di lasciarli propagare.

Questo esempio invia un webhook dopo il completamento di ogni strumento, registrando quale strumento è stato eseguito e quando. L'hook cattura gli errori da un webhook non riuscito:

import asyncio
import json
import urllib.request
from datetime import datetime


def _send_webhook(tool_name):
    """Synchronous helper that POSTs tool usage data to an external webhook."""
    data = json.dumps(
        {
            "tool": tool_name,
            "timestamp": datetime.now().isoformat(),
        }
    ).encode()
    req = urllib.request.Request(
        "https://api.example.com/webhook",
        data=data,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    urllib.request.urlopen(req)


async def webhook_notifier(input_data, tool_use_id, context):
    # Only fire after a tool completes (PostToolUse), not before
    if input_data["hook_event_name"] != "PostToolUse":
        return {}

    try:
        # Run the blocking HTTP call in a thread to avoid blocking the event loop
        await asyncio.to_thread(_send_webhook, input_data["tool_name"])
    except Exception as e:
        # Log the error but don't raise
        print(f"Webhook request failed: {e}")

    return {}
import { query, HookCallback, PostToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";

const webhookNotifier: HookCallback = async (input, toolUseID, { signal }) => {
  // Only fire after a tool completes (PostToolUse), not before
  if (input.hook_event_name !== "PostToolUse") return {};

  try {
    await fetch("https://api.example.com/webhook", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        tool: (input as PostToolUseHookInput).tool_name,
        timestamp: new Date().toISOString()
      }),
      // Pass signal so the request cancels if the hook times out
      signal
    });
  } catch (error) {
    // Handle cancellation separately from other errors
    if (error instanceof Error && error.name === "AbortError") {
      console.log("Webhook request cancelled");
    }
    // Don't re-throw
  }

  return {};
};

// Register as a PostToolUse hook
for await (const message of query({
  prompt: "Refactor the auth module",
  options: {
    hooks: {
      PostToolUse: [{ hooks: [webhookNotifier] }]
    }
  }
})) {
  console.log(message);
}

Per confermare che l'hook si attiva, puntate l'URL del webhook a un endpoint che potete osservare e inviate un prompt che utilizza uno strumento: l'hook invia un POST con il nome dello strumento e il timestamp dopo il completamento di ogni strumento.

Inoltrare le notifiche a Slack

Utilizzate gli hooks Notification per ricevere notifiche di sistema dall'agente e inoltrarle a servizi esterni. Nelle sessioni SDK, Claude Code esegue questo hook per i seguenti tipi di notifica:

  • permission_prompt una volta che una richiesta di autorizzazione ha atteso circa sei secondi sul vostro callback canUseTool. Richiede TypeScript Agent SDK v0.3.233 o successivo, oppure Python Agent SDK v0.2.139 o successivo
  • elicitation_complete e elicitation_response per i flussi di elicitazione dell'input dell'utente
  • plugin_notification quando un mod chiama $.ui.notify

Claude Code emette gli altri tipi, come idle_prompt, auth_success e elicitation_dialog, dall'interfaccia utente interattiva che le sessioni SDK non eseguono.

Ogni notifica include un campo message con una descrizione leggibile dall'uomo e facoltativamente un title.

Questo esempio inoltra ogni notifica a un canale Slack. Richiede un URL webhook in arrivo di Slack, che create aggiungendo un'app al vostro spazio di lavoro Slack e abilitando i webhook in arrivo:

import asyncio
import json
import urllib.request

from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, HookMatcher


def _send_slack_notification(message):
    """Synchronous helper that sends a message to Slack via incoming webhook."""
    data = json.dumps({"text": f"Agent status: {message}"}).encode()
    req = urllib.request.Request(
        "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
        data=data,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    urllib.request.urlopen(req)


async def notification_handler(input_data, tool_use_id, context):
    try:
        # Run the blocking HTTP call in a thread to avoid blocking the event loop
        await asyncio.to_thread(_send_slack_notification, input_data.get("message", ""))
    except Exception as e:
        print(f"Failed to send notification: {e}")

    # Return empty object. Notification hooks don't modify agent behavior
    return {}


async def main():
    options = ClaudeAgentOptions(
        hooks={
            # Register the hook for Notification events (no matcher needed)
            "Notification": [HookMatcher(hooks=[notification_handler])],
        },
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Analyze this codebase")
        async for message in client.receive_response():
            print(message)


asyncio.run(main())
import { query, HookCallback, NotificationHookInput } from "@anthropic-ai/claude-agent-sdk";

// Define a hook callback that sends notifications to Slack
const notificationHandler: HookCallback = async (input, toolUseID, { signal }) => {
  // Cast to NotificationHookInput to access the message field
  const notification = input as NotificationHookInput;

  try {
    // POST the notification message to a Slack incoming webhook
    await fetch("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        text: `Agent status: ${notification.message}`
      }),
      // Pass signal so the request cancels if the hook times out
      signal
    });
  } catch (error) {
    if (error instanceof Error && error.name === "AbortError") {
      console.log("Notification cancelled");
    } else {
      console.error("Failed to send notification:", error);
    }
  }

  // Return empty object. Notification hooks don't modify agent behavior
  return {};
};

// Register the hook for Notification events (no matcher needed)
for await (const message of query({
  prompt: "Analyze this codebase",
  options: {
    hooks: {
      Notification: [{ hooks: [notificationHandler] }]
    }
  }
})) {
  console.log(message);
}

Quando un evento Notification si attiva, l'hook pubblica il message della notifica, con il prefisso Agent status:, al canale a cui il vostro webhook è destinato.

Risolvere i problemi comuni

Hook non si attiva

  • Verifica che il nome dell'evento hook sia corretto e sensibile alle maiuscole (PreToolUse, non preToolUse)
  • Controlla che il tuo pattern di matcher corrisponda esattamente al nome dello strumento
  • Assicurati che l'hook sia sotto il tipo di evento corretto in options.hooks
  • Per gli hook non basati su strumenti che supportano matcher, come Notification e SubagentStop, i matcher corrispondono a campi diversi, e Stop ignora completamente i matcher (consulta pattern di matcher)
  • Gli hook potrebbero non attivarsi quando l'agente raggiunge il limite max_turns perché la sessione termina prima che gli hook possano essere eseguiti

Matcher non filtra come previsto

I matcher corrispondono solo ai nomi degli strumenti, non ai percorsi dei file o ad altri argomenti. Per filtrare per percorso di file, controlla tool_input.file_path all'interno del tuo hook:

const myHook: HookCallback = async (input, toolUseID, { signal }) => {
  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = toolInput?.file_path as string;
  if (!filePath?.endsWith(".md")) return {}; // Skip non-markdown files
  // Process markdown files...
  return {};
};

Timeout dell'hook

Claude Code esegue ogni callback con un timeout, che imposti in secondi con il campo timeout sul suo HookMatcher. Quando non ne imposti uno, Claude Code utilizza il valore predefinito dell'evento: 600 secondi per la maggior parte degli eventi, 30 secondi per UserPromptSubmit, PreModelSwitch e PostModelSwitch, e 10 secondi per MessageDisplay. Claude Code esegue i callback SessionEnd durante l'arresto con il budget di timeout di SessionEnd più breve, 1,5 secondi per impostazione predefinita.

Quando un callback supera il suo timeout, Claude Code lo annulla e scarta il suo output, e la sessione continua anziché bloccarsi. Quello che accade dopo dipende dall'evento:

  • PreToolUse: Claude Code blocca la chiamata allo strumento. Se ogni altro hook non ha restituito nulla o solo una decisione allow, il risultato dello strumento indica a Claude che l'hook non ha risposto prima del suo timeout, e il turno continua. Se un altro dei tuoi hook ha già negato la chiamata, il suo rifiuto raggiunge comunque Claude: il risultato dello strumento include la motivazione di quell'hook, e il testo del timeout può seguirla.

    Prima della v2.1.210, Claude Code segnalava il timeout a Claude come un rifiuto dell'utente, il che faceva fermare le sessioni incustodite in attesa di input.

  • PostToolUse e PostToolUseFailure: Claude Code mantiene il risultato dello strumento e il turno continua.

  • UserPromptSubmit e UserPromptExpansion: Claude Code blocca il prompt con un messaggio che nomina l'hook e il timeout, e la sessione continua. Prima della v2.1.208, Claude Code terminava la query con error_during_execution quando un callback su questi eventi scadeva.

  • Stop e SubagentStop: il callback scaduto conta come se non avesse restituito alcuna decisione. L'agente o il subagent si ferma come se quel callback lo avesse consentito, e una decisione dei tuoi altri hook sull'evento si applica comunque. Prima di Claude Code v2.1.273, un callback Stop o SubagentStop scaduto contava come un'esecuzione di hook non riuscita, e Claude Code scartava le decisioni dei tuoi altri hook sull'evento.

  • SessionStart: il callback scaduto conta come se non avesse restituito alcun output, e la sessione continua con l'output dei tuoi altri hook SessionStart.

  • PreModelSwitch: Claude Code blocca il cambio di modello. Un hook che non risponde non ha approvato il cambio.

  • Altri eventi, come Notification, PreCompact e PostModelSwitch: Claude Code registra l'errore nei log e continua. Con onFailure: "block", un timeout su PreCompact, Elicitation o WorktreeCreate blocca l'azione.

La prima volta che un callback Stop o SessionStart scade nella sessione principale, Claude Code aggiunge anche un SDKInformationalMessage al flusso dei messaggi indicando che l'app che guida la sessione non ha risposto. I timeout successivi non ripetono quel messaggio finché la tua app rimane non responsiva.

Se interrompi la query mentre un callback è in sospeso, Claude Code annulla la chiamata allo strumento in sospeso. Prima della v2.1.208, la chiamata allo strumento poteva ancora procedere se interrompevi durante un callback PreToolUse in sospeso.

Se il tuo callback ha bisogno di più tempo, imposta un timeout più alto sul suo HookMatcher. In TypeScript, utilizza l'AbortSignal dal terzo argomento del callback per gestire l'annullamento in modo pulito quando scatta il timeout.

Strumento bloccato inaspettatamente

  • Controlla tutti gli hook PreToolUse per i ritorni permissionDecision: 'deny'
  • Aggiungi dei log ai tuoi hook per vedere quale permissionDecisionReason stanno restituendo
  • Verifica che i pattern di matcher non siano troppo ampi: un matcher vuoto corrisponde a tutti gli strumenti

Input modificato non applicato

  • Assicurati che updatedInput sia all'interno di hookSpecificOutput, non al livello superiore:

    return {
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "allow",
        updatedInput: { command: "new command" }
      }
    };
    
  • Non abbinare updatedInput a permissionDecision: 'defer', che scarta l'input modificato. Omettere permissionDecision va bene: l'input modificato si applica comunque attraverso la normale valutazione dei permessi. Puoi anche restituire 'allow' per approvare automaticamente l'input modificato o 'ask' per mostrarlo all'utente per l'approvazione

  • Includi hookEventName in hookSpecificOutput per identificare a quale tipo di hook si riferisce l'output

Hook di sessione non disponibili in Python

SessionStart e SessionEnd possono essere registrati come hook di callback SDK in TypeScript, ma non sono disponibili nell'SDK Python perché il suo tipo HookEvent li omette. In Python, sono disponibili solo come hook di comandi shell definiti in file di impostazioni come .claude/settings.json. Quali file di impostazioni carica la tua applicazione SDK dipende da setting_sources o settingSources. Se imposti quell'opzione, includi la fonte che contiene gli hook:

options = ClaudeAgentOptions(
    setting_sources=["project"],  # Loads .claude/settings.json including hooks
)
const options = {
  settingSources: ["project"] // Loads .claude/settings.json including hooks
};

Per eseguire invece la logica di inizializzazione come callback dell'SDK Python, utilizza il primo messaggio da client.receive_response() come trigger.

Le richieste di permesso dei subagent si moltiplicano

Quando avvii più subagent, ognuno potrebbe richiedere permessi separatamente per le proprie chiamate agli strumenti. Per evitare richieste ripetute, utilizza gli hook PreToolUse per approvare automaticamente strumenti specifici, oppure configura regole di permesso, che i subagent ereditano dalla conversazione principale.

Loop ricorsivi di hook con subagent

Un hook UserPromptSubmit che avvia subagent può creare loop infiniti se quei subagent attivano lo stesso hook. Per prevenire questo:

  • Utilizza una variabile condivisa o lo stato della sessione per tenere traccia del fatto che ti trovi già all'interno di un subagent
  • Limita gli hook in modo che vengano eseguiti solo per la sessione dell'agente di livello superiore

systemMessage non appare nell'output

Il campo systemMessage mostra un messaggio all'utente, non al modello. Su Claude Code v2.1.227 o successivo, il systemMessage di un hook può emergere nel flusso dei messaggi come un SDKInformationalMessage. Che ciò avvenga dipende dall'evento. La sezione di ogni evento nella pagina degli hook indica come emerge l'output. Per passare invece il contesto al modello, restituisci additionalContext.

Prima della v2.1.227, l'SDK faceva emergere l'output degli hook nel flusso dei messaggi solo per gli hook SessionStart e Setup. Per qualsiasi altro evento, l'output appariva solo negli eventi del ciclo di vita che includeHookEvents (include_hook_events in Python) aggiunge. La voce di quell'opzione descrive quali eventi del ciclo di vita produce ogni evento hook.

Se hai bisogno di far emergere in modo affidabile le decisioni degli hook nella tua applicazione, registrale separatamente nei log o utilizza un canale di output dedicato.

Risorse correlate