SpyBara
Go Premium

agent-sdk/hooks.md 2026-10-31 23:02 UTC to 2026-11-01 23:57 UTC

No changes for this document in this history range.

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

Interceptar e controlar o comportamento do agente com hooks

Interceptar e personalizar o comportamento do agente em pontos-chave de execução com hooks

Hooks são funções de callback que executam seu código em resposta a eventos do agente, como uma ferramenta sendo chamada, uma sessão iniciando ou a execução parando. Com hooks, você pode:

  • Bloquear operações perigosas antes de serem executadas, como comandos shell destrutivos ou acesso a arquivos não autorizado
  • Registrar e auditar chamadas de ferramenta para conformidade, depuração ou análise
  • Transformar entradas e saídas para sanitizar dados, injetar credenciais ou redirecionar caminhos de arquivo
  • Exigir aprovação humana para ações sensíveis como gravações em banco de dados ou chamadas de API
  • Rastrear ciclo de vida da sessão para gerenciar estado, limpar recursos ou enviar notificações

Como hooks funcionam

1. Um evento é disparado

Algo acontece durante a execução do agente e o SDK dispara um evento: uma ferramenta está prestes a ser chamada (PreToolUse), uma ferramenta retornou um resultado (PostToolUse), um subagente iniciou ou parou, o agente está ocioso ou a execução terminou. Veja a lista completa de eventos.

2. O SDK coleta hooks registrados

O SDK verifica se há hooks registrados para esse tipo de evento. Isso inclui hooks de callback que você passa em options.hooks e hooks de comando shell de arquivos de configuração quando a entrada settingSources ou setting_sources correspondente está habilitada, o que é o padrão para opções query().

3. Matchers filtram quais hooks são executados

Se um hook tem um padrão matcher (como "Write|Edit"), o SDK o testa contra o alvo do evento (por exemplo, o nome da ferramenta). Hooks sem um matcher são executados para cada evento desse tipo.

4. Funções de callback são executadas

Cada função de callback do hook correspondente recebe informações sobre o que está acontecendo: o nome da ferramenta, seus argumentos, o ID da sessão e outros detalhes específicos do evento.

5. Seu callback retorna uma decisão

Após realizar qualquer operação (registro, chamadas de API, validação), seu callback retorna um objeto de saída que diz ao agente o que fazer: permitir a operação, bloqueá-la, modificar a entrada ou injetar contexto na conversa.

Este exemplo registra um hook PreToolUse (etapa 1) com um matcher "Write|Edit" (etapa 3) para que o callback seja acionado para as ferramentas Write e Edit. Quando acionado, o callback recebe a entrada da ferramenta (etapa 4), verifica se o caminho do arquivo tem como alvo um arquivo .env e retorna permissionDecision: "deny" para bloquear a operação (etapa 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);
  }
}

O hook nega uma chamada Write ou Edit cujo file_path termina em /.env, com qualquer combinação de maiúsculas e minúsculas. No Windows, file_path usa barras invertidas, como C:\project\.env, e este hook permite uma chamada Write ou Edit para C:\project\.env.

[!WARNING]

O matcher Write|Edit limita este hook às ferramentas Write e Edit. Se Claude escrever .env com um comando Bash, o hook não é executado. Para bloquear escritas via Bash em um arquivo onde o sandbox é executado, consulte a configuração do sandbox para Python ou TypeScript.

Hooks disponíveis

O SDK fornece hooks para diferentes estágios de execução do agente. Alguns hooks estão disponíveis em ambos os SDKs, enquanto outros são apenas para TypeScript.

