SpyBara
Go Premium

agent-sdk/hooks.md 2026-11-10 21:57 UTC to 2026-11-11 22:59 UTC

No changes for this document in this history range.

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

Intercepter et contrôler le comportement des agents avec des hooks

Interceptez et personnalisez le comportement des agents aux points d'exécution clés avec des hooks

Les hooks sont des fonctions de rappel qui exécutent votre code en réponse à des événements d'agent, comme un outil appelé, une session démarrée ou l'exécution arrêtée. Avec les hooks, vous pouvez :

  • Bloquer les opérations dangereuses avant leur exécution, comme les commandes shell destructrices ou l'accès non autorisé aux fichiers
  • Journaliser et auditer les appels d'outils pour la conformité, le débogage ou l'analyse
  • Transformer les entrées et les sorties pour nettoyer les données, injecter des identifiants ou rediriger les chemins de fichiers
  • Exiger une approbation humaine pour les actions sensibles comme les écritures de base de données ou les appels API
  • Suivre le cycle de vie de la session pour gérer l'état, nettoyer les ressources ou envoyer des notifications

Fonctionnement des hooks

1. Un événement se déclenche

Quelque chose se produit lors de l'exécution de l'agent et le SDK déclenche un événement : un outil est sur le point d'être appelé (PreToolUse), un outil a renvoyé un résultat (PostToolUse), un sous-agent a démarré ou s'est arrêté, l'agent est inactif ou l'exécution s'est terminée. Consultez la liste complète des événements.

2. Le SDK collecte les hooks enregistrés

Le SDK vérifie les hooks enregistrés pour ce type d'événement. Cela inclut les hooks de rappel que vous transmettez dans options.hooks et les hooks de commande shell à partir des fichiers de paramètres lorsque l'entrée settingSources ou setting_sources correspondante est activée, ce qui est le cas pour les options query() par défaut.

3. Les matchers filtrent les hooks qui s'exécutent

Si un hook a un motif matcher (comme "Write|Edit"), le SDK le teste par rapport à la cible de l'événement (par exemple, le nom de l'outil). Les hooks sans matcher s'exécutent pour chaque événement de ce type.

4. Les fonctions de rappel s'exécutent

Chaque fonction de rappel du hook correspondant reçoit des informations sur ce qui se passe : le nom de l'outil, ses arguments, l'ID de session et d'autres détails spécifiques à l'événement.

5. Votre rappel retourne une décision

Après avoir effectué toute opération (journalisation, appels API, validation), votre rappel retourne un objet de sortie qui indique à l'agent quoi faire : autoriser l'opération, la bloquer, modifier l'entrée ou injecter du contexte dans la conversation.

Cet exemple enregistre un hook PreToolUse (étape 1) avec un matcher "Write|Edit" (étape 3) afin que le rappel se déclenche pour les outils Write et Edit. Lorsqu'il est déclenché, le rappel reçoit l'entrée de l'outil (étape 4), vérifie si le chemin du fichier cible un fichier .env et retourne permissionDecision: "deny" pour bloquer l'opération (étape 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);
  }
}

Le hook refuse un appel Write ou Edit dont le file_path se termine par /.env, quelle que soit la casse. Sous Windows, file_path utilise des barres obliques inverses, comme C:\project\.env, et ce hook autorise un appel Write ou Edit vers C:\project\.env.

[!WARNING]

Le matcher Write|Edit limite ce hook aux outils Write et Edit. Si Claude écrit .env avec une commande Bash, le hook ne s'exécute pas. Pour bloquer les écritures Bash dans un fichier là où le sandbox s'exécute, consultez la configuration du sandbox pour Python ou TypeScript.

Hooks disponibles

Le SDK fournit des hooks pour différentes étapes de l'exécution de l'agent. Certains hooks sont disponibles dans les deux SDK, tandis que d'autres sont réservés à TypeScript.