Evento de Hook SDK Python SDK TypeScript O que o dispara Caso de uso de exemplo
PreToolUse Sim Sim Solicitação de chamada de ferramenta (pode bloquear ou modificar) Bloquear comandos shell perigosos
PostToolUse Sim Sim Resultado de execução de ferramenta Registrar alterações de arquivo em uma trilha de auditoria
PostToolUseFailure Sim Sim Falha na execução de ferramenta Lidar ou registrar erros de ferramenta
PostToolBatch Não Sim Um lote completo de chamadas de ferramenta é resolvido, uma vez por lote antes da próxima chamada de modelo Injetar convenções uma vez para todo o lote
UserPromptSubmit Sim Sim Um prompt é enviado, incluindo um turno que o Claude Code inicia por conta própria Injetar contexto adicional em prompts
UserPromptExpansion Não Sim Um comando digitado pelo usuário, ou um prompt MCP, se expande em um prompt antes de chegar ao Claude. Não dispara quando Claude invoca uma skill por si mesmo Bloquear um comando de invocação direta ou adicionar contexto quando uma skill é digitada
MessageDisplay Não Sim Uma mensagem do assistente com texto é concluída, uma vez por mensagem com o texto completo da mensagem Redigir ou reformatar o texto exibido sem alterar a transcrição
Stop Sim Sim Parada de execução do agente Salvar estado da sessão antes de sair
StopFailure Não Sim O turno termina com um erro de API em vez de uma parada normal Registrar falhas ou enviar alertas
SubagentStart Sim Sim Inicialização de subagente Rastrear geração de tarefas paralelas
SubagentStop Sim Sim Conclusão de subagente Agregar resultados de tarefas paralelas
PreCompact Sim Sim Solicitação de compactação de conversa Arquivar transcrição completa antes de resumir
PostCompact Não Sim Compactação de conversa é concluída Registrar o resumo gerado
PreModelSwitch Não Sim Uma mudança de modelo solicitada, antes de acontecer (pode bloquear) Bloquear a mudança para um modelo específico
PostModelSwitch Não Sim O modelo da sessão muda, incluindo um fallback automático Dar ao Claude orientação específica do modelo para o novo modelo
PermissionRequest Sim Sim Uma chamada de ferramenta precisa de uma decisão de permissão Manipulação de permissão personalizada
PermissionDenied Não Sim O modo automático nega uma chamada de ferramenta, incluindo negações sem um veredicto do classificador Registrar negações, ou informar ao modelo que ele pode tentar novamente; Claude Code ignora retry: true para negações sem veredicto. Veja PermissionDenied
SessionStart Não Sim Inicialização de sessão Inicializar registro e telemetria
SessionEnd Não Sim Encerramento de sessão Limpar recursos temporários
Notification Sim Sim Mensagens de status do agente Enviar atualizações de status do agente para Slack ou PagerDuty
Setup Não Sim Configuração/manutenção de sessão Executar tarefas de inicialização
TeammateIdle Não Sim Colega fica ocioso Reatribuir trabalho ou notificar
TaskCreated Não Sim Uma tarefa é criada via a ferramenta TaskCreate Aplicar convenções de nomenclatura de tarefas
TaskCompleted Não Sim Uma tarefa é marcada como concluída Exigir testes aprovados antes de uma tarefa ser fechada
Elicitation Não Sim Um servidor MCP solicita entrada do usuário no meio da tarefa Responder a solicitações de entrada MCP programaticamente
ElicitationResult Não Sim Um usuário responde a uma elicitação MCP Modificar ou bloquear a resposta antes de ela retornar ao servidor
ConfigChange Não Sim Arquivo de configuração muda Recarregar configurações dinamicamente
InstructionsLoaded Não Sim Um arquivo CLAUDE.md ou arquivo de regras é carregado no contexto Auditar quais arquivos de instrução são carregados
WorktreeCreate Não Sim Git worktree criado Rastrear espaços de trabalho isolados
WorktreeRemove Não Sim Um worktree criado por um hook WorktreeCreate está sendo removido Limpar recursos de espaço de trabalho
CwdChanged Não Sim O diretório de trabalho muda durante uma sessão Recarregar variáveis de ambiente por diretório
FileChanged Não Sim Um arquivo monitorado é modificado, criado ou deletado Recarregar configuração quando arquivos do projeto mudam
DirectoryAdded Não Sim Um diretório de trabalho é adicionado durante uma sessão Instalar dependências para um repositório adicionado no meio da sessão

Configurar hooks

Para configurar um hook, passe-o no campo hooks de suas opções de agente (ClaudeAgentOptions em Python, o objeto options em TypeScript). Este trecho assume que você já definiu um callback de hook, como protect_env_files em Python ou protectEnvFiles em TypeScript do exemplo acima:

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

A opção hooks é um dicionário em Python ou um objeto em TypeScript, onde:

Matchers

Use matchers para filtrar quando seus callbacks são acionados. O campo matcher corresponde a um valor diferente dependendo do tipo de evento de hook. Por exemplo, hooks baseados em ferramentas correspondem ao nome da ferramenta, enquanto hooks Notification correspondem ao tipo de notificação.

Os matchers do SDK seguem as mesmas regras que matchers em arquivos de configuração. Essa seção documenta os caminhos de avaliação de string exata e expressão regular, seus requisitos de versão e os valores de matcher para cada tipo de evento.

Opção Tipo Padrão Descrição
matcher string undefined Padrão correspondido contra o campo de filtro do evento, seguindo as regras para matchers em arquivos de configuração. Para hooks de ferramenta, esse campo é o nome da ferramenta. As ferramentas integradas incluem Bash, Read, Write, Edit, Glob, Grep, WebFetch, Agent e outras. Veja Tipos de Entrada de Ferramenta para a lista completa. Ferramentas MCP usam o padrão mcp__<server>__<action>, onde <server> é a chave que você usa na configuração mcpServers, escrita conforme descrito na convenção de nomenclatura de ferramentas.
hooks HookCallback[] - Obrigatório. Array de funções de callback a executar quando o padrão corresponde
timeout number undefined Timeout em segundos. Quando omitido, Claude Code aplica o timeout padrão do evento. Seus callbacks do SDK seguem os padrões de hook command

Use o padrão matcher para direcionar ferramentas específicas sempre que possível. Um matcher com 'Bash' é executado apenas para comandos Bash, enquanto omitir o padrão executa seus callbacks para cada ocorrência do evento.

Funções de callback

Entradas

Cada callback de hook recebe três argumentos:

  • Dados de entrada: um objeto tipado contendo detalhes do evento. Cada tipo de hook tem sua própria forma de entrada. Por exemplo, PreToolUseHookInput inclui tool_name e tool_input, enquanto NotificationHookInput inclui message. Veja as definições de tipo completas nas referências do SDK TypeScript e Python.
    • Todas as entradas de hook compartilham session_id, cwd e hook_event_name.
    • agent_id e agent_type são preenchidos quando o hook é acionado dentro de um subagente. Em TypeScript, estes estão na entrada de hook base e disponíveis para todos os tipos de hook. Em Python, eles são campos opcionais em PreToolUse, PostToolUse, PostToolUseFailure e PermissionRequest, e campos obrigatórios em SubagentStart e SubagentStop.
  • ID de uso de ferramenta (str | None / string | undefined): correlaciona eventos PreToolUse e PostToolUse para a mesma chamada de ferramenta.
  • Contexto: em TypeScript, contém uma propriedade signal (AbortSignal) para cancelamento. Em Python, este argumento é reservado para uso futuro.

Saídas