Événement Hook SDK Python SDK TypeScript Ce qui le déclenche Cas d'usage exemple
PreToolUse Oui Oui Demande d'appel d'outil (peut bloquer ou modifier) Bloquer les commandes shell dangereuses
PostToolUse Oui Oui Résultat de l'exécution de l'outil Consigner les changements de fichiers dans une piste d'audit
PostToolUseFailure Oui Oui Échec de l'exécution de l'outil Gérer ou enregistrer les erreurs d'outil
PostToolBatch Non Oui Un lot complet d'appels d'outil se résout, une fois par lot avant l'appel du modèle suivant Injecter des conventions une fois pour tout le lot
UserPromptSubmit Oui Oui Un prompt est soumis, y compris un tour que Claude Code démarre de lui-même Injecter du contexte supplémentaire dans les prompts
UserPromptExpansion Non Oui Une commande tapée par l'utilisateur, ou une invite MCP, se développe en une invite avant d'atteindre Claude. Ne se déclenche pas quand Claude invoque une compétence lui-même Bloquer une commande d'invocation directe ou ajouter du contexte quand une compétence est tapée
MessageDisplay Non Oui Un message d'assistant avec du texte se termine, une fois par message avec le texte complet du message Masquer ou reformater le texte affiché sans modifier la transcription
Stop Oui Oui Arrêt de l'exécution de l'agent Enregistrer l'état de la session avant la sortie
StopFailure Non Oui Le tour se termine par une erreur API au lieu d'un arrêt normal Enregistrer les défaillances ou envoyer des alertes
SubagentStart Oui Oui Initialisation du sous-agent Suivre le lancement des tâches parallèles
SubagentStop Oui Oui Achèvement du sous-agent Agréger les résultats des tâches parallèles
PreCompact Oui Oui Demande de compaction de conversation Archiver la transcription complète avant le résumé
PostCompact Non Oui La compaction de conversation se termine Enregistrer le résumé généré
PreModelSwitch Non Oui Un changement de modèle demandé, avant qu'il ne se produise (peut bloquer) Bloquer le changement vers un modèle spécifique
PostModelSwitch Non Oui Le modèle de la session change, y compris un basculement automatique Donner à Claude des conseils spécifiques au modèle pour le nouveau modèle
PermissionRequest Oui Oui Un appel d'outil nécessite une décision de permission Gestion des permissions personnalisée
PermissionDenied Non Oui Le mode automatique refuse un appel d'outil, y compris les refus sans verdict du classificateur Enregistrer les refus, ou dire au modèle qu'il peut réessayer ; Claude Code ignore retry: true pour les refus sans verdict. Voir PermissionDenied
SessionStart Non Oui Initialisation de la session Initialiser la journalisation et la télémétrie
SessionEnd Non Oui Arrêt de la session Nettoyer les ressources temporaires
Notification Oui Oui Messages d'état de l'agent Envoyer les mises à jour d'état de l'agent à Slack ou PagerDuty
Setup Non Oui Configuration/maintenance de la session Exécuter les tâches d'initialisation
TeammateIdle Non Oui Le coéquipier devient inactif Réassigner le travail ou notifier
TaskCreated Non Oui Une tâche est créée via l'outil TaskCreate Appliquer les conventions de nommage des tâches
TaskCompleted Non Oui Une tâche est marquée comme terminée Exiger des tests réussis avant la fermeture d'une tâche
Elicitation Non Oui Un serveur MCP demande une entrée utilisateur en cours de tâche Répondre aux demandes d'entrée MCP par programmation
ElicitationResult Non Oui Un utilisateur répond à une élicitation MCP Modifier ou bloquer la réponse avant qu'elle ne retourne au serveur
ConfigChange Non Oui Le fichier de configuration change Recharger les paramètres dynamiquement
InstructionsLoaded Non Oui Un fichier CLAUDE.md ou de règles est chargé dans le contexte Auditer quels fichiers d'instructions se chargent
WorktreeCreate Non Oui Git worktree créé Suivre les espaces de travail isolés
WorktreeRemove Non Oui Un worktree créé par un hook WorktreeCreate est en cours de suppression Nettoyer les ressources de l'espace de travail
CwdChanged Non Oui Le répertoire de travail change pendant une session Recharger les variables d'environnement par répertoire
FileChanged Non Oui Un fichier surveillé est modifié, créé ou supprimé Recharger la configuration quand les fichiers du projet changent
DirectoryAdded Non Oui Un répertoire de travail est ajouté pendant une session Installer les dépendances pour un référentiel ajouté en cours de session