Seu callback retorna um objeto com duas categorias de campos:

  • Campos de nível superior são aceitos em cada evento: systemMessage mostra uma mensagem ao usuário, e continue (continue_ em Python) determina se o agente continua executando após este hook. Alguns eventos descartam-nos ou os entregam em outro lugar. A seção de cada evento na página de hooks diz onde eles chegam.
  • hookSpecificOutput controla a operação atual. Os campos que você define dentro dependem do tipo de evento de hook:
    • Para hooks PreToolUse, é aqui que você define permissionDecision ("allow", "deny", "ask" ou "defer"), permissionDecisionReason e updatedInput. Se você retornar "defer", o turno termina com uma mensagem de resultado cujo stop_reason é "tool_deferred", para que você possa retomar a chamada depois.
    • Para hooks PostToolUse, você pode definir additionalContext para anexar informações ao resultado da ferramenta. Para substituir a saída da ferramenta antes de Claude vê-la, defina updatedToolOutput, que funciona para qualquer ferramenta em ambos os SDKs. O campo mais antigo updatedMCPToolOutput substitui apenas a saída de ferramentas MCP.
    • No SDK TypeScript, um callback PostToolUse também pode retornar classifierContext, uma nota breve sobre o resultado da chamada de ferramenta para o classificador de permissão do modo auto. Como seu callback é executado no próprio processo de sua aplicação, o classificador pode pesar uma declaração de usuário que você transmite na nota como intenção do usuário. O campo requer Agent SDK TypeScript v0.3.236 ou posterior. Anotar um resultado para o classificador do modo auto cobre o limite de comprimento, a regra somente síncrona e o que não colocar na nota.

Retorne {} para permitir a operação sem alterações. Hooks de callback do SDK usam o mesmo formato de saída JSON que hooks de comando shell do Claude Code, que documenta cada campo e opção específica do evento. Para as definições de tipo do SDK, veja as referências do SDK TypeScript e Python.

[!NOTE]

Quando múltiplos hooks PreToolUse retornam decisões diferentes, deny tem prioridade sobre defer, que tem prioridade sobre ask, que tem prioridade sobre allow.

Saída assíncrona

Por padrão, o agente aguarda seu hook retornar antes de prosseguir. Se seu hook realiza um efeito colateral, como registro ou envio de webhook, e não precisa influenciar o comportamento do agente, você pode retornar uma saída assíncrona. Isso diz ao agente para continuar imediatamente sem aguardar o hook terminar. Neste trecho, send_to_logging_service em Python e sendToLoggingService em TypeScript representam qualquer função de registro que você defina:

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 Descrição
async true Sinaliza modo assíncrono. O agente prossegue sem aguardar. Em Python, use async_ para evitar a palavra-chave reservada.
asyncTimeout number Timeout opcional em milissegundos para a operação em segundo plano

[!NOTE]

Saídas assíncronas não podem bloquear, modificar ou injetar contexto na operação, pois o agente já avançou. Use-as apenas para efeitos colaterais como registro, métricas ou notificações.

Exemplos

Vários exemplos nesta seção mostram apenas a função de callback. Para executar um, registre o callback sob o evento correspondente no campo hooks de suas opções, conforme mostrado em Configurar hooks.

Modificar entrada de ferramenta

Este exemplo intercepta chamadas de ferramenta Write e reescreve o argumento file_path para prepender /sandbox. O callback retorna updatedInput com o caminho modificado e permissionDecision: 'allow' para aprovar automaticamente a operação reescrita:

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]

Combine updatedInput com permissionDecision: 'allow' para aprovar automaticamente a entrada modificada, ou permissionDecision: 'ask' para mostrá-la ao usuário. Se você omitir permissionDecision, a entrada modificada ainda se aplica e flui pela avaliação de permissão normal. Com 'defer', updatedInput é ignorado. Sempre retorne um novo objeto em vez de mutar o tool_input original.

Para confirmar o redirecionamento, defina o prefixo para um caminho em que você possa escrever, como ./sandbox ou /tmp/sandbox (macOS não permite criar um diretório /sandbox no nível raiz), depois peça ao agente para escrever um arquivo: o resultado da ferramenta Write no fluxo de mensagens nomeia o caminho com seu prefixo de sandbox em vez do que Claude solicitou.

Adicionar contexto e bloquear uma ferramenta

Este exemplo nega chamadas de ferramenta cujo file_path começa com /etc e explica o motivo tanto para o modelo quanto para o usuário:

  • permissionDecision: 'deny' interrompe a chamada de ferramenta.
  • permissionDecisionReason informa ao modelo o motivo.
  • systemMessage mostra ao usuário o que aconteceu.
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 {};
};

Para confirmar o bloqueio, registre o callback sob PreToolUse com um matcher Write|Edit e peça ao agente para criar um arquivo em /etc: o resultado da ferramenta Write no fluxo de mensagens contém Writing to /etc is not allowed, e nenhum arquivo é criado.

Aprovar automaticamente ferramentas específicas

Por padrão, o agente pode solicitar permissão antes de usar certas ferramentas. Este exemplo aprova automaticamente ferramentas de sistema de arquivos somente leitura (Read, Glob, Grep) retornando permissionDecision: 'allow', permitindo que sejam executadas sem confirmação do usuário, exceto leituras de caminhos de rede, enquanto deixa todas as outras ferramentas sujeitas a verificações de permissão normais:

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

Registrar múltiplos hooks

Quando um evento é disparado, os hooks correspondentes são executados em paralelo. Para decisões de permissão, o resultado mais restritivo se aplica. Um deny de um hook bloqueia a chamada de ferramenta. Como a ordem de conclusão é não-determinística, escreva cada hook para agir independentemente em vez de depender de outro hook ter sido executado primeiro.

O exemplo abaixo registra três verificações independentes, cada uma com um matcher omitido. Os nomes de hook nele, como audit_logger em Python ou auditLogger em TypeScript, representam callbacks que você define:

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

Filtrar com matchers de múltiplas ferramentas

Use matchers de múltiplas ferramentas para compartilhar um callback entre ferramentas relacionadas. Este exemplo registra três matchers com escopos diferentes, e cada hook que ele nomeia representa um callback que você define:

  • Uma lista exata separada por pipe (Write|Edit|NotebookEdit) dispara file_security_hook para as ferramentas Write, Edit e NotebookEdit.
  • Uma regex (^mcp__) dispara mcp_audit_hook para qualquer ferramenta MCP cujo nome começa com mcp__.
  • Um matcher omitido dispara global_logger sem filtrar pelo nome da ferramenta.
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] }
    ]
  }
};

Rastrear atividade de subagente

Use hooks SubagentStop para monitorar quando subagentes terminam seu trabalho. Veja o tipo de entrada completo nas referências do SDK TypeScript e Python. Este exemplo registra um resumo cada vez que um subagente é concluído:

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

Para confirmar que o hook é disparado, registre o callback e peça ao agente para delegar uma pequena tarefa a um subagente, como listar os arquivos no diretório atual: quando o subagente termina, o callback imprime as linhas [SUBAGENT] Completed: com o ID do subagente e o caminho da transcrição.

Fazer requisições HTTP a partir de hooks

Hooks podem realizar operações assíncronas como requisições HTTP. Capture erros dentro de seu hook em vez de deixá-los se propagar.

Este exemplo envia um webhook após cada ferramenta ser concluída, registrando qual ferramenta foi executada e quando. O hook captura erros de um webhook falhado:

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

Para confirmar que o hook é disparado, aponte a URL do webhook para um endpoint que você possa monitorar e envie um prompt que use uma ferramenta: o hook envia um POST com o nome da ferramenta e o timestamp após cada ferramenta ser concluída.

Encaminhar notificações para Slack

Use hooks Notification para receber notificações do sistema do agente e encaminhá-las para serviços externos. Em sessões do SDK, Claude Code executa este hook para os seguintes tipos de notificação:

  • permission_prompt uma vez que uma solicitação de permissão aguardou cerca de seis segundos em seu callback canUseTool. Requer Agent SDK TypeScript v0.3.233 ou posterior, ou Agent SDK Python v0.2.139 ou posterior
  • elicitation_complete e elicitation_response para fluxos de elicitação de entrada do usuário
  • plugin_notification quando um mod chama $.ui.notify

Claude Code emite os outros tipos, como idle_prompt, auth_success e elicitation_dialog, a partir da interface interativa que sessões do SDK não executam.