Configurer les hooks

Pour configurer un hook, transmettez-le dans le champ hooks de vos options d'agent (ClaudeAgentOptions en Python, l'objet options en TypeScript). Cet extrait suppose que vous avez déjà défini un rappel hook, comme protect_env_files en Python ou protectEnvFiles en TypeScript à partir de l'exemple ci-dessus :

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'option hooks est un dictionnaire en Python ou un objet en TypeScript, où :

Matchers

Utilisez les matchers pour filtrer quand vos rappels se déclenchent. Le champ matcher correspond à une valeur différente selon le type d'événement hook. Par exemple, les hooks basés sur les outils correspondent au nom de l'outil, tandis que les hooks Notification correspondent au type de notification.

Les matchers du SDK suivent les mêmes règles que les matchers dans les fichiers de paramètres. Cette section documente les chemins d'évaluation de chaîne exacte et d'expression régulière, leurs exigences de version, et les valeurs de matcher pour chaque type d'événement.

Option Type Par défaut Description
matcher string undefined Motif mis en correspondance avec le champ de filtre de l'événement, en suivant les règles pour les matchers dans les fichiers de paramètres. Pour les hooks d'outils, ce champ est le nom de l'outil. Les outils intégrés incluent Bash, Read, Write, Edit, Glob, Grep, WebFetch, Agent et d'autres. Consultez Types d'entrée d'outil pour la liste complète. Les outils MCP utilisent le motif mcp__<server>__<action>, où <server> est la clé que vous utilisez dans la configuration mcpServers, orthographiée comme le décrit la convention de nommage des outils.
hooks HookCallback[] - Requis. Tableau de fonctions de rappel à exécuter lorsque le motif correspond
timeout number undefined Délai d'expiration en secondes. Lorsqu'il est omis, Claude Code applique le délai d'expiration par défaut de l'événement. Vos rappels du SDK suivent les valeurs par défaut du hook command
onFailure "continue" | "block" undefined Ce que fait un rappel en échec. Définissez "block" pour bloquer l'action lorsqu'un rappel échoue. En Python, utilisez on_failure

Utilisez le motif matcher pour cibler des outils spécifiques chaque fois que possible. Avec un matcher 'Bash', vos rappels s'exécutent pour les commandes Bash. Sur Claude Code v2.1.314 ou ultérieur, ils s'exécutent également sur PreToolUse et PermissionRequest pour les commandes Monitor, qu'ils peuvent refuser mais pas approuver. Sans matcher, ils s'exécutent pour chaque occurrence de l'événement.

Fonctions de rappel

Entrées

Chaque rappel hook reçoit trois arguments :

  • Données d'entrée : un objet typé contenant les détails de l'événement. Chaque type de hook a sa propre forme d'entrée. Par exemple, PreToolUseHookInput inclut tool_name et tool_input, tandis que NotificationHookInput inclut message. Consultez les définitions de type complètes dans les références du SDK TypeScript et Python.
    • Toutes les entrées de hook partagent session_id, cwd et hook_event_name.
    • agent_id et agent_type sont remplis lorsque le hook se déclenche à l'intérieur d'un sous-agent. En TypeScript, ceux-ci se trouvent sur l'entrée de hook de base et sont disponibles pour tous les types de hook. En Python, ils sont des champs optionnels sur PreToolUse, PostToolUse, PostToolUseFailure et PermissionRequest, et des champs requis sur SubagentStart et SubagentStop.
  • ID d'utilisation d'outil (str | None / string | undefined) : met en corrélation les événements PreToolUse et PostToolUse pour le même appel d'outil.
  • Contexte : en TypeScript, contient une propriété signal (AbortSignal) pour l'annulation. En Python, cet argument est réservé pour une utilisation future.

Sorties