Cada notificação inclui um campo message com uma descrição legível por humanos e opcionalmente um title.

Este exemplo encaminha cada notificação para um canal Slack. Requer uma URL de webhook de entrada do Slack, que você cria adicionando um app ao seu espaço de trabalho Slack e habilitando webhooks de entrada:

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 um evento Notification é disparado, o hook publica a message da notificação, prefixada com Agent status:, para o canal que seu webhook aponta.

Corrigir problemas comuns

Hook não é disparado

  • Verifique se o nome do evento de hook está correto e sensível a maiúsculas/minúsculas (PreToolUse, não preToolUse)
  • Verifique se seu padrão de matcher corresponde exatamente ao nome da ferramenta
  • Certifique-se de que o hook está sob o tipo de evento correto em options.hooks
  • Para hooks não baseados em ferramentas que suportam matchers, como Notification e SubagentStop, matchers correspondem a campos diferentes, e Stop ignora matchers completamente (veja padrões de matcher)
  • Hooks podem não ser disparados quando o agente atinge o limite max_turns porque a sessão termina antes que hooks possam ser executados

Matcher não filtra como esperado

Matchers apenas correspondem a nomes de ferramentas, não a caminhos de arquivo ou outros argumentos. Para filtrar por caminho de arquivo, verifique tool_input.file_path dentro de seu 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 de hook

Claude Code executa cada callback com um timeout, que você define em segundos com o campo timeout em seu HookMatcher. Quando você não define um, Claude Code usa o padrão do evento: 600 segundos para a maioria dos eventos, 30 segundos para UserPromptSubmit, PreModelSwitch e PostModelSwitch, e 10 segundos para MessageDisplay. Claude Code executa callbacks SessionEnd durante o desligamento sob o orçamento de timeout mais curto de SessionEnd, 1,5 segundos por padrão.

Quando um callback excede seu timeout, Claude Code o cancela e descarta sua saída, e a sessão continua em vez de travar. O que acontece a seguir depende do evento:

  • PreToolUse: Claude Code bloqueia a chamada de ferramenta. Se todos os outros hooks não retornaram nada ou retornaram apenas uma decisão allow, o resultado da ferramenta informa a Claude que o hook não respondeu antes de seu timeout, e o turno continua. Se outro de seus hooks já negou a chamada, sua negação ainda chega a Claude: o resultado da ferramenta inclui o motivo desse hook, e o texto do timeout pode vir em seguida.

    Antes da v2.1.210, Claude Code relatava o timeout a Claude como uma rejeição do usuário, o que fazia sessões autônomas pararem e aguardarem entrada.

  • PostToolUse e PostToolUseFailure: Claude Code mantém o resultado da ferramenta e o turno continua.

  • UserPromptSubmit e UserPromptExpansion: Claude Code bloqueia o prompt com uma mensagem nomeando o hook e o timeout, e a sessão continua. Antes da v2.1.208, Claude Code terminava a consulta com error_during_execution quando um callback nesses eventos expirava.

  • Stop e SubagentStop: o callback com timeout conta como retornando nenhuma decisão. O agente ou subagente para como se esse callback o tivesse permitido, e uma decisão de seus outros hooks no evento ainda se aplica. Antes do Claude Code v2.1.273, um callback Stop ou SubagentStop com timeout contava como uma execução de hook falhada, e Claude Code descartava as decisões de seus outros hooks no evento.

  • SessionStart: o callback com timeout conta como retornando nenhuma saída, e a sessão continua com a saída de seus outros hooks SessionStart.

  • PreModelSwitch: Claude Code bloqueia a mudança de modelo. Um hook que não responde não aprovou a mudança.

  • Outros eventos, como Notification, PreCompact e PostModelSwitch: Claude Code registra a falha e continua.