Votre rappel retourne un objet avec deux catégories de champs :

  • Champs de niveau supérieur sont acceptés sur chaque événement : systemMessage affiche un message à l'utilisateur, et continue (continue_ en Python) détermine si l'agent continue à s'exécuter après ce hook. Certains événements les rejettent ou les livrent ailleurs. La section de chaque événement sur la page des hooks indique où ils aboutissent.
  • hookSpecificOutput contrôle l'opération actuelle. Les champs que vous définissez à l'intérieur dépendent du type d'événement hook :
    • Pour les hooks PreToolUse, c'est là que vous définissez permissionDecision ("allow", "deny", "ask" ou "defer"), permissionDecisionReason et updatedInput. Si vous retournez "defer", le tour se termine par un message de résultat dont le stop_reason est "tool_deferred", afin que vous puissiez reprendre l'appel plus tard.
    • Pour les hooks PostToolUse, vous pouvez définir additionalContext pour ajouter des informations au résultat de l'outil. Pour remplacer la sortie de l'outil avant que Claude ne la voie, définissez updatedToolOutput, qui fonctionne pour les outils intégrés et MCP dans les deux SDK. Le champ plus ancien updatedMCPToolOutput remplace uniquement la sortie des outils MCP.
    • Dans le SDK TypeScript, un rappel PostToolUse peut également retourner classifierContext, une courte note sur le résultat de l'appel d'outil pour le classificateur de permission du mode auto. Parce que votre rappel s'exécute dans le propre processus de votre application, le classificateur peut peser une déclaration d'utilisateur que vous relayez dans la note comme intention de l'utilisateur. Le champ nécessite le SDK Agent TypeScript v0.3.236 ou ultérieur. Annoter un résultat pour le classificateur du mode auto couvre le plafond de longueur, la règle synchrone uniquement, et ce qu'il ne faut pas mettre dans la note.

Retournez {} pour autoriser l'opération sans modifications. Les hooks de rappel du SDK utilisent le même format de sortie JSON que les hooks de commande shell Claude Code, qui documente chaque champ et option spécifique à l'événement. Pour les définitions de type du SDK, consultez les références du SDK TypeScript et Python.

[!NOTE]

Lorsque plusieurs hooks PreToolUse retournent des décisions différentes, deny a priorité sur defer, qui a priorité sur ask, qui a priorité sur allow.

Sortie asynchrone

Par défaut, l'agent attend que votre hook retourne avant de continuer. Si votre hook effectue un effet secondaire, tel que la journalisation ou l'envoi d'un webhook, et n'a pas besoin d'influencer le comportement de l'agent, vous pouvez retourner une sortie asynchrone à la place. Cela indique à l'agent de continuer immédiatement sans attendre la fin du hook. Dans cet extrait, send_to_logging_service en Python et sendToLoggingService en TypeScript représentent n'importe quelle fonction de journalisation que vous définissez :

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 };
};
Champ Type Description
async true Signale le mode asynchrone. L'agent continue sans attendre. En Python, utilisez async_ pour éviter le mot-clé réservé.
asyncTimeout number Délai d'expiration optionnel en millisecondes pour l'opération en arrière-plan

[!NOTE]

Les sorties asynchrones ne peuvent pas bloquer, modifier ou injecter du contexte dans l'opération puisque l'agent a déjà avancé. Utilisez-les uniquement pour les effets secondaires comme la journalisation, les métriques ou les notifications.

Bloquer l'action lorsqu'un rappel échoue

Sur la plupart des événements, si un rappel lève une exception ou retourne une sortie qui échoue à la validation, l'action se poursuit et votre application reçoit un avertissement SDKInformationalMessage dont le tag est hook_callback_failed. Un garde-fou PreToolUse qui plante laisse alors l'appel d'outil atteindre le flux de permission normal. Un rappel qui ne retourne rien, comme un chemin sans return, compte également comme un échec. En Python, lisez tag depuis SystemMessage.data.

Pour bloquer l'action à la place, définissez onFailure: "block" en TypeScript ou on_failure="block" en Python sur le matcher. Nécessite le SDK Agent TypeScript v0.3.317 ou ultérieur, ou le SDK Agent Python v0.2.177 ou ultérieur. Cet exemple bloque un appel Bash lorsque check_policy en Python ou checkPolicy en TypeScript échoue :

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" }]
  }
};

Pour le tester, faites lever une erreur au rappel et demandez à Claude d'exécuter une commande Bash. Le résultat de l'outil inclut failed; blocking because onFailure is "block", et Claude ne voit pas le texte de votre erreur.

Retournez {} lorsque le rappel n'a pas d'avis. Avec "block" défini, un chemin sans return bloque l'action. Un rappel qui intercepte sa propre erreur et retourne {} n'est pas en échec : laissez donc l'erreur se propager depuis un garde-fou.

Événements au comportement différent

Ces événements ne bloquent pas toujours lorsque "block" est défini :

  • Stop, SubagentStop, TaskCompleted et TeammateIdle : ces événements ignorent l'option, donc l'action se poursuit.
  • PermissionRequest : le blocage n'est pris en compte que s'il arrive avant que votre rappel canUseTool ne réponde.
  • PostToolUse : l'outil s'est déjà exécuté, donc Claude reçoit l'échec comme retour.

Un rappel qui dépasse son délai d'expiration suit Délai d'expiration du hook. Avec "block" défini, un délai d'expiration bloque également PreCompact, Elicitation et WorktreeCreate.

Erreurs de démarrage pour onFailure

Avec "block" défini, la requête échoue au démarrage et n'envoie aucun prompt lorsqu'elle ne peut pas respecter l'option :

  • Claude Code ne prend pas en charge l'option : l'erreur inclut does not honor. Mettez à jour Claude Code ou supprimez l'option.
  • La session était déjà en cours d'exécution et n'a pas enregistré vos hooks : l'erreur inclut did not register your hooks. Démarrez une nouvelle session.

Exemples

Plusieurs exemples dans cette section montrent uniquement la fonction de rappel. Pour en exécuter un, enregistrez le rappel sous l'événement correspondant dans le champ hooks de vos options, comme indiqué dans Configurer les hooks.

Modifier l'entrée de l'outil

Cet exemple intercepte les appels d'outil Write et réécrit l'argument file_path pour y ajouter le préfixe /sandbox. Le rappel retourne updatedInput avec le chemin modifié et permissionDecision: 'allow' pour approuver automatiquement l'opération réécrite :

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]

Associez updatedInput à permissionDecision: 'allow' pour approuver automatiquement l'entrée modifiée, ou à permissionDecision: 'ask' pour la montrer à l'utilisateur. Si vous omettez permissionDecision, l'entrée modifiée s'applique toujours et passe par l'évaluation de permission normale. Avec 'defer', updatedInput est ignoré. Retournez toujours un nouvel objet plutôt que de muter le tool_input original.

Pour confirmer la redirection, définissez le préfixe sur un chemin dans lequel vous pouvez écrire, tel que ./sandbox ou /tmp/sandbox (macOS ne permet pas de créer un répertoire /sandbox au niveau racine), puis demandez à l'agent d'écrire un fichier : le résultat de l'outil Write dans le flux de messages nomme le chemin avec votre préfixe sandbox plutôt que celui que Claude a demandé.

Ajouter du contexte et bloquer un outil

Cet exemple refuse les appels d'outil dont le file_path commence par /etc et explique pourquoi au modèle et à l'utilisateur :

  • permissionDecision: 'deny' arrête l'appel d'outil.
  • permissionDecisionReason indique au modèle pourquoi.
  • systemMessage montre à l'utilisateur ce qui s'est passé.
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 {};
};

Pour confirmer le blocage, enregistrez le rappel sous PreToolUse avec un matcher Write|Edit et demandez à l'agent de créer un fichier sous /etc : le résultat de l'outil Write dans le flux de messages contient Writing to /etc is not allowed, et aucun fichier n'est créé.

Approuver automatiquement des outils spécifiques

Par défaut, l'agent peut demander une permission avant d'utiliser certains outils. Cet exemple approuve automatiquement les outils de système de fichiers en lecture seule (Read, Glob, Grep) en retournant permissionDecision: 'allow', les laissant s'exécuter sans confirmation de l'utilisateur, à l'exception des lectures depuis des chemins réseau, tout en laissant tous les autres outils soumis aux vérifications de permission normales :

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 {};
};