A primeira vez que um callback Stop ou SessionStart expira na sessão principal, Claude Code também adiciona uma SDKInformationalMessage ao fluxo de mensagens dizendo que o aplicativo que dirige a sessão não respondeu. Timeouts posteriores não repetem essa mensagem enquanto seu aplicativo permanecer sem resposta.

Se você interromper a consulta enquanto um callback está pendente, Claude Code cancela a chamada de ferramenta pendente. Antes da v2.1.208, a chamada de ferramenta ainda poderia prosseguir se você interrompesse durante um callback PreToolUse pendente.

Se seu callback precisar de mais tempo, defina um timeout mais alto em seu HookMatcher. Em TypeScript, use o AbortSignal do terceiro argumento de callback para lidar com cancelamento graciosamente quando o timeout dispara.

Ferramenta bloqueada inesperadamente

  • Verifique todos os hooks PreToolUse para retornos permissionDecision: 'deny'
  • Adicione logs aos seus hooks para ver qual permissionDecisionReason eles estão retornando
  • Verifique se padrões de matcher não são muito amplos: um matcher vazio corresponde a todas as ferramentas

Entrada modificada não aplicada

  • Certifique-se de que updatedInput está dentro de hookSpecificOutput, não no nível superior:

    return {
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "allow",
        updatedInput: { command: "new command" }
      }
    };
    
  • Não emparelhe updatedInput com permissionDecision: 'defer', que descarta a entrada modificada. Omitir permissionDecision é aceitável: a entrada modificada ainda se aplica através da avaliação de permissão normal. Você também pode retornar 'allow' para aprovar automaticamente a entrada modificada ou 'ask' para mostrá-la ao usuário para aprovação

  • Inclua hookEventName em hookSpecificOutput para identificar qual tipo de hook a saída é

Hooks de sessão não disponíveis em Python

SessionStart e SessionEnd podem ser registrados como hooks de callback do SDK em TypeScript, mas não estão disponíveis no SDK Python porque seu tipo HookEvent os omite. Em Python, eles estão disponíveis apenas como hooks de comando shell definidos em arquivos de configuração como .claude/settings.json. Quais arquivos de configuração sua aplicação SDK carrega depende de setting_sources ou settingSources. Se você definir essa opção, inclua a fonte que contém os hooks:

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

Para executar lógica de inicialização como um callback do SDK Python em vez disso, use a primeira mensagem de client.receive_response() como seu gatilho.

Prompts de permissão de subagente se multiplicando

Ao gerar múltiplos subagentes, cada um pode solicitar permissões separadamente para suas próprias chamadas de ferramenta. Para evitar prompts repetidos, use hooks PreToolUse para aprovar automaticamente ferramentas específicas, ou configure regras de permissão, que subagentes herdam da conversa pai.

Loops recursivos de hook com subagentes

Um hook UserPromptSubmit que gera subagentes pode criar loops infinitos se esses subagentes acionarem o mesmo hook. Para evitar isso:

  • Use uma variável compartilhada ou estado de sessão para rastrear se você já está dentro de um subagente
  • Restrinja os hooks para executar apenas na sessão de agente de nível superior

systemMessage não aparecendo na saída

O campo systemMessage mostra uma mensagem ao usuário, não ao modelo. No Claude Code v2.1.227 ou posterior, o systemMessage de um hook pode aparecer no fluxo de mensagens como uma SDKInformationalMessage. Se aparece ou não depende do evento. Cada seção de evento na página de hooks diz como a saída aparece. Para passar contexto ao modelo em vez disso, retorne additionalContext.

Antes da v2.1.227, o SDK expunha a saída de hook no fluxo de mensagens apenas para hooks SessionStart e Setup. Para qualquer outro evento, a saída aparecia apenas nos eventos de ciclo de vida que includeHookEvents (include_hook_events em Python) adiciona. A entrada dessa opção cobre quais eventos de ciclo de vida cada evento de hook produz.

Se você precisar expor decisões de hook para sua aplicação de forma confiável, registre-as separadamente ou use um canal de saída dedicado.

Recursos relacionados