Enregistrer plusieurs hooks

Quand un événement se déclenche, les hooks correspondants s'exécutent en parallèle. Pour les décisions de permission, le résultat le plus restrictif s'applique. Le deny d'un seul hook bloque l'appel d'outil. Parce que l'ordre de complétion est non-déterministe, écrivez chaque hook pour agir indépendamment plutôt que de compter sur l'exécution préalable d'un autre hook.

L'exemple ci-dessous enregistre trois vérifications indépendantes, chacune avec un matcher omis. Les noms de hook qu'il contient, tels que audit_logger en Python ou auditLogger en TypeScript, représentent des rappels que vous définissez :

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] }
    ]
  }
};

Filtrer avec des matchers multi-outils

Utilisez des matchers multi-outils pour partager un rappel entre outils connexes. Cet exemple enregistre trois matchers avec des portées différentes, et chaque hook qu'il nomme représente un rappel que vous définissez :

  • Une liste exacte séparée par des barres (Write|Edit|NotebookEdit) déclenche file_security_hook pour les outils Write, Edit et NotebookEdit.
  • Une regex (^mcp__) déclenche mcp_audit_hook pour tout outil MCP dont le nom commence par mcp__.
  • Un matcher omis déclenche global_logger sans filtrer par nom d'outil.
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] }
    ]
  }
};

Suivre l'activité des sous-agents

Utilisez les hooks SubagentStop pour surveiller quand les sous-agents terminent leur travail. Consultez le type d'entrée complet dans les références du SDK TypeScript et Python. Cet exemple enregistre un résumé chaque fois qu'un sous-agent se termine :

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] }]
  }
};

Pour confirmer que le hook se déclenche, enregistrez le rappel et demandez à l'agent de déléguer une petite tâche à un sous-agent, comme lister les fichiers du répertoire courant : quand le sous-agent se termine, le rappel affiche les lignes [SUBAGENT] Completed: avec l'ID du sous-agent et le chemin de sa transcription.

Effectuer des requêtes HTTP à partir des hooks

Les hooks peuvent effectuer des opérations asynchrones comme les requêtes HTTP. Capturez les erreurs à l'intérieur de votre hook au lieu de les laisser se propager.

Cet exemple envoie un webhook après chaque exécution d'outil, enregistrant quel outil a été exécuté et quand. Le hook capture les erreurs afin qu'un webhook échoué n'interrompe pas l'agent :

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);
}

Pour confirmer que le hook se déclenche, pointez l'URL du webhook vers un endpoint que vous pouvez surveiller et envoyez une invite qui utilise un outil : le hook envoie un POST avec le nom de l'outil et l'horodatage après chaque exécution d'outil.

Transférer les notifications à Slack

Utilisez les hooks Notification pour recevoir les notifications système de l'agent et les transférer vers des services externes. Dans les sessions SDK, Claude Code exécute ce hook pour les types de notification suivants :

  • permission_prompt une fois qu'une demande de permission a attendu environ six secondes sur votre rappel canUseTool. Nécessite TypeScript Agent SDK v0.3.233 ou ultérieur, ou Python Agent SDK v0.2.139 ou ultérieur
  • elicitation_complete et elicitation_response pour les flux d'élicitation de saisie utilisateur
  • plugin_notification lorsqu'un mod appelle $.ui.notify

Claude Code émet les autres types, tels que idle_prompt, auth_success, et elicitation_dialog, à partir de l'interface utilisateur interactive que les sessions SDK n'exécutent pas.

Chaque notification inclut un champ message avec une description lisible par l'homme et optionnellement un title.

Cet exemple transfère chaque notification à un canal Slack. Il nécessite une URL de webhook entrant Slack, que vous créez en ajoutant une application à votre espace de travail Slack et en activant les webhooks entrants :

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);
}

Quand un événement Notification se déclenche, le hook publie le message de la notification, préfixé par Agent status:, sur le canal que votre webhook cible.

Corriger les problèmes courants

Hook ne se déclenche pas

  • Vérifiez que le nom de l'événement hook est correct et sensible à la casse (PreToolUse, pas preToolUse)
  • Vérifiez que votre motif matcher correspond exactement au nom de l'outil
  • Assurez-vous que le hook se trouve sous le type d'événement correct dans options.hooks
  • Pour les hooks non basés sur les outils qui prennent en charge les matchers, comme Notification et SubagentStop, les matchers correspondent à des champs différents, et Stop ignore complètement les matchers (consultez motifs matcher)
  • Les hooks peuvent ne pas se déclencher lorsque l'agent atteint la limite max_turns car la session se termine avant que les hooks puissent s'exécuter

Matcher ne filtre pas comme prévu

Les matchers ne correspondent qu'aux noms d'outils, pas aux chemins de fichiers ou à d'autres arguments. Pour filtrer par chemin de fichier, vérifiez tool_input.file_path à l'intérieur de votre 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 {};
};

Délai d'expiration du hook

Claude Code exécute chaque rappel avec un délai d'expiration, que vous définissez en secondes avec le champ timeout sur son HookMatcher. Lorsque vous n'en définissez pas, Claude Code utilise la valeur par défaut de l'événement : 600 secondes pour la plupart des événements, 30 secondes pour UserPromptSubmit, PreModelSwitch et PostModelSwitch, et 10 secondes pour MessageDisplay. Claude Code exécute les rappels SessionEnd lors de l'arrêt sous le budget de délai d'expiration SessionEnd plus court, 1,5 secondes par défaut.

Lorsqu'un rappel dépasse son délai d'expiration, Claude Code l'annule et rejette sa sortie, et la session continue plutôt que de se bloquer. Ce qui se passe ensuite dépend de l'événement :

  • PreToolUse : Claude Code bloque l'appel d'outil. Si tous les autres hooks n'ont rien retourné ou seulement une décision allow, le résultat de l'outil indique à Claude que le hook n'a pas répondu avant son délai d'expiration, et le tour continue. Si un autre de vos hooks a déjà refusé l'appel, son refus parvient toujours à Claude : le résultat de l'outil inclut la raison de ce hook, et le texte du délai d'expiration peut la suivre.

    Avant la v2.1.210, Claude Code signalait le délai d'expiration à Claude comme un refus utilisateur, ce qui faisait arrêter les sessions sans surveillance et attendre une entrée.

  • PostToolUse et PostToolUseFailure : Claude Code conserve le résultat de l'outil et le tour continue.

  • UserPromptSubmit et UserPromptExpansion : Claude Code bloque le prompt avec un message nommant le hook et le délai d'expiration, et la session continue. Avant la v2.1.208, Claude Code terminait la requête avec error_during_execution lorsqu'un rappel sur ces événements expirait.

  • Stop et SubagentStop : le rappel expiré compte comme ne retournant aucune décision. L'agent ou le sous-agent s'arrête comme si ce rappel l'avait autorisé, et une décision de vos autres hooks sur l'événement s'applique toujours. Avant Claude Code v2.1.273, un rappel Stop ou SubagentStop expiré comptait comme une exécution de hook échouée, et Claude Code rejetait les décisions de vos autres hooks sur l'événement.

  • SessionStart : le rappel expiré compte comme ne retournant aucune sortie, et la session continue avec la sortie de vos autres hooks SessionStart.

  • PreModelSwitch : Claude Code bloque le changement de modèle. Un hook qui ne répond pas n'a pas approuvé le changement.

  • Autres événements, tels que Notification, PreCompact et PostModelSwitch : Claude Code consigne l'échec dans les logs et continue. Avec onFailure: "block", un délai d'expiration sur PreCompact, Elicitation ou WorktreeCreate bloque l'action.

La première fois qu'un rappel Stop ou SessionStart expire dans la session principale, Claude Code ajoute également un SDKInformationalMessage au flux de messages indiquant que l'application pilotant la session n'a pas répondu. Les expirations ultérieures ne répètent pas ce message tant que votre application reste sans réponse.

Si vous interrompez la requête pendant qu'un rappel est en attente, Claude Code annule l'appel d'outil en attente. Avant la v2.1.208, l'appel d'outil pouvait toujours se poursuivre si vous interrompiez pendant un rappel PreToolUse en attente.

Si votre rappel a besoin de plus de temps, définissez un timeout plus élevé sur son HookMatcher. En TypeScript, utilisez le AbortSignal du troisième argument de rappel pour gérer l'annulation correctement lorsque le délai d'expiration se déclenche.

Outil bloqué de manière inattendue

  • Vérifiez tous les hooks PreToolUse pour les retours permissionDecision: 'deny'
  • Ajoutez la journalisation à vos hooks pour voir quel permissionDecisionReason ils retournent
  • Vérifiez que les motifs matcher ne sont pas trop larges : un matcher vide correspond à tous les outils

Entrée modifiée non appliquée

  • Assurez-vous que updatedInput se trouve à l'intérieur de hookSpecificOutput, pas au niveau supérieur :

    return {
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "allow",
        updatedInput: { command: "new command" }
      }
    };
    
  • N'associez pas updatedInput avec permissionDecision: 'defer', qui supprime l'entrée modifiée. Omettre permissionDecision est correct : l'entrée modifiée s'applique toujours par l'évaluation des permissions normale. Vous pouvez également retourner 'allow' pour approuver automatiquement l'entrée modifiée ou 'ask' pour la montrer à l'utilisateur pour approbation

  • Incluez hookEventName dans hookSpecificOutput pour identifier le type de hook pour lequel la sortie est destinée

Hooks de session non disponibles en Python

SessionStart et SessionEnd peuvent être enregistrés en tant que hooks de rappel du SDK en TypeScript, mais ne sont pas disponibles dans le SDK Python car son type HookEvent les omet. En Python, ils ne sont disponibles que comme hooks de commande shell définis dans les fichiers de paramètres tels que .claude/settings.json. Les fichiers de paramètres que charge votre application SDK dépendent de setting_sources ou de settingSources. Si vous définissez cette option, incluez la source qui contient les hooks :

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

Pour exécuter la logique d'initialisation en tant que rappel du SDK Python à la place, utilisez le premier message de client.receive_response() comme déclencheur.

Les demandes de permission des sous-agents se multiplient

Lors du lancement de plusieurs sous-agents, chacun peut demander des permissions séparément pour ses propres appels d'outils. Pour éviter les demandes répétées, utilisez les hooks PreToolUse pour approuver automatiquement des outils spécifiques, ou configurez des règles de permission, que les sous-agents héritent de la conversation parent.

Boucles de hook récursives avec des sous-agents

Un hook UserPromptSubmit qui lance des sous-agents peut créer des boucles infinies si ces sous-agents déclenchent le même hook. Pour éviter cela :

  • Utilisez une variable partagée ou un état de session pour suivre si vous êtes déjà à l'intérieur d'un sous-agent
  • Limitez les hooks pour qu'ils s'exécutent uniquement pour la session d'agent de niveau supérieur

systemMessage n'apparaît pas dans la sortie

Le champ systemMessage affiche un message à l'utilisateur, pas au modèle. Sur Claude Code v2.1.227 ou ultérieur, le systemMessage d'un hook peut apparaître dans le flux de messages en tant que SDKInformationalMessage. Que ce soit le cas dépend de l'événement. La section de chaque événement sur la page des hooks indique comment la sortie apparaît. Pour transmettre du contexte au modèle à la place, retournez additionalContext.

Avant la v2.1.227, le SDK ne faisait apparaître la sortie du hook dans le flux de messages que pour les hooks SessionStart et Setup. Pour tout autre événement, la sortie n'apparaissait que dans les événements de cycle de vie que includeHookEvents (include_hook_events en Python) ajoute. L'entrée de cette option couvre les événements de cycle de vie que chaque événement hook produit.

Si vous avez besoin de faire apparaître les décisions de hook à votre application de manière fiable, journalisez-les séparément ou utilisez un canal de sortie dédié.

Ressources connexes