SpyBara
Go Premium

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

This page contains 1 addition and 1 deletion.

2026
Mon 2 23:57 Tue 3 23:58 Fri 6 23:01 Sat 7 23:58 Mon 9 23:02 Tue 10 21:57 Wed 11 22:59

Referencia de hooks

Referencia para eventos de hooks de Claude Code, esquema de configuración, formatos de entrada/salida JSON, códigos de salida, hooks asincronos, hooks HTTP, hooks de prompt y hooks de herramientas MCP.

[!TIP]

Para una guía de inicio rápido con ejemplos, consulte Automatizar acciones con hooks.

Los hooks son comandos de shell definidos por el usuario, puntos finales HTTP, llamadas a herramientas MCP, prompts de LLM o subagentes que se ejecutan automáticamente en puntos específicos del ciclo de vida de Claude Code. Claude Code dispara los mismos eventos de hooks dondequiera que se ejecute: sesiones en la terminal, extensiones de IDE, la aplicación de escritorio y las sesiones en la nube. Utilice esta referencia para buscar esquemas de eventos, opciones de configuración, formatos de entrada/salida JSON y características avanzadas como hooks asincronos, hooks HTTP y hooks de herramientas MCP.

Un plugin también puede registrar hooks como funciones JavaScript que Claude Code llama en su propio proceso, que pueden dibujarse en la interfaz así como actuar sobre eventos. Un plugin que lo hace es un mod, y esos hooks de función se cubren en Reaccionar a eventos en lugar de aquí. Los hooks en esta página siguen funcionando junto con los mods.

Ciclo de vida de los hooks

Claude Code ejecuta hooks en puntos específicos durante una sesión. Cuando se activa un evento y un matcher coincide, Claude Code pasa contexto JSON sobre el evento a su controlador de hook. Para hooks de comando, la entrada llega en stdin. Para hooks HTTP, llega como el cuerpo de la solicitud POST. Su controlador puede entonces inspeccionar la entrada, tomar medidas y opcionalmente devolver una decisión.

Los eventos se dividen en tres cadencias:

  • por sesión: SessionStart y SessionEnd
  • por turno: UserPromptSubmit, Stop y StopFailure
  • en cada llamada a herramienta dentro del bucle agentico: PreToolUse y PostToolUse, excepto las llamadas EndConversation, que omiten ambas

Diagrama del ciclo de vida de hooks que muestra Setup opcional alimentando a SessionStart, luego un bucle por turno que contiene UserPromptSubmit, UserPromptExpansion para slash commands, el bucle agentico anidado (PreToolUse, PermissionRequest, PostToolUse, PostToolUseFailure, PostToolBatch, SubagentStart/Stop, TaskCreated, TaskCompleted), y Stop o StopFailure, seguido de TeammateIdle, PreCompact, PostCompact y SessionEnd, con Elicitation y ElicitationResult anidados dentro de la ejecución de herramientas MCP, PermissionDenied como una rama lateral de PermissionRequest para denegaciones en modo automático, WorktreeCreate, WorktreeRemove, Notification, ConfigChange, InstructionsLoaded, CwdChanged, FileChanged y DirectoryAdded como eventos asincronos independientes, PreModelSwitch como un evento secuencial independiente que se ejecuta antes de un cambio de modelo solicitado, PostModelSwitch como un evento asincrónico independiente que se ejecuta después de que cambia el modelo de la sesión, y MessageDisplay como un evento de solo visualización que se ejecuta mientras el texto del mensaje del asistente se transmite

La tabla a continuación resume cuándo se activa cada evento. La sección Hook events documenta el esquema de entrada completo y las opciones de control de decisión para cada uno.

Evento Cuándo se dispara
SessionStart Cuando una sesión comienza o se reanuda
Setup Cuando inicia Claude Code con --init-only, o con --init o --maintenance en modo -p. Para preparación única en CI o scripts
UserPromptSubmit Cuando se envía un prompt, antes de que Claude lo procese. También se dispara en turnos que Claude Code inicia por su cuenta
UserPromptExpansion Cuando un comando escrito por el usuario se expande en un prompt, antes de que llegue a Claude. Puede bloquear la expansión
PreToolUse Antes de que se ejecute una llamada a herramienta. Puede bloquearlo
PermissionRequest Cuando una llamada a herramienta necesita una decisión de permiso
PermissionDenied Cuando el modo automático deniega una llamada a herramienta, incluidas las denegaciones sin un veredicto del clasificador. Use JSON hookSpecificOutput.retry: true para indicar al modelo que puede reintentar la llamada a herramienta denegada. Claude Code ignora retry cuando el clasificador no produjo veredicto
PostToolUse Después de que una llamada a herramienta se ejecuta correctamente
PostToolUseFailure Después de que una llamada a herramienta falla
PostToolBatch Después de que se resuelve un lote completo de llamadas a herramientas paralelas, antes de la siguiente llamada al modelo
Notification Cuando Claude Code envía una notificación
MessageDisplay Mientras se muestra el texto del mensaje del asistente
SubagentStart Cuando se genera un subagente
SubagentStop Cuando un subagente finaliza
TaskCreated Cuando se está creando una tarea a través de TaskCreate
TaskCompleted Cuando se marca una tarea como completada
Stop Cuando Claude termina de responder
StopFailure Cuando el turno termina debido a un error de API
TeammateIdle Cuando un compañero de equipo de agentes está a punto de quedarse inactivo
InstructionsLoaded Cuando se carga un archivo CLAUDE.md o .claude/rules/*.md en el contexto. Se dispara al inicio de la sesión y cuando los archivos se cargan de forma diferida durante una sesión
ConfigChange Cuando un archivo de configuración cambia durante una sesión
CwdChanged Cuando el directorio de trabajo cambia, por ejemplo cuando Claude ejecuta un comando cd. Útil para la gestión reactiva del entorno con herramientas como direnv
DirectoryAdded Cuando se agrega un directorio de trabajo a mitad de sesión a través de /add-dir o la solicitud de control register_repo_root del SDK
FileChanged Cuando un archivo observado cambia en el disco. El campo matcher especifica qué nombres de archivo observar
WorktreeCreate Cuando se está creando un worktree a través de --worktree, isolation: "worktree", o para una sesión en segundo plano. Reemplaza el comportamiento predeterminado de git
WorktreeRemove Cuando se está eliminando un worktree que creó un hook WorktreeCreate
PreCompact Antes de la compactación de contexto
PostCompact Después de que se completa la compactación de contexto
PreModelSwitch Antes de que Claude Code aplique un cambio de modelo que usted o un cliente solicitó. Puede bloquear el cambio
PostModelSwitch Después de que cambia el modelo de la sesión, incluidos los cambios que Claude Code realiza por su cuenta, como restaurar el modelo cuando reanuda una sesión
Elicitation Cuando un servidor MCP solicita entrada del usuario durante una llamada a herramienta
ElicitationResult Después de que un usuario responde a una solicitud de MCP, antes de que la respuesta se envíe de vuelta al servidor
SessionEnd Cuando una sesión termina

Cómo se resuelve un hook

Para ver cómo encajan el evento, el matcher y el controlador, considere este hook PreToolUse que bloquea comandos de shell destructivos.

[tab] macOS/Linux

El matcher se reduce a llamadas a la herramienta Bash y a comandos de Monitor, y la condición if se reduce aún más a subcomandos Bash que coinciden con rm *, por lo que block-rm.sh solo se genera cuando ambos filtros coinciden:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

El script lee la entrada JSON desde stdin, extrae el comando y devuelve una permissionDecision de "deny" si contiene rm -rf. Guárdelo en .claude/hooks/block-rm.sh en su proyecto y hágalo ejecutable con chmod +x .claude/hooks/block-rm.sh para que Claude Code pueda ejecutarlo:

#!/bin/bash
# .claude/hooks/block-rm.sh
COMMAND=$(jq -r '.tool_input.command')

if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Destructive command blocked by hook"
    }
  }'
else
  exit 0  # no decision; normal permission flow applies
fi

Este script, como los otros ejemplos de Bash en esta página que analizan entrada JSON, utiliza jq, así que instala jq y asegúrate de que esté en tu PATH antes de probarlos.

[tab] Windows (PowerShell)

El matcher Bash|PowerShell cubre la herramienta PowerShell así como Bash. Una única regla if nombra una sola herramienta, por lo que cada herramienta obtiene su propio controlador: el primero se reduce a subcomandos Bash que coinciden con rm *, el segundo a comandos PowerShell que coinciden con Remove-Item *. Ambos ejecutan el mismo script a través de powershell.exe:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|PowerShell",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "powershell.exe",
            "args": [
              "-NoProfile",
              "-ExecutionPolicy",
              "Bypass",
              "-File",
              "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"
            ]
          },
          {
            "type": "command",
            "if": "PowerShell(Remove-Item *)",
            "command": "powershell.exe",
            "args": [
              "-NoProfile",
              "-ExecutionPolicy",
              "Bypass",
              "-File",
              "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.ps1"
            ]
          }
        ]
      }
    ]
  }
}

La bandera -NoProfile omite cargar su perfil de PowerShell para que el hook se inicie rápidamente, y -ExecutionPolicy Bypass permite que PowerShell ejecute el archivo de script local.

El script lee la entrada JSON desde stdin, extrae el comando y devuelve una permissionDecision de "deny" si contiene rm -rf o Remove-Item seguido de -Recurse. Guárdelo en .claude/hooks/block-rm.ps1 en su proyecto:

# .claude/hooks/block-rm.ps1
$callInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
$command = $callInput.tool_input.command

if ($command -match 'rm -rf|Remove-Item.*-Recurse') {
  @{
    hookSpecificOutput = @{
      hookEventName = "PreToolUse"
      permissionDecision = "deny"
      permissionDecisionReason = "Destructive command blocked by hook"
    }
  } | ConvertTo-Json
} else {
  exit 0  # no decision; normal permission flow applies
}

Ahora suponga que Claude Code decide ejecutar Bash "rm -rf /tmp/build" contra la configuración de macOS/Linux. Esto es lo que sucede:

Diagrama de resolución de hooks: PreToolUse se activa, el matcher verifica la coincidencia de Bash, luego la condición if verifica la coincidencia de Bash(rm *). Si ambos coinciden, el comando del hook se ejecuta y devuelve permissionDecision deny, por lo que la llamada a la herramienta se bloquea y Claude Code continúa. Si alguna verificación no coincide, el hook se omite y la llamada a la herramienta se permite continuar.

1. Se activa el evento

El evento PreToolUse se activa. Claude Code envía la entrada de la herramienta como JSON en stdin al hook:

{ "tool_name": "Bash", "tool_input": { "command": "rm -rf /tmp/build" }, ... }

2. El matcher verifica

El matcher "Bash" coincide con el nombre de la herramienta, por lo que se activa este grupo de hooks. Si omite el matcher o usa "*", el grupo se activa en cada ocurrencia del evento.

3. La condición if verifica

La condición if "Bash(rm *)" coincide porque rm -rf /tmp/build es un subcomando que coincide con rm *, por lo que se genera este controlador. Si el comando hubiera sido npm test, la verificación if habría fallado y block-rm.sh nunca se habría ejecutado, evitando la sobrecarga de generación de procesos. El campo if es opcional; sin él, cada controlador en el grupo coincidente se ejecuta.

4. Se ejecuta el controlador de hooks

El script inspecciona el comando completo y encuentra rm -rf, por lo que imprime una decisión en stdout:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Destructive command blocked by hook"
  }
}

Si el comando hubiera sido una variante más segura de rm como rm file.txt, el script habría alcanzado exit 0 en su lugar. El código de salida 0 sin salida significa que el hook no tiene decisión que reportar, por lo que la llamada a la herramienta continúa a través del flujo de permisos normal. El hook puede denegar la llamada, pero permanecer en silencio no la aprueba.

5. Claude Code actúa sobre el resultado

Claude Code lee la decisión JSON, bloquea la llamada a la herramienta y muestra a Claude la razón.

La sección Configuration a continuación documenta el esquema completo, y cada sección hook event documenta qué entrada recibe su comando y qué salida puede devolver.

Configuración

Los hooks se definen en archivos de configuración JSON. La configuración tiene tres niveles de anidamiento:

  1. Elige un evento de hook al que responder, como PreToolUse o Stop
  2. Agrega un grupo de matcher para filtrar cuándo se activa, por ejemplo, "para la herramienta Bash"
  3. Define uno o más manejadores de hook para ejecutar cuando coincida

Consulta Cómo se resuelve un hook arriba para un recorrido completo con un ejemplo anotado.

[!NOTE]

Esta página utiliza términos específicos para cada nivel: evento de hook para el punto del ciclo de vida, grupo de matcher para el filtro, y manejador de hook para el comando shell, punto final HTTP, herramienta MCP, prompt o agente que se ejecuta. "Hook" por sí solo se refiere a la característica general.

Ubicaciones de hooks

Dónde defines un hook determina su alcance:

Ubicación Alcance Compartible
~/.claude/settings.json Todos tus proyectos No, local en tu máquina
.claude/settings.json Proyecto único Sí, puede ser confirmado en el repositorio
.claude/settings.local.json Proyecto único No, ignorado por git cuando Claude Code guarda una configuración en él
Configuración de política administrada Toda la organización Sí, controlado por administrador
Plugin hooks/hooks.json Cuando el plugin está habilitado Sí, incluido con el plugin
Skill frontmatter El resto de la sesión una vez que se invoca la skill. Consulta Hooks en skills y agentes Sí, definido en el archivo de skill
Subagent frontmatter Mientras ese subagente se está ejecutando Sí, definido en el archivo del subagente

Las sesiones en la nube no leen tu ~/.claude/settings.json local. En un entorno autohospedado, Claude Code también ejecuta los hooks que el operador sembró desde el ~/.claude/ del host del ejecutor, y ejecuta los hooks en el archivo de configuración administrada de la imagen del ejecutor cuando ese archivo está entre las fuentes administradas que Claude Code aplica, lo que por defecto significa solo cuando ni la configuración administrada por servidor ni una política de Claude Code entregada por MDM suministran el nivel administrado. Consulta qué se transfiere de tu configuración para saber qué archivos de configuración y plugins, y por lo tanto qué hooks, llegan a una sesión en la nube.

Para obtener detalles sobre la resolución de archivos de configuración, consulta settings.

Los hooks de archivos de configuración, configuración de política administrada y plugins también se ejecutan dentro de subagentes. Cuando un subagente llama a una herramienta, eventos de herramienta como PreToolUse y PostToolUse activan los mismos hooks configurados que en la conversación principal, y la entrada lleva los campos de entrada comunes agent_id y agent_type que identifican al subagente.

Los administradores pueden usar allowManagedHooksOnly en configuración administrada para restringir qué hooks se ejecutan:

  • Tus hooks de usuario, proyecto, local y plugin están bloqueados. Los hooks de plugins forzados a estar habilitados en la configuración administrada enabledPlugins están exentos
  • Claude Code también reduce tu configuración statusLine, fileSuggestion y subagentStatusLine a la configuración administrada
  • Claude Code también deshabilita plugins con una fuente command, incluidos los plugins forzados a estar habilitados en la configuración administrada enabledPlugins, a menos que disableCommandPluginSources esté explícitamente establecido en false. Las fuentes command requieren Claude Code v2.1.229 o posterior
  • Claude Code también bloquea los comandos headersHelper del marketplace a menos que disableCommandPluginSources esté explícitamente establecido en false, excepto para un marketplace que la propia configuración administrada declare

Consulta qué se ejecuta bajo allowManagedHooksOnly.

Las entradas de hook se fusionan entre niveles de configuración en lugar de reemplazarse entre sí: la configuración de usuario, proyecto y local añaden sus propios hooks sin eliminar los administrados, y la configuración disableAllHooks no puede deshabilitar hooks administrados desde fuera de la configuración administrada.

Las listas de permitidos de hooks HTTP se aplican a hooks de todas las fuentes, incluida la configuración de política administrada:

  • allowedHttpHookUrls: cuando se define en cualquier nivel de configuración, Claude Code ejecuta un manejador de hook HTTP solo si su URL coincide con la lista de permitidos fusionada
  • httpHookAllowedEnvVars: cuando se define, Claude Code interpola solo las variables de entorno en esa lista en los encabezados de hook

Patrones de matcher

El campo matcher filtra cuándo se activan los hooks. Cómo se evalúa un matcher depende de los caracteres que contenga:

Valor de matcher Evaluado como Ejemplo
"*", "" u omitido Coincidir con todo se activa en cada ocurrencia del evento
Solo letras, dígitos, _, -, espacios, , y | Cadena exacta, o lista de cadenas exactas separadas por | o , con espacios en blanco opcionales alrededor Bash coincide con la herramienta Bash y, en PreToolUse y PermissionRequest, también con comandos de Monitor; Edit|Write y Edit, Write cada una coincide con cualquiera de las herramientas exactamente; code-reviewer coincide solo con ese tipo de agente
Contiene cualquier otro carácter Expresión regular de JavaScript, sin anclar ^Notebook coincide con cualquier herramienta cuyo nombre comience con Notebook; mcp__memory__.* coincide con cada herramienta del servidor memory

Un matcher en la ruta de expresión regular se prueba con RegExp.prototype.test de JavaScript, que tiene éxito en una coincidencia en cualquier lugar del valor. Edit.* coincide tanto con Edit como con NotebookEdit; envuelve el patrón en ^ y $, como en ^Edit$, cuando necesites una coincidencia de cadena completa.

FileChanged y StopFailure utilizan un conjunto de coincidencia exacta más estrecho de solo letras, dígitos, _ y |. Un guion, espacio o coma en un matcher para esos dos eventos lo mantiene en la ruta de expresión regular, y solo | separa alternativas. Todos los demás eventos con soporte de matcher en la tabla que sigue aceptan | o ,.

El evento FileChanged no sigue estas reglas al construir su lista de vigilancia. Consulta FileChanged.

Cada tipo de evento coincide en un campo diferente:

Evento Qué filtra el matcher Valores de matcher de ejemplo
PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied nombre de la herramienta Bash, Edit|Write, mcp__.*
SessionStart cómo comenzó la sesión startup, resume, clear, compact, fork
Setup qué bandera CLI activó la configuración init, maintenance
SessionEnd por qué terminó la sesión clear, resume, logout, prompt_input_exit, other
Notification tipo de notificación permission_prompt, idle_prompt, auth_success, auth_storage_failure, elicitation_dialog, elicitation_url_dialog, elicitation_complete, elicitation_response, agent_needs_input, agent_completed, quota_auto_resume_fired, quota_auto_resume_stale, quota_auto_resume_disabled, plugin_notification
SubagentStart tipo de agente general-purpose, Explore, Plan, nombres de agentes personalizados, o nombres con alcance de plugin como ^my-plugin:reviewer$
PreCompact, PostCompact qué activó la compactación manual, auto
PreModelSwitch, PostModelSwitch nombre canónico del modelo al que cambia la sesión, como se describe en PreModelSwitch claude-opus-5, claude-opus-4-6|claude-opus-5, .*opus.*
SubagentStop tipo de agente los mismos valores que SubagentStart
ConfigChange fuente de configuración user_settings, project_settings, local_settings, policy_settings, skills
CwdChanged sin soporte de matcher siempre se activa en cada ocurrencia
DirectoryAdded cómo se añadió el directorio slash_command, register_repo_root
FileChanged nombres de archivo literales a vigilar (consulta FileChanged) .envrc|.env
StopFailure tipo de error rate_limit, overloaded, authentication_failed, oauth_org_not_allowed, account_on_hold, billing_error, invalid_request, model_not_found, server_error, max_output_tokens, cloud_credential_error, unknown
InstructionsLoaded razón de carga session_start, nested_traversal, path_glob_match, include, compact
UserPromptExpansion nombre del comando tus nombres de skill o comando
Elicitation nombre del servidor MCP tus nombres de servidor MCP configurados
ElicitationResult nombre del servidor MCP los mismos valores que Elicitation
UserPromptSubmit, PostToolBatch, Stop, TeammateIdle, TaskCreated, TaskCompleted, WorktreeCreate, WorktreeRemove, MessageDisplay sin soporte de matcher siempre se activa en cada ocurrencia

Hacer coincidir StopFailure en cloud_credential_error requiere Claude Code v2.1.267 o posterior, la primera versión que reporta fallos de carga de credenciales bajo ese valor en lugar de server_error o unknown.

Para la mayoría de eventos, Claude Code evalúa el matcher contra un campo de la entrada JSON que envía a tu hook en stdin. Para eventos de herramienta, ese campo es tool_name. Para PreModelSwitch y PostModelSwitch, Claude Code evalúa el matcher contra el nombre canónico que deriva de to_model, como se describe en PreModelSwitch. Cada sección de evento de hook enumera el conjunto completo de valores de matcher y el esquema de entrada para ese evento.

Este ejemplo ejecuta un script de linting solo cuando Claude escribe o edita un archivo:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/lint-check.sh"
          }
        ]
      }
    ]
  }
}

Si añades un campo matcher a un evento sin soporte de matcher, se ignora silenciosamente.

Para eventos de herramienta, puedes filtrar más estrictamente estableciendo el campo if en manejadores de hook individuales. if utiliza sintaxis de regla de permisos para coincidir contra el nombre de la herramienta y los argumentos juntos, así que "Bash(git *)" se ejecuta cuando cualquier subcomando de la entrada de Bash coincide con git * y "Edit(*.ts)" se ejecuta solo para archivos TypeScript.

Coincidir herramientas MCP

Las herramientas del servidor MCP aparecen como herramientas regulares en eventos de herramienta (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied), así que puedes hacerlas coincidir de la misma manera que cualquier otro nombre de herramienta.

Las herramientas MCP siguen el patrón de nomenclatura mcp__<server>__<tool>, por ejemplo:

  • mcp__memory__create_entities: herramienta crear entidades del servidor Memory
  • mcp__filesystem__read_file: herramienta leer archivo del servidor Filesystem
  • mcp__github__search_repositories: herramienta buscar del servidor GitHub

Para hacer coincidir cada herramienta de un servidor, añade .* al prefijo del servidor. El .* es obligatorio: un matcher como mcp__memory o mcp__brave-search contiene solo caracteres de coincidencia exacta, así que se compara como una cadena exacta y no coincide con ninguna herramienta.

  • mcp__memory__.* coincide con todas las herramientas del servidor memory
  • mcp__brave-search__.* coincide con todas las herramientas de un servidor cuyo nombre contiene un guion
  • mcp__.*__write.* coincide con cualquier herramienta cuyo nombre comienza con write de cualquier servidor

Las herramientas de un servidor MCP incluido en plugin utilizan un segmento de servidor con alcance que incluye el nombre del plugin: mcp__plugin_<plugin-name>_<server-name>__<tool>. Un matcher escrito contra la clave del servidor desnuda nunca se activa para estas herramientas. Para un plugin llamado my-plugin que incluye un servidor bajo la clave db, una herramienta query aparece como mcp__plugin_my-plugin_db__query, así que el matcher para cada herramienta de ese servidor es mcp__plugin_my-plugin_db__.*. Utiliza el mismo nombre de herramienta con alcance en el campo if de un manejador. Consulta Servidores MCP proporcionados por plugin para saber cómo se construye el nombre con alcance.

Este ejemplo registra todas las operaciones del servidor de memoria y valida operaciones de escritura de cualquier servidor MCP:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__memory__.*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Memory operation initiated' >> ~/mcp-operations.log"
          }
        ]
      },
      {
        "matcher": "mcp__.*__write.*",
        "hooks": [
          {
            "type": "command",
            "command": "/home/user/scripts/validate-mcp-write.py"
          }
        ]
      }
    ]
  }
}

Coincidir comandos de Monitor

Un hook PreToolUse o PermissionRequest con un matcher Bash también se ejecuta en los comandos de shell que Claude inicia con la herramienta Monitor, pero su allow y su updatedInput no tienen efecto sobre ellos. Requiere Claude Code v2.1.314 o posterior. Los matchers como Bash|PowerShell, ^Bash$ y Bash.* también son matchers Bash. Un hook con un matcher Bash no se ejecuta para una vigilancia de WebSocket, y un hook PostToolUse con uno no se ejecuta para Monitor.

Un hook que se ejecuta para un comando de Monitor puede denegarlo o hacer que Claude Code te pregunte al respecto. Para permitir o reescribir el comando, cambia de un matcher Bash a uno que incluya Monitor:

  • Matcher que incluye Monitor: con un matcher como Bash|Monitor o .*, o sin matcher, el hook recibe la propia entrada de la llamada a Monitor, y allow y updatedInput se aplican. En if, nombra Monitor, como en Monitor(tail *), que coincide según las reglas que sigue Bash(tail *). Si su if nombra Bash, el hook sigue recibiendo la entrada de la llamada a Monitor, pero allow y updatedInput no tienen efecto
  • Matcher Bash: el hook recibe una llamada a Bash. En if, nombra Bash, como en Bash(tail *). El hook no se ejecuta si su if nombra Monitor

Campos de manejador de hook

Cada objeto en el array hooks interno es un manejador de hook: el comando shell, punto final HTTP, herramienta MCP, prompt LLM o agente que se ejecuta cuando el matcher coincide. Hay cinco tipos:

  • Hooks de comando (type: "command"): ejecutan un comando shell. Tu script recibe la entrada JSON del evento en stdin y comunica resultados de vuelta a través de códigos de salida y stdout.
  • Hooks HTTP (type: "http"): envían la entrada JSON del evento como una solicitud HTTP POST a una URL. El punto final comunica resultados de vuelta a través del cuerpo de respuesta utilizando el mismo formato de salida JSON que los hooks de comando.
  • Hooks de herramienta MCP (type: "mcp_tool"): llaman a una herramienta en un servidor MCP configurado. La salida de texto de la herramienta se trata como stdout de hook de comando.
  • Hooks de prompt (type: "prompt"): envían un prompt a un modelo Claude para evaluación de un solo turno. El modelo devuelve su decisión como JSON. Consulta Hooks basados en prompt.
  • Hooks de agente (type: "agent"): generan un subagente que puede usar herramientas como Read, Grep y Glob para verificar condiciones antes de devolver una decisión. Los hooks de agente son experimentales y pueden cambiar. Consulta Hooks basados en agente.

Cuando se activa un evento, sus hooks coincidentes se ejecutan en paralelo. Si más de 12 de ellos son hooks de prompt o de agente, 12 de esos se ejecutan a la vez y el resto comienza a medida que terminan los que están en ejecución. El timeout de cada hook en espera empieza a contar cuando ese hook comienza.

Si defines el mismo manejador en más de un archivo de configuración, se ejecuta una vez. Una copia del mismo manejador de un plugin o skill se mantiene separada.

Los manejadores se ejecutan en el directorio actual con el entorno de Claude Code. Si el directorio actual ya no existe, por ejemplo un worktree o directorio temporal que otro shell eliminó a mitad de sesión, Claude Code ejecuta hooks de comando desde el primero de estos que aún existe: el directorio en el que comenzó la sesión, la raíz del proyecto, tu directorio home o el directorio temporal del sistema. Claude Code registra una advertencia nombrando el directorio de respaldo en el registro de depuración. Para una sesión de worktree que inicias desde la aplicación de escritorio, consulta Qué comparten los worktrees con el checkout principal.

La variable de entorno $CLAUDE_CODE_REMOTE es "true" en entornos web remotos y no está establecida en la CLI local. Claude Code v2.1.199 y posterior establece $CLAUDE_CODE_BRIDGE_SESSION_ID en el ID de sesión de Remote Control mientras la sesión local tiene una conexión activa de Remote Control.

Campos comunes

Estos campos se aplican a todos los tipos de hook:

Campo Requerido Descripción
type sí "command", "http", "mcp_tool", "prompt" o "agent"
if no Sintaxis de regla de permisos para filtrar cuándo se ejecuta este hook, como "Bash(git *)" o "Edit(*.ts)". El comando de hook solo se ejecuta si la llamada a herramienta coincide con el patrón. Consulta la tabla de coincidencia de Bash para saber cómo los patrones de Bash se evalúan contra subcomandos, $() y backticks. Solo se evalúa en eventos de herramienta: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest y PermissionDenied. En otros eventos, un hook con if establecido nunca se ejecuta
timeout no Segundos antes de cancelar. Claude Code no lo aplica en un hook de comando que ejecutes con async: true. Valores por defecto: 600 para command, http y mcp_tool; 30 para prompt; 60 para agent. Claude Code reduce el valor por defecto de command, http y mcp_tool a 30 en UserPromptSubmit, PreModelSwitch y PostModelSwitch, y a 10 en MessageDisplay. Los hooks de SessionEnd comparten un presupuesto de 1.5 segundos; si tu configuración establece un timeout por hook más largo, Claude Code aumenta el presupuesto para que coincida, hasta 60 segundos
statusMessage no Mensaje de spinner personalizado mostrado mientras se ejecuta el hook
once no Si es true, Claude Code elimina el hook después de su primera ejecución exitosa. Una ejecución que falla, bloquea con código de salida 2 o agota el tiempo de espera deja el hook en su lugar, así que se ejecuta de nuevo en el siguiente evento coincidente. Solo se respeta para hooks declarados en frontmatter de skill; se ignora en archivos de configuración y frontmatter de agente

El campo if contiene exactamente una regla de permisos. No hay sintaxis &&, || o de lista para combinar reglas; para aplicar múltiples condiciones, define un manejador de hook separado para cada una.

En una condición if para una herramienta de archivo, un patrón de directorio de un solo segmento como "Edit(src/**)" coincide solo con el directorio src en el directorio de trabajo y los archivos bajo él. Para hacer coincidir un directorio llamado src a cualquier profundidad, escribe "Edit(**/src/**)".

Cómo los patrones if coinciden con comandos de Bash

Para patrones de Bash en el campo if, si tu comando de hook se ejecuta depende de la forma del patrón y del comando de Bash que Claude está invocando. Las asignaciones VAR=value iniciales se eliminan antes de hacer coincidir.

patrón if comando Bash ¿Se ejecuta el hook? Por qué
Bash(git *) FOO=bar git push sí las asignaciones iniciales se eliminan; git push coincide
Bash(git *) npm test && git push sí cada subcomando se verifica; git push coincide
Bash(rm *) echo $(rm -rf /) sí los comandos dentro de $() y backticks se verifican; rm -rf / coincide
Bash(rm *) echo $(date) no ningún subcomando coincide con rm *
Bash(git push *) echo $(date) sí los patrones que especifican más que el nombre del comando ejecutan el hook de todas formas en $(), backticks o $VAR

Cuando Claude Code no puede determinar qué comandos ejecuta la entrada de Bash, ejecuta tu hook independientemente del patrón. Debido a que el filtro if es de mejor esfuerzo, utiliza el sistema de permisos en lugar de un hook para aplicar una autorización o denegación dura.

Campos de hook de comando

Además de los campos comunes, los hooks de comando aceptan estos campos:

Campo Requerido Descripción
command sí Comando shell a ejecutar. Con args, el ejecutable a generar directamente. Consulta Forma exec y forma shell
args no Lista de argumentos. Cuando está presente, command se resuelve como un ejecutable y se genera directamente con args como el vector de argumentos, sin shell involucrado. Consulta Forma exec y forma shell
async no Si es true, se ejecuta en segundo plano sin bloquear. Consulta Ejecutar hooks en segundo plano
asyncRewake no Si es true, se ejecuta en segundo plano y despierta a Claude en código de salida 2. El stderr del hook, o stdout si stderr está vacío, se muestra a Claude como un recordatorio del sistema para que pueda reaccionar a un fallo de fondo de larga duración
shell no Shell a usar para este hook. Acepta "bash" o "powershell". Por defecto es "bash", o "powershell" en Windows cuando Git Bash no está instalado. Establecer "powershell" ejecuta el comando a través de PowerShell en Windows. No requiere CLAUDE_CODE_USE_POWERSHELL_TOOL ya que los hooks generan PowerShell directamente. Se ignora cuando args está establecido
onFailure no Si la acción que activó el hook sigue adelante cuando el hook falla: "continue" o "block". Consulta Bloquear la acción cuando un hook falla para "block" y para dejar el campo sin establecer. Requiere Claude Code v2.1.295 o posterior
Forma exec y forma shell

Un hook de comando se ejecuta como forma exec cuando args está establecido, y forma shell cuando args se omite. Establece args siempre que el hook haga referencia a un marcador de posición de ruta, ya que cada elemento se pasa como un argumento sin comillas. Omite args cuando necesites características de shell como pipes o &&, o cuando ninguna preocupación se aplique.

Forma exec se ejecuta cuando args está presente. Claude Code resuelve command como un ejecutable en PATH y lo genera directamente con args como el vector de argumentos. No hay shell, así que cada elemento de args es un argumento exactamente como está escrito, sin comillas, y los marcadores de posición de ruta como ${CLAUDE_PLUGIN_ROOT} se sustituyen en command y en cada elemento de args como cadenas simples. Los caracteres especiales como apóstrofes, $ y backticks pasan sin cambios porque no hay shell para interpretarlos. No ocurre tokenización de shell en ninguna plataforma.

Forma shell se ejecuta cuando args está ausente. La cadena command se pasa a un shell: sh -c en macOS y Linux, Git Bash en Windows, o PowerShell cuando Git Bash no está instalado. Establece el campo shell para elegir explícitamente. El shell tokeniza la cadena, expande variables e interpreta pipes, &&, redirecciones y globs.

[!NOTE]

En Windows, la forma exec requiere que command se resuelva en un ejecutable real como .exe. Los shims .cmd y .bat que npm, npx, eslint y otras herramientas instalan en node_modules/.bin no son ejecutables y no se pueden generar sin un shell. Para ejecutarlos en forma exec, invoca el script subyacente con node directamente, por ejemplo "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/node_modules/eslint/bin/eslint.js"]. El patrón node más ruta de script funciona en cada plataforma porque node.exe es un binario real. Para ejecutar un shim .cmd o .bat por nombre, usa forma shell.

Este ejemplo ejecuta un script de Node incluido con un plugin. La forma exec pasa la ruta de script resuelta como un argumento sin comillas:

{
  "type": "command",
  "command": "node",
  "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/format.js", "--fix"]
}

La forma shell equivalente necesita comillas para manejar rutas con espacios o caracteres especiales:

{
  "type": "command",
  "command": "node \"${CLAUDE_PLUGIN_ROOT}\"/scripts/format.js --fix"
}

Ambas formas soportan los mismos marcadores de posición de ruta, y ambas los exportan como las variables de entorno CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_ROOT y CLAUDE_PLUGIN_DATA en el proceso generado, así que un script puede leer process.env.CLAUDE_PLUGIN_ROOT independientemente de cómo fue lanzado.

Los hooks de plugin además sustituyen valores ${user_config.*}, solo en forma exec: el valor se sustituye en command y en cada elemento de args como una cadena simple, así que ningún shell lo re-analiza.

Un hook de plugin en forma shell cuyo command hace referencia a ${user_config.*} falla con un error en lugar de ejecutarse. Para usar un valor de opción de un hook en forma shell, lee la variable de entorno $CLAUDE_PLUGIN_OPTION_<KEY>, como $CLAUDE_PLUGIN_OPTION_WEBHOOK_URL para una opción webhook_url, o establece args para cambiar el hook a forma exec. Antes de v2.1.207, los comandos de hook de plugin en forma shell también sustituían ${user_config.*}.

[!NOTE]

En forma exec, command es solo el nombre o ruta del ejecutable. Si command es un nombre desnudo sin separador de ruta y contiene espacios junto con args, Claude Code registra una advertencia porque la generación fallará: no hay un ejecutable llamado node script.js. Mueve los tokens extra a args. Las rutas absolutas con espacios, como C:\Program Files\nodejs\node.exe, son un ejecutable válido único y no activan la advertencia.

Campos de hook HTTP

Además de los campos comunes, los hooks HTTP aceptan estos campos:

Campo Requerido Descripción
url sí URL a la que enviar la solicitud POST
headers no Encabezados HTTP adicionales como pares clave-valor. Los valores soportan interpolación de variables de entorno usando sintaxis $VAR_NAME o ${VAR_NAME}. Solo se resuelven las variables listadas en allowedEnvVars
allowedEnvVars no Lista de nombres de variables de entorno que pueden interpolarse en valores de encabezado. Las referencias a variables no listadas se reemplazan con cadenas vacías. Requerido para que funcione cualquier interpolación de variable de entorno
onFailure no Qué le sucede a la acción cuando el hook falla: "continue", el valor por defecto, o "block". Consulta Bloquear la acción cuando un hook falla. Requiere Claude Code v2.1.295 o posterior

Claude Code envía la entrada JSON del hook como el cuerpo de la solicitud POST con Content-Type: application/json. El cuerpo de respuesta utiliza el mismo formato de salida JSON que los hooks de comando.

El manejo de errores difiere de los hooks de comando; consulta Manejo de respuesta HTTP.

Este ejemplo envía eventos PreToolUse a un servicio de validación local, autenticándose con un token de la variable de entorno MY_TOKEN:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "http",
            "url": "http://localhost:8080/hooks/pre-tool-use",
            "timeout": 30,
            "headers": {
              "Authorization": "Bearer $MY_TOKEN"
            },
            "allowedEnvVars": ["MY_TOKEN"]
          }
        ]
      }
    ]
  }
}

Campos de hook de herramienta MCP

Además de los campos comunes, los hooks de herramienta MCP aceptan estos campos:

Campo Requerido Descripción
server sí Nombre de un servidor MCP configurado. Para un servidor incluido en plugin, este es el nombre con alcance plugin:<plugin-name>:<server-name>, como plugin:my-plugin:db, no la clave del servidor desnuda
tool sí Nombre de la herramienta a llamar en ese servidor
input no Argumentos pasados a la herramienta. Los valores de cadena soportan sustitución de ${path} de la entrada JSON del hook, como "${tool_input.file_path}"

Este ejemplo llama a la herramienta security_scan en el servidor MCP my_server después de cada Write o Edit, pasando la ruta del archivo editado:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "mcp_tool",
            "server": "my_server",
            "tool": "security_scan",
            "input": { "file_path": "${tool_input.file_path}" }
          }
        ]
      }
    ]
  }
}
Cómo se lee el resultado de la herramienta

Claude Code lee el contenido de texto de la herramienta de la misma manera que lee stdout de hook de comando, siguiendo la regla de análisis bajo código de salida 0. Si la herramienta devuelve isError: true, el hook produce un error sin bloqueo y la ejecución continúa.

Si un error de hook contiene uno de estos mensajes, Claude Code no pudo leer la respuesta de la herramienta, así que el hook no dio ningún veredicto:

MCP tool "security_scan" on server "my_server" gave no answer that could be read, because a message from the server was too large, so this hook gave no verdict. If this keeps happening, the tool must send a smaller answer.
MCP tool "security_scan" on server "my_server" answered without its text, which happens when a result is too large, so this hook gave no verdict. If this keeps happening, the tool must send a smaller answer.

En PreToolUse, Claude Code rechaza la llamada a herramienta que el hook estaba verificando. En otros eventos como UserPromptSubmit, PostToolUse y Stop, el error es sin bloqueo.

Si el error sigue apareciendo, devuelve menos datos desde la herramienta, o cambia tool o input del hook para que la respuesta sea más pequeña.

Cuando el servidor aún se está conectando

En eventos donde un hook puede bloquear o cambiar el resultado, como PreToolUse o Stop, Claude Code espera a que un servidor se conecte antes de llamar a la herramienta, durante como máximo MCP_TIMEOUT y dentro del timeout del propio hook. En eventos observacionales, como Notification o SessionEnd, no espera.

Un servidor que muestra el estado cached se conecta cuando el hook llama a su herramienta. Si el servidor no está conectado en ese punto, el hook produce un error sin bloqueo y la ejecución continúa. El hook nunca inicia un flujo OAuth, así que autentica el servidor desde /mcp primero.

Eventos que se activan antes de que los servidores MCP estén disponibles

SessionStart al lanzar, incluyendo con --continue o --resume, y cada evento Setup se activan antes de que los servidores MCP de la sesión estén disponibles para hooks. Claude Code omite sus hooks mcp_tool sin llamar a la herramienta, y el registro de depuración registra mcp_tool hooks are not available for the 'SessionStart' hook event (no MCP client context), o el mismo mensaje nombrando Setup. Cuando SessionStart se activa de nuevo más tarde en la sesión, después de /clear o una compactación, sus hooks mcp_tool se ejecutan. Para cualquier cosa que la sesión necesite al lanzar, usa un hook type: "command" en SessionStart en su lugar.

Campos de hook de prompt y agente

Además de los campos comunes, los hooks de prompt y agente aceptan estos campos:

Campo Requerido Descripción
prompt sí Texto de prompt a enviar al modelo. Usa $ARGUMENTS como marcador de posición para la entrada JSON del hook. Escapa con una barra invertida para incluir texto literal: \$1.00 se renderiza como $1.00
model no Modelo a usar para evaluación. Por defecto es el modelo que Claude Code usa para funcionalidad de fondo

Hacer referencia a scripts por ruta

Utiliza estos marcadores de posición para hacer referencia a scripts de hook relativos a la raíz del proyecto o plugin, independientemente del directorio de trabajo cuando se ejecuta el hook:

  • ${CLAUDE_PROJECT_DIR}: la raíz del proyecto donde comenzó la sesión. Claude Code también establece esta variable en el entorno de servidores MCP stdio y servidores LSP de plugin.
  • ${CLAUDE_PLUGIN_ROOT}: el directorio de instalación del plugin, para scripts incluidos con un plugin. Consulta variables de entorno de plugin para saber cómo se comporta la ruta entre actualizaciones.
  • ${CLAUDE_PLUGIN_DATA}: el directorio de datos persistentes del plugin, para dependencias y estado que deben sobrevivir a actualizaciones de plugin.

[!NOTE]

Los worktrees son diferentes. Si Claude entra en un worktree durante la sesión, Claude Code mantiene ${CLAUDE_PROJECT_DIR} donde estaba y pasa la ruta del worktree a tus hooks de una manera diferente:

  • ${CLAUDE_PROJECT_DIR} se queda en su lugar: aún apunta a la raíz del proyecto donde comenzó la sesión, así que un comando como ${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh aún ejecuta el script en el checkout principal.
  • cwd sigue a Claude: el campo cwd en la entrada JSON del hook es la raíz del worktree después de que Claude entra en un worktree, y el nuevo directorio después de que Claude ejecuta cd. Léelo cuando un hook necesite saber en qué directorio está trabajando Claude.

Para una sesión de worktree que inicias desde la aplicación de escritorio, consulta Qué comparten los worktrees con el checkout principal para saber a dónde apunta ${CLAUDE_PROJECT_DIR}.

Prefiere forma exec para cualquier hook que haga referencia a un marcador de posición de ruta. En forma shell, envuelve cada marcador de posición en comillas dobles.

[tab] Project scripts

Este ejemplo usa ${CLAUDE_PROJECT_DIR} para ejecutar un verificador de estilo desde el directorio .claude/hooks/ del proyecto después de cualquier llamada de herramienta Write o Edit:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-style.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

[tab] Plugin scripts

Define hooks de plugin en hooks/hooks.json con un campo description opcional de nivel superior. Cuando un plugin está habilitado, sus hooks se fusionan con tus hooks de usuario y proyecto.

Este ejemplo ejecuta un script de formato incluido con el plugin:

{
  "description": "Automatic code formatting",
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
            "args": [],
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Consulta la referencia de componentes de plugin para obtener detalles sobre cómo crear hooks de plugin.

Hooks en skills y agentes

Además de archivos de configuración y plugins, los hooks pueden definirse directamente en skills y subagentes usando frontmatter, en el mismo formato de configuración que los hooks basados en configuración. Cuánto tiempo Claude Code los mantiene registrados depende del componente:

  • Hooks de subagente: Claude Code los ejecuta solo mientras ese subagente se está ejecutando y los elimina cuando termina. Claude Code convierte un hook Stop aquí a SubagentStop, el evento que se activa cuando un subagente se completa.
  • Hooks de skill: Claude Code los registra cuando tú o Claude invocas la skill y los mantiene ejecutándose durante el resto de la sesión, en turnos después del turno de la propia skill también. Para que Claude Code elimine un hook después de su primera ejecución exitosa en su lugar, establece once: true en él.

Esta skill define un hook PreToolUse que ejecuta un script de validación de seguridad antes de cada comando Bash:

---
name: secure-operations
description: Perform operations with security checks
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "./scripts/security-check.sh"
---

Los subagentes utilizan el mismo formato en su frontmatter YAML.

Los hooks de frontmatter en una skill de proyecto siguen la misma regla de confianza de espacio de trabajo que los hooks en archivos de configuración. Claude Code los registra cuando tú o Claude invocas la skill, incluyendo en una ejecución -p en una carpeta que no has confiado.

Los hooks de frontmatter en un subagente de proyecto se ejecutan solo después de que aceptes el diálogo de confianza de espacio de trabajo para la carpeta de la que vino el archivo del agente. Una sesión -p no cuenta como aceptarlo. Qué se ejecuta antes de confiar en una carpeta compara esto con la regla del archivo de configuración, y la página de subagentes enumera qué alcances están exentos.

Límites de los hooks de skills

Cuando los hooks de frontmatter de un skill no se activan después de que tú o Claude invocan el skill, ejecuta /hooks en esa sesión. Si los enumera, consulta El hook no se activa. Si no enumera ninguno de ellos, es posible que el skill esté bajo un bloqueo de solo plugins o haya superado un límite de hooks, y se ejecuta sin ninguno de sus hooks de frontmatter. Comprueba cada caso:

  • Bloqueo de solo plugins: el administrador de tu organización puede restringir los hooks a plugins y configuración administrada con el ajuste administrado strictPluginOnlyCustomization. Un skill conserva sus hooks de frontmatter bajo ese bloqueo solo si es un skill de plugin, un skill incluido o un skill en el directorio de configuración administrada. Los hooks de tus propios archivos de configuración tampoco se ejecutan.
    • Comprobación: escribe /status en una sesión de Claude Code. Si la línea Setting sources incluye Enterprise managed settings, pregunta a tu administrador si strictPluginOnlyCustomization bloquea los hooks.
    • Solución: mueve el skill a un plugin, porque los skills de plugin conservan sus hooks de frontmatter. Agrega el plugin a un marketplace y luego instálalo desde ahí. Si Claude Code rechaza el marketplace con is blocked by enterprise policy, pide a tu administrador que permita ese marketplace.
  • Límite de hooks: en Claude Code v2.1.304 o posterior, cada fuente de skills tiene su propio límite por sesión. Tu proyecto, tu carpeta de skills personales y cada plugin cuentan como fuentes separadas. Los skills de una fuente pueden registrar como máximo 1,024 hooks. Los matchers de esos hooks pueden sumar como máximo 16,384 caracteres. Un matcher con varios hooks bajo él cuenta una vez por cada hook.
    • Comprobación: repite la sesión en la que el hook no se activó, con el registro de depuración activado:
      1. Inicia una nueva sesión desde tu shell con claude --debug. Si no inicias sesiones desde un shell, escribe /debug primero en la nueva sesión en su lugar.
      2. Invoca los mismos skills en el mismo orden, incluidos los que invocó Claude, porque ambos totales se acumulan skill por skill. Pide a Claude que invoque cualquier skill que no puedas escribir como comando.
      3. Busca registers none of its hooks en el registro de depuración. Una línea que contiene ese texto nombra el skill que superó el límite.
    • Solución: declara menos hooks o matchers más cortos en ese skill o en los demás skills de su fuente, y luego inicia una nueva sesión. Si no te corresponde editar esos skills, pide ese cambio a su autor. Mientras tanto, inicia una nueva sesión e invoca el skill que necesitas antes que cualquier otro skill de su fuente.

Si ninguno de los casos es el tuyo, comprueba si hay frontmatter que no se analizó correctamente.

El menú /hooks

Escribe /hooks en Claude Code para abrir un navegador de solo lectura para tus hooks configurados. La lista etiqueta cada hook con su procedencia, como la configuración de usuario, la configuración de proyecto, la configuración local, un plugin o la sesión actual.

Selecciona un hook para ver el texto completo de lo que ejecuta y dónde está definido, como la ruta de su archivo de configuración o el nombre de su plugin.

Para explorar todos los eventos de hook, incluidos los que no tienen hooks configurados, selecciona All events al final de la lista.

Deshabilitar o eliminar hooks

Para eliminar un hook definido en un archivo de configuración, borra su entrada de ese archivo.

Para deshabilitar temporalmente todos los hooks sin eliminarlos, establece "disableAllHooks": true en tu archivo de configuración. Claude Code lee el valor que queda después de que se aplica la precedencia de configuración, así que un "disableAllHooks": false en .claude/settings.json de un proyecto anula un true en tu configuración de usuario. Para desactivar hooks para una ejecución sin importar lo que diga la configuración del proyecto, pasa --settings '{"disableAllHooks": true}', que tiene precedencia sobre la configuración de proyecto y local. No hay forma de deshabilitar un hook individual mientras lo mantienes en la configuración.

La configuración disableAllHooks respeta la jerarquía de configuración administrada. Si un administrador ha configurado hooks a través de configuración de política administrada, disableAllHooks establecido en configuración de usuario, proyecto o local no puede deshabilitar esos hooks administrados. Solo disableAllHooks establecido en el nivel de configuración administrada puede deshabilitar hooks administrados. Para el alcance completo de cada nivel, consulta disableAllHooks.

Las ediciones directas a hooks en archivos de configuración normalmente son recogidas automáticamente por el observador de archivos.

Entrada y salida de hooks

Los hooks de comando reciben datos JSON a través de stdin y comunican resultados a través de códigos de salida, stdout y stderr. Los hooks HTTP reciben el mismo JSON como cuerpo de la solicitud POST y comunican resultados a través del cuerpo de la respuesta HTTP. Esta sección cubre campos y comportamiento comunes a todos los eventos. Cada sección de evento bajo Eventos de hook incluye su esquema de entrada específico y opciones de control de decisión.

En macOS y Linux, los hooks de comando se ejecutan en su propia sesión sin una terminal de control. El proceso del hook y cualquier proceso secundario no pueden abrir /dev/tty ni enviar secuencias de escape directamente a la interfaz de Claude Code. Windows no tiene /dev/tty.

Para mostrar un mensaje al usuario en cualquier plataforma, devuelve systemMessage en la salida JSON. Algunos eventos lo descartan o lo entregan en otro lugar, y cada sección de evento lo indica. Para activar una notificación de escritorio, establecer un título de ventana o hacer sonar la campana, devuelve terminalSequence en su lugar.

Campos de entrada comunes

Los eventos de hook reciben estos campos como JSON, además de campos específicos del evento documentados en cada sección de evento de hook. Para hooks de comando, este JSON llega a través de stdin. Para hooks HTTP, llega como el cuerpo de la solicitud POST.

Campo Descripción
session_id Identificador de sesión actual
prompt_id UUID que identifica el prompt del usuario que se está procesando actualmente. Coincide con el atributo prompt.id en eventos de OpenTelemetry, para que puedas correlacionar la salida del hook con la telemetría de un único prompt. Ausente hasta la primera entrada del usuario
transcript_path Ruta al JSON de conversación. El archivo de transcripción se escribe de forma asincrónica y puede rezagarse con respecto a la conversación en memoria, por lo que es posible que aún no incluya los mensajes más recientes del turno actual cuando se activa un hook. Los hooks que necesitan el texto final del asistente del turno actual deben usar last_assistant_message en Stop y SubagentStop en lugar de leer la transcripción
cwd Directorio de trabajo actual cuando se invoca el hook
scratchpad_dir Ruta al directorio de scratchpad de la sesión, donde Claude mantiene archivos de trabajo temporales. Ausente cuando la sesión no tiene scratchpad o el directorio temporal no está disponible. Requiere Claude Code v2.1.257 o posterior
permission_mode Modo de permisos actual: "default", "plan", "acceptEdits", "auto", "dontAsk" o "bypassPermissions". El modo etiquetado como Manual llega como "default", nunca como "manual", por lo que los scripts que coinciden con "default" siguen funcionando. No todos los eventos reciben este campo. Consulta el ejemplo JSON en cada sección de evento de hook
effort Objeto cuyo level es el ajuste de nivel de esfuerzo cuando se ejecuta el hook, incluido el nivel de un skill, de un subagente o de ultrathink: "low", "medium", "high", "xhigh" o "max". Presente para PreToolUse, PostToolUse, Stop, SubagentStop y otros eventos con un contexto de uso de herramienta, en modelos que admiten esfuerzo. Si el modelo no admite tu nivel, level es el nivel que se ejecutó. Un nivel que cambiaste puede aparecer aquí antes de que entre en vigor. $CLAUDE_EFFORT contiene el mismo nivel.
hook_event_name Nombre del evento que se activó

agent_id y agent_type le indican a tu script en qué agente se activó un hook, como un subagente, un compañero en proceso o el agente que elegiste con --agent:

Campo Descripción
agent_id Identificador único del subagente o compañero en proceso en el que se activa el hook.
agent_type Nombre del agente (por ejemplo, "Explore" o "security-reviewer"). Presente cuando la sesión usa --agent o el hook se activa dentro de un subagente. Para subagentes, el tipo del subagente tiene precedencia sobre el valor --agent de la sesión. Consulta SubagentStart para ver los valores que reportan los subagentes personalizados y de plugin y cómo escribir un matcher contra un nombre con alcance de plugin.

Solo los hooks SessionStart pueden recibir un campo model, y Claude Code no siempre lo incluye. Los hooks PreModelSwitch y PostModelSwitch reciben from_model y to_model en su lugar, así que usa un hook PostModelSwitch para seguir el modelo mientras cambia durante una sesión.

No hay variable de entorno $CLAUDE_MODEL. El hook puede leer $ANTHROPIC_MODEL si la estableces en tu shell, pero ese valor no cambia cuando cambias de modelo con /model durante una sesión.

Un proceso de hook hereda el entorno principal, aparte de las variables exportadoras OTEL_* que Claude Code elimina de cada subproceso que genera y, cuando CLAUDE_CODE_SUBPROCESS_ENV_SCRUB se establece en 1, las variables que elimina. En una sesión que recibe la configuración HIPAA, Claude Code también elimina las credenciales de Anthropic del entorno del hook.

Por ejemplo, un hook PreToolUse para un comando Bash recibe esto en stdin:

{
  "session_id": "abc123",
  "prompt_id": "550e8400-e29b-41d4-a716-446655440000",
  "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
  "cwd": "/home/user/my-project",
  "scratchpad_dir": "/tmp/claude-1000/-home-user-my-project/abc123/scratchpad",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite",
    "timeout": 120000,
    "run_in_background": false
  },
  "tool_use_id": "toolu_01ABC123..."
}

Los campos tool_name, tool_input y tool_use_id son específicos del evento. Cada sección de evento de hook documenta los campos adicionales para ese evento.

Salida por código de salida

Cuando termina la ejecución de tu hook, Claude Code usa su código de salida para decidir si continúa con la acción que lo activó, como una llamada a herramienta o un prompt:

  • Éxito: tu hook sale con 0. Claude Code aplica cualquier campo de salida JSON que tu hook haya impreso, y la acción continúa a menos que esos campos la bloqueen o la denieguen. Hooks que inician un proceso en segundo plano cubre la excepción.
  • Error de bloqueo: tu hook sale con 2. En los eventos que pueden bloquear, Claude Code detiene la acción.
  • Error sin bloqueo: tu hook sale con cualquier otro código, o falla de alguna otra manera, como no iniciarse o imprimir JSON no válido. La acción continúa, excepto en los hooks cuyo fallo bloquea la acción, por ejemplo un hook con onFailure establecido en "block". En eventos como PreToolUse, ves un aviso <hook name> hook error en la transcripción.

Lo que tu hook imprime en stdout puede cambiar el resultado. Por ejemplo, cuando un hook PreToolUse sale con 1 pero imprime JSON que pasa la validación, ese JSON todavía puede decidir qué sucede. Para saber el resultado de tu hook en un evento como PreToolUse, busca lo que imprimió en stdout en la primera columna y su código de salida en la parte superior:

Stdout Exit 0 Exit 2 Cualquier otro código de salida
Objeto JSON que pasa la validación del esquema Éxito. Los campos se aplican Error de bloqueo. Claude Code sigue leyendo los campos, pero no pueden sobrescribir el bloqueo Éxito. Claude Code ignora el código de salida, y solo los campos deciden. En los hooks cuyo fallo bloquea la acción, la ejecución cuenta en cambio como un fallo
JSON que no se puede analizar o que falla la validación del esquema Error sin bloqueo. El aviso incluye el mensaje de análisis o de validación Error de bloqueo. Tu stderr es la razón Error sin bloqueo. El aviso incluye el mensaje de análisis o de validación
Texto plano, o nada Éxito Error de bloqueo. Tu stderr es la razón Error sin bloqueo. El aviso incluye la primera línea de tu stderr

Algunos eventos tienen sus propias reglas:

  • WorktreeCreate: cualquier código de salida distinto de cero hace que la creación del worktree falle, diga lo que diga tu JSON.
  • WorktreeRemove: cualquier código de salida distinto de cero hace que la eliminación del worktree falle si el directorio sigue existiendo después.
  • Stop, SubagentStop, TaskCompleted y el hook UserPromptSubmit de un plugin: cuando tu hook sale con 2 sin nada en stdout y su stderr indica que falta un archivo, como No such file or directory, Claude Code trata la ejecución como un error sin bloqueo.
  • Elicitation y ElicitationResult: Claude Code aplica tu hookSpecificOutput cuando tu hook sale con 0, y lo ignora con cualquier otro código de salida.
  • Eventos que descartan la salida del hook, como StopFailure: Claude Code ignora tu JSON con cualquier código de salida, aparte de los campos de efecto secundario como terminalSequence, que se siguen activando.

Para comprobar qué hace el código de salida 2 en tu evento, consulta Comportamiento del código de salida 2 por evento. Para comprobar qué campos de decisión respeta, consulta Control de decisión.

Exit code 0

Exit 0 significa éxito, y es el código de salida previsto cuando imprimes JSON para control estructurado. Cuando tu hook sale con 0 pero imprime JSON que no se puede analizar o que falla la validación del esquema, la acción continúa salvo que el hook sea uno cuyo fallo bloquea la acción. En lugar de un éxito con tus campos aplicados, la ejecución es un error sin bloqueo.

Para corregir tu salida, usa el mensaje de análisis o de validación que incluye el aviso <hook name> hook error. Para un hook que no es async, tu stdout cuenta como salida JSON solo cuando imprimes un único objeto JSON, en una línea o en varias, sin nada alrededor salvo espacios en blanco. Claude Code lee cualquier otro stdout como texto plano o como un fallo de análisis:

  • Salida que no comienza con {, o que comienza con { y no termina con }: texto plano. Según esta regla, un array JSON y una cadena JSON entrecomillada son texto plano.
  • Dos o más líneas que se analizan como JSON cada una por su cuenta, la primera comenzando con { y la última terminando con }: texto plano cuando ninguna línea es un objeto de salida JSON que establezca un campo. Cuando alguna lo es, la salida es un fallo de análisis, así que imprime ese objeto sin nada más en stdout.
  • Cualquier otra cosa que comience con { y termine con } pero no sea JSON válido: un fallo de análisis.

Para la mayoría de los eventos, Claude Code escribe stdout en el registro de depuración y no lo muestra en la transcripción. En estos eventos, tu stdout llega a Claude o al resumen de compactación:

  • UserPromptSubmit, UserPromptExpansion, SessionStart y PostModelSwitch: Claude Code agrega el stdout de texto plano como contexto que Claude puede ver y sobre el que puede actuar. No agrega el stdout que no pudo analizar.
  • PreCompact: tu stdout se convierte en instrucciones para el resumen de compactación

Claude nunca ve el stderr de un hook que sale con 0. Para leerlo tú mismo en eventos como PreToolUse, habilita el registro de depuración. Para mostrarle una advertencia a Claude desde un hook PostToolUse o PostToolUseFailure, sal con 2 en su lugar para que Claude vea el stderr aunque la herramienta ya se haya ejecutado.

Exit code 2

Sal con el código 2 para bloquear la acción. En los eventos que pueden bloquear, Claude Code detiene la acción: un hook PreToolUse bloquea la llamada a herramienta, por ejemplo, y un hook UserPromptSubmit rechaza el prompt.

El mensaje que acompaña al bloqueo es el stderr de tu hook. Si tu hook también imprimió JSON que toma una decisión de bloqueo, Claude Code usa la razón de esa decisión en su lugar.

Exit 2 bloquea incluso cuando tu hook imprime JSON:

  • JSON que pasa la validación del esquema: Claude Code sigue leyendo los campos de salida JSON, pero no pueden sobrescribir el bloqueo. Ni siquiera un permissionDecision de "allow" deja pasar la acción. En Elicitation y ElicitationResult, el hookSpecificOutput de un hook que sale con 2 se ignora.
  • JSON que falla la validación del esquema: el hook sigue bloqueando. Claude Code usa tu stderr como la razón del bloqueo y registra el fallo de validación en el registro de depuración.

Este script bloquea los comandos rm saliendo con 2 y deja todos los demás comandos al flujo de permisos normal:

#!/bin/bash
# Lee entrada JSON desde stdin, verifica el comando
input=$(cat)
command=$(jq -r '.tool_input.command' <<<"$input")

if [[ "$command" == rm* ]]; then
  echo "Blocked: rm commands are not allowed" >&2
  exit 2  # Blocking error: tool call is prevented
fi

exit 0  # No decision: the normal permission flow applies

Con este script registrado como un hook PreToolUse en Bash, un comando que comienza con rm se bloquea, y Claude recibe el stderr del hook como el error de la herramienta, con el prefijo del nombre del evento, el nombre de la herramienta y el comando del hook:

PreToolUse:Bash hook error: [${CLAUDE_PROJECT_DIR}/.claude/hooks/no-rm.sh]: Blocked: rm commands are not allowed

Otros códigos de salida

Un aviso <hook name> hook error con Failed with non-blocking status code: significa que el código de salida de tu hook no fue 0 ni 2 y que su stdout era texto plano o estaba vacío. Salvo que el hook sea uno cuyo fallo bloquea la acción, esto es un error sin bloqueo, por lo que tu hook no detuvo la acción. Para capturar el stderr completo de tu hook en lugar de la primera línea que muestra el aviso, habilita el registro de depuración.

Por ejemplo, cuando un hook PreToolUse en Bash imprime something broke en stderr y sale con 1, el aviso PreToolUse:Bash hook error incluye esta línea:

Failed with non-blocking status code: something broke

Si el aviso incluye el mensaje No such file or directory del shell, como en la siguiente línea, el hook nunca se ejecutó, por ejemplo porque la ruta de su script no existe:

Failed with non-blocking status code: /bin/sh: /path/to/hook.sh: No such file or directory

Si la ruta es incorrecta, corrígela donde está configurado el hook. En la forma de shell, cuando la ruta del script no existe o no es ejecutable, el shell sale con un código como 127 y el aviso incluye el mensaje del intérprete. Salvo que el hook sea uno cuyo fallo bloquea la acción, un hook que no puede iniciarse también es un error sin bloqueo.

Cuando configures un hook de política, fíjate en este aviso en su primera ejecución, porque una ruta mal escrita en settings.json significa que el hook nunca se ejecuta. Para bloquear la acción cuando el hook no puede iniciarse, establece onFailure: "block".

[!WARNING]

El código de salida 1 sin salida JSON válida es un error sin bloqueo, salvo que el hook sea uno cuyo fallo bloquea la acción, aunque 1 es el código de fallo convencional de Unix. Si tu hook está pensado para aplicar una política, usa exit 2.

Tiempos de espera

Aparte de un hook de comando que ejecutes con async: true, Claude Code cancela un hook command, http o mcp_tool que alcanza su timeout, descartando la salida del hook, por lo que en la mayoría de los eventos un hook cuyo tiempo de espera se agota no emite ninguna decisión. En PreModelSwitch, un hook cancelado al agotarse su tiempo de espera bloquea el cambio de modelo.

En PreToolUse, un hook command, http o mcp_tool cuyo tiempo de espera se agota no bloquea la llamada a herramienta, excepto en los hooks cuyo fallo bloquea la acción. La llamada continúa a través del flujo de permisos normal, así que no cuentes con que un hook bloqueado actúe como control. Para bloquear la llamada cuando se agota el tiempo de espera de un hook command o http, establece onFailure: "block". Un hook de callback del Agent SDK que excede su tiempo de espera bloquea la llamada a herramienta.

Hooks que inician un proceso en segundo plano

Un hook puede iniciar un programa de larga duración, como un servidor de desarrollo, y dejarlo en ejecución mientras la sesión continúa. En el script del hook o en el comando en forma de shell:

  • Ejecuta el programa en segundo plano
  • Redirige su stdout y su stderr. Por ejemplo, el comando Bash npm run dev > dev-server.log 2>&1 & envía ambos a un archivo. Si no los rediriges, un hook que no es async tarda hasta 10 segundos en terminar después de que sale su propio proceso.

Si tu hook de comando toma su decisión en un proceso en segundo plano y sale, una denegación que ese proceso imprima después de que Claude Code haya dejado de leer no bloquea la llamada a herramienta. Para un hook que no es async, Claude Code termina de leer la salida del hook poco después de que sale el propio proceso del hook.

Imprime la decisión antes de iniciar un proceso en segundo plano, y redirige el stdout y el stderr de ese proceso. Si tu script imprime un allow y luego inicia un proceso en segundo plano sin redirigir el stdout y el stderr de ese proceso, Claude Code bloquea la llamada a herramienta. El error del hook incluye stdio went quiet y refusing to honor the suspect capture.

Este script de hook PreToolUse imprime la decisión con policy-check.sh y luego sube métricas en segundo plano:

#!/bin/bash
"$CLAUDE_PROJECT_DIR"/.claude/hooks/policy-check.sh || exit $?
"$CLAUDE_PROJECT_DIR"/.claude/hooks/upload-metrics.sh >/dev/null 2>&1 &

El resto del script hace lo siguiente:

  • || exit $?: hace que el código de salida de la verificación sea el del propio hook, de modo que una verificación que sale con 2 para denegar siga bloqueando. De lo contrario, el hook sale con el código de la última línea, que es 0.
  • >/dev/null 2>&1 &: ejecuta upload-metrics.sh en segundo plano con su salida redirigida, para que no mantenga abierta la salida del hook.

Para probar el script, haz que policy-check.sh salga con 2 y luego activa una llamada a herramienta que coincida con el hook. La llamada a herramienta se bloquea.

Bloquear la acción cuando un hook falla

En la mayoría de los eventos, cuando un hook falla o se agota su tiempo de espera, Claude Code sigue llevando a cabo la acción. Así, un hook de política con una ruta incorrecta o un script que se bloquea deja pasar todo. Para bloquear la acción en su lugar, establece "onFailure": "block" en un hook command o http. Requiere Claude Code v2.1.295 o posterior. Para los hooks de callback del Agent SDK, consulta Bloquear la acción cuando falla un callback.

[!NOTE]

En Windows sin Git Bash, un hook de comando PreToolUse o PermissionRequest en forma de shell que falla sin onFailure establecido puede, en cambio, bloquear la llamada a herramienta.

Este hook PreToolUse en .claude/settings.json ejecuta un script del proyecto antes de cada comando Bash, y bloquea el comando si el script falla:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js"],
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

Para probarlo, deja que falte check-command.js y pídele a Claude que ejecute un comando Bash como ls. Claude Code bloquea la llamada, y el error incluye failed; blocking because onFailure is "block" seguido de la salida de error propia de node, recortada aquí a una línea:

PreToolUse:Bash hook error: [node ${CLAUDE_PROJECT_DIR}/.claude/hooks/check-command.js]: failed; blocking because onFailure is "block"
Error: Cannot find module '/path/to/project/.claude/hooks/check-command.js'

Después de que se agota el tiempo de espera, el mensaje dice timed out en lugar de failed. Sin onFailure establecido, el mismo script faltante es un error sin bloqueo y ls se ejecuta.

Cada uno de estos cuenta como un fallo:

  • No puede iniciarse: un hook de comando no logra iniciarse, por ejemplo porque el script o el ejecutable no existe
  • Código de salida distinto de 0 o 2: cuenta para un hook de comando incluso si imprimió JSON que permite la acción, como permissionDecision: "allow". Para devolver una decisión JSON, sal con 0
  • Error HTTP: la conexión de un hook HTTP falla, o el estado de la respuesta no es 2xx
  • Tiempo de espera agotado: el hook alcanza su timeout
  • Salida no válida: la salida JSON no se puede analizar o falla la validación del esquema. Para un hook HTTP, un cuerpo 2xx que no está vacío ni es un objeto JSON también cuenta. El stdout de texto plano de un hook de comando no es un fallo

Con "block" establecido, un fallo hace lo que el código de salida 2 hace en ese evento, excepto en PermissionRequest, donde deniega la solicitud. Por ejemplo, un fallo de PreToolUse bloquea la llamada a herramienta y un fallo de UserPromptSubmit bloquea el prompt.

El campo no tiene efecto en estos hooks:

  • Hooks Stop, SubagentStop, TaskCompleted y TeammateIdle: el código de salida 2 en estos eventos hace que Claude siga trabajando, y Claude no puede reparar un hook que no se ejecuta
  • Hooks de comando en segundo plano: hooks de comando que establecen async o asyncRewake

Hooks fallidos en Windows sin Git Bash

Si el error de una llamada a herramienta bloqueada incluye este texto, un hook de comando PreToolUse o PermissionRequest falló o agotó su tiempo de espera y bloqueó la llamada:

blocking because Git Bash wasn't found, so only exit code 0 or 2 counts as this hook's verdict.

Si el error también dice PowerShell started no program, empieza por las soluciones para un command que solo contiene cadenas entre comillas, que PowerShell imprime en lugar de ejecutar.

Sin Git Bash, cualquier código de salida distinto de 0 o 2 de ese hook cuenta como un fallo, incluso cuando imprime JSON que no deniega la llamada. Para averiguar por qué falló el hook, revisa el registro de depuración. Luego elige la solución que corresponda a tu hook:

  • La llamada debe continuar cuando el hook falla: agrega "onFailure": "continue" al hook
  • El hook sale con 1 para advertir pero debe bloquear cuando se bloquea por un error: con "onFailure": "continue", la llamada continúa con el código de salida 1, tanto si es una advertencia como si es un error. Agrega "onFailure": "block" en su lugar, y haz que el script salga con 0 donde advierte, para que esas llamadas continúen. Para seguir mostrando la advertencia en PreToolUse, imprímela en stdout en el campo systemMessage de un único objeto JSON.
  • El hook está escrito para Bash: sin Git Bash, un hook en forma de shell sin shell establecido se ejecuta en PowerShell, donde puede fallar. Instala Git for Windows, o establece CLAUDE_CODE_GIT_BASH_PATH si Git Bash está instalado en un lugar donde Claude Code no lo encuentra. Luego inicia una nueva sesión.
  • El hook ejecuta un único .exe, no un archivo .cmd o .bat: pon el programa en command y sus argumentos en args para usar la forma exec, y agrega "onFailure": "block". Claude Code entonces inicia el programa sin shell. Sin "onFailure": "block", la llamada puede continuar cuando el programa falla, no existe o agota su tiempo de espera
  • Un hook de protección de PowerShell que debe bloquear cuando falla: con onFailure sin establecer, deja de hacerlo una vez que Git for Windows está instalado y Claude Code encuentra Git Bash. Agrega "onFailure": "block" para que un fallo o un tiempo de espera agotado bloquee la llamada con o sin Git Bash. Establece también "shell": "powershell" para que el hook siga ejecutándose en PowerShell a partir de entonces en lugar de en Git Bash

Antes de v2.1.309, el mensaje decía blocking because Git Bash wasn't found and this hook doesn't set "shell" to "powershell", so only exit code 0 or 2 counts as its verdict.

PowerShell started no program

Un hook con este error se ejecutó en PowerShell, que imprime un command que solo contiene cadenas entre comillas, como "command": "\"C:\\Tools\\guard.exe\"", en lugar de ejecutarlo.

  • Para imprimir texto, como JSON: establece "shell": "powershell", y el hook sale con 0
  • Para ejecutar un archivo .ps1, .cmd o .bat, o un .exe cuya ruta contiene una variable: establece "shell": "powershell" y pon & antes de la ruta entre comillas. Deja $HOME tal como está escrito, y escribe las variables de entorno como el ejemplo de check.ps1 escribe $env:CLAUDE_PROJECT_DIR
  • Para ejecutar un único .exe en una ruta sin $: quita las comillas internas de command y luego aplica la solución para un hook que ejecuta un único .exe, escribiendo "args": [] si el programa no recibe argumentos
  • Para ejecutar un script escrito para Bash: aplica la solución para un hook escrito para Bash

Comportamiento del código de salida 2 por evento

El código de salida 2 es la forma en que un hook señala "detente, no hagas esto". El efecto depende del evento, porque algunos eventos representan acciones que pueden bloquearse (como una llamada a herramienta que aún no ha sucedido) y otros representan cosas que ya sucedieron o no pueden evitarse.

Evento de hook ¿Puede bloquear? Qué sucede con exit 2
PreToolUse Sí Bloquea la llamada a herramienta
PermissionRequest No El código de salida 2 no se respeta para este evento y el flujo de permisos continúa sin cambios. Deniega a través del objeto decision en su lugar
UserPromptSubmit Sí Bloquea el prompt, por lo que nunca llega a Claude. Consulta Qué deja atrás un prompt bloqueado
UserPromptExpansion Sí Bloquea la expansión
Stop Sí Evita que Claude se detenga, continúa la conversación
SubagentStop Sí Evita que el subagente se detenga
TeammateIdle Sí Evita que el compañero quede inactivo, por lo que sigue trabajando
TaskCreated Sí Revierte la creación de la tarea
TaskCompleted Sí Evita que la tarea se marque como completada
ConfigChange Sí Impide que el cambio de configuración tenga efecto (excepto policy_settings)
StopFailure No La salida y el código de salida se ignoran, excepto terminalSequence
PostToolUse No Muestra stderr a Claude; la herramienta ya se ejecutó
PostToolUseFailure No Muestra stderr a Claude; la herramienta ya falló
PostToolBatch Sí Detiene el bucle agéntico antes de la siguiente llamada al modelo
PermissionDenied No El código de salida y stderr se ignoran porque la denegación ya ocurrió. Usa JSON hookSpecificOutput.retry: true para indicarle al modelo que puede reintentar; Claude Code ignora retry: true para denegaciones sin veredicto
Notification No El código de salida y stderr se ignoran
SubagentStart No Muestra stderr solo al usuario
SessionStart No Muestra stderr solo al usuario
Setup No El código de salida y stderr se ignoran
SessionEnd No Muestra stderr solo al usuario
CwdChanged No Muestra stderr solo al usuario
DirectoryAdded No Stderr va al registro de depuración; el directorio ya está agregado
FileChanged No Muestra stderr solo al usuario
PreCompact Sí Bloquea la compactación
PostCompact No Muestra stderr solo al usuario
PreModelSwitch Sí Bloquea el cambio de modelo y muestra stderr al usuario
PostModelSwitch No Muestra stderr solo al usuario; el modelo ya cambió
Elicitation Sí Rechaza la solicitud, y no aparece ningún diálogo
ElicitationResult Sí Bloquea la respuesta (la acción se convierte en decline)
WorktreeCreate Sí Cualquier código de salida distinto de cero hace que la creación del worktree falle
WorktreeRemove Sí Cualquier código de salida distinto de cero hace que la eliminación del worktree falle si el directorio sigue existiendo después. Consulta WorktreeRemove para saber qué sucede con el directorio
InstructionsLoaded No El código de salida se ignora
MessageDisplay No Se muestra el texto original

Para SessionStart, SubagentStart y PostModelSwitch, Claude Code muestra el stderr del código de salida 2 en la transcripción como un aviso <hook name> hook error, de la misma manera que muestra un error sin bloqueo. Claude no lo ve, y la sesión o el subagente continúa. Para SubagentStart, el aviso aparece en la propia transcripción del subagente, no en la conversación principal.

Manejo de respuestas HTTP

Los hooks HTTP usan códigos de estado HTTP y cuerpos de respuesta en lugar de códigos de salida y stdout. Para bloquear la acción cuando la solicitud falla o devuelve un estado que no es 2xx, establece onFailure: "block". Para bloquear una llamada a herramienta o denegar un permiso, devuelve una respuesta 2xx con un cuerpo JSON que contenga los campos de decisión apropiados.

Después de un error sin bloqueo, la acción continúa salvo que el hook sea uno cuyo fallo bloquea la acción. Excepto en los eventos con su propio contrato de fallo en la tabla por evento, como WorktreeCreate, obtienes estos resultados para cada respuesta o fallo:

  • 2xx con un cuerpo vacío: éxito, equivalente al código de salida 0 sin salida
  • 2xx con un cuerpo de objeto JSON: se analiza usando el mismo esquema de salida JSON que los hooks de comando. Un cuerpo que falla la validación del esquema es un error sin bloqueo
  • 2xx con cualquier otro cuerpo, como texto plano: error sin bloqueo, manejado igual que un estado que no es 2xx. Claude Code no agrega el texto al contexto de Claude
  • Estado que no es 2xx: error sin bloqueo
  • Fallo de conexión: error sin bloqueo
  • Tiempo de espera agotado: el hook se cancela, como se describe en Tiempos de espera

Salida JSON

Los códigos de salida solo te permiten bloquear o permanecer en silencio, pero la salida JSON te da un control más granular. En lugar de salir con código 2 para bloquear, sal con 0 e imprime un objeto JSON en stdout. Claude Code lee campos específicos de ese JSON para controlar el comportamiento, incluido el control de decisión para bloquear, permitir o escalar al usuario.

[!NOTE]

Elige un enfoque por hook: usa solo códigos de salida para señalizar, o sal con 0 e imprime JSON para control estructurado. Si los mezclas, exit 2 mantiene su efecto de bloqueo, y Claude Code sigue leyendo los campos JSON, con la única excepción de elicitación indicada en Exit code 2.

No imprimas nada en stdout salvo el objeto JSON. Para un hook que no es async, cualquier otro texto ahí, como una línea que tu perfil de shell imprime al iniciarse, impide que Claude Code lea el objeto como JSON, y El JSON del hook no tiene efecto muestra cómo encontrar y silenciar ese texto.

Las cadenas additionalContext, systemMessage e initialUserMessage de un hook, y su stdout plano, están limitadas a 10.000 caracteres:

  • Alcance: Claude Code mide cada cadena por separado, incluso cuando varios hooks se ejecutan para el mismo evento. En la salida JSON, cada campo se mide por separado; el stdout plano se mide completo.
  • Por encima del límite: excepto para el stdout de PreCompact, Claude Code guarda la salida en un archivo en el directorio de la sesión. Reemplaza la salida con la ruta del archivo y una vista previa de hasta los primeros 2.000 caracteres. Un resultado de Bash válido y grande se maneja de la misma manera, como se describe en Límites de salida. A diferencia de ese tope de Bash, este límite no tiene ningún ajuste ni variable de entorno para aumentarlo.
  • Lectura del archivo: Claude Code no le pide a Claude que lea el archivo, así que mantén dentro del límite todo lo que Claude deba ver siempre.

El objeto JSON admite tres tipos de campos:

  • Campos universales como continue, que se enumeran en la tabla a continuación. Todos los eventos los aceptan, pero algunos eventos los descartan o entregan systemMessage en un lugar distinto de la transcripción. La sección de cada evento lo indica. terminalSequence también funciona en esos eventos, con las excepciones enumeradas en Emitir notificaciones de terminal.
  • decision y reason de nivel superior, que algunos eventos usan para bloquear o proporcionar retroalimentación.
  • hookSpecificOutput, un objeto anidado para eventos que necesitan un control más rico. Requiere un campo hookEventName establecido en el nombre del evento.
Campo Predeterminado Descripción
continue true Si es false, Claude detiene el procesamiento por completo después de que se ejecuta el hook. Tiene precedencia sobre cualquier campo de decisión específico del evento
stopReason ninguno Mensaje mostrado al usuario cuando continue es false. Permanece en la conversación, por lo que Claude lo ve si la conversación continúa
suppressOutput false No tiene efecto: Claude Code acepta el campo pero no actúa sobre él. El stdout de un hook exitoso nunca se muestra en la transcripción y se registra en el registro de depuración
systemMessage ninguno Mensaje de advertencia mostrado al usuario. En la salida del Agent SDK y de --output-format stream-json, puede llegar como un SDKInformationalMessage
terminalSequence ninguno Una secuencia de escape de terminal para que Claude Code la emita en tu nombre, como una notificación de escritorio, un título de ventana o una campana. Restringida a OSC 0/1/2/9/99/777 y BEL. Si el valor contiene algo fuera de la lista de permitidos, el campo se ignora. Usa esto en lugar de escribir en /dev/tty, que no está disponible para los hooks

Para detener a Claude por completo:

{ "continue": false, "stopReason": "Build failed, fix errors before continuing" }

Para los hooks PreToolUse y PostToolUse, la detención se aplica incluso cuando la llamada a herramienta falla o se completa mientras Claude todavía está transmitiendo una respuesta en streaming.

Emitir notificaciones de terminal

Los hooks se ejecutan sin una terminal de control, por lo que escribir secuencias de escape directamente en /dev/tty falla. En su lugar, devuelve la secuencia de escape en el campo terminalSequence y Claude Code la emite por ti a través de su propia ruta de escritura en la terminal. Esto está libre de condiciones de carrera y funciona en Windows, donde no hay /dev/tty.

El campo acepta una cadena de una o más secuencias de escape permitidas:

  • OSC 0, 1, 2: títulos de ventana e icono
  • OSC 9: notificaciones de iTerm2, ConEmu, Windows Terminal y WezTerm, incluido el progreso de la barra de tareas 9;4
  • OSC 99: notificaciones de Kitty
  • OSC 777: notificaciones de urxvt, Ghostty y Warp
  • BEL sin más

Las secuencias pueden terminar con BEL o con ST. Cualquier cosa fuera de la lista de permitidos, incluidas las secuencias CSI de cursor y color, las secuencias OSC de paleta, los hipervínculos OSC 8, las escrituras al portapapeles OSC 52 y OSC 1337, se rechaza y el campo se ignora.

Claude Code escribe la secuencia por sí mismo cuando procesa la salida de tu hook, por lo que el campo funciona en eventos que descartan systemMessage y continue, como Notification y StopFailure. Tiene algunas limitaciones:

  • Claude Code escribe la secuencia solo en una sesión interactiva, y solo mientras su interfaz está abierta. En modo no interactivo con el flag -p y en el Agent SDK, ignora el campo.
  • El campo solo funciona desde un hook que no se ejecuta en segundo plano, así que deja desactivados async y asyncRewake.
  • Un hook de comando WorktreeCreate no puede devolver JSON, porque Claude Code lee su stdout como la ruta del worktree. Un hook HTTP WorktreeCreate devuelve JSON y puede incluir el campo.

Si usas tmux o GNU screen, completa primero la configuración para ellos. El ejemplo a continuación dispara una notificación de escritorio desde un hook Notification en una terminal que lee OSC 777, como Ghostty o Warp. La llamada jq -nc --arg del script construye la salida JSON y escapa las comillas, barras invertidas y saltos de línea del mensaje de notificación:

#!/bin/bash
# Hook de notificación: ping al escritorio cuando Claude Code necesita atención.
input=$(cat)
title="Claude Code"
body=$(jq -r '.message // "Needs your attention"' <<<"$input")
seq=$(printf '\033]777;notify;%s;%s\007' "$title" "$body")
jq -nc --arg seq "$seq" '{terminalSequence: $seq}'

La forma { "terminalSequence": "..." } es la misma desde cualquier shell o lenguaje.

Recibir notificaciones de terminal dentro de tmux o GNU screen

Cuando Claude Code se ejecuta dentro de tmux o GNU screen, las secuencias OSC necesitan lo siguiente para llegar a tu aplicación de terminal:

  • tmux 3.3 o posterior: agrega set -g allow-passthrough on a ~/.tmux.conf y luego ejecuta tmux source-file ~/.tmux.conf desde un shell. La opción se aplica a todos los programas de esos paneles, no solo a Claude Code. Con on, solo los paneles que tmux está mostrando pueden enviarlas. Para permitir que también las envíen otros paneles de una sesión a la que estás conectado, usa all en lugar de on, en tmux 3.4 o posterior.
  • GNU screen: mantén visible la ventana de Claude Code en screen, porque las secuencias de una ventana en segundo plano no llegan a tu aplicación de terminal

Agregar contexto para Claude

El campo additionalContext pasa una cadena desde tu hook a la ventana de contexto de Claude. Claude Code envuelve la cadena en un recordatorio del sistema y la inserta en la conversación en el punto donde se activó el hook. Claude lee el recordatorio en la siguiente solicitud al modelo, pero no aparece como un mensaje de chat en la interfaz.

Devuelve additionalContext dentro de hookSpecificOutput junto al nombre del evento:

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "This file is generated. Edit src/schema.ts and run `bun generate` instead."
  }
}

Dónde aparece el recordatorio depende del evento:

Cuando varios hooks devuelven additionalContext para el mismo evento, Claude recibe todos los valores.

Si tu cadena contiene una etiqueta <system-reminder> o </system-reminder>, Claude recibe la cadena con el < de esa etiqueta reemplazado por &lt;.

Si un valor supera los 10.000 caracteres, Claude Code escribe el texto en un archivo en el directorio de la sesión y, en su lugar, le pasa a Claude la ruta del archivo con una vista previa de hasta los primeros 2.000 caracteres. Claude puede leer el archivo, pero Claude Code no se lo pide.

Usa additionalContext para información que Claude debe conocer sobre el estado actual de tu entorno o sobre la operación que acaba de ejecutarse:

  • Estado del entorno: la rama actual, el destino de despliegue o los feature flags activos
  • Reglas de proyecto condicionales: qué comando de prueba se aplica al archivo que se acaba de editar, qué directorios son de solo lectura en este worktree
  • Datos externos: issues abiertos asignados a ti, resultados recientes de CI, contenido obtenido de un servicio interno

Para instrucciones que nunca cambian, prefiere CLAUDE.md. Se carga sin ejecutar un script y es el lugar estándar para las convenciones estáticas del proyecto.

Escribe el texto como afirmaciones de hechos en lugar de instrucciones de sistema imperativas. Frases como "El destino de despliegue es producción" o "Este repositorio usa bun test" se leen como información del proyecto. El texto planteado como comandos de sistema fuera de banda puede activar las defensas contra la inyección de prompts de Claude, lo que hace que Claude te muestre el texto en lugar de tratarlo como contexto.

Claude Code guarda el texto inyectado en la transcripción de la sesión. Para eventos a mitad de sesión como PostToolUse o UserPromptSubmit, cuando reanudas con --continue o --resume, Claude Code reproduce el texto guardado en lugar de volver a ejecutar el hook para los turnos anteriores, por lo que valores como marcas de tiempo o SHAs de commits quedan obsoletos. Los hooks SessionStart se ejecutan de nuevo al reanudar con source establecido en "resume", o en "fork" si agregaste --fork-session, por lo que pueden actualizar su contexto.

Control de decisión

No todos los eventos admiten bloquear o controlar el comportamiento mediante JSON. Los que sí lo admiten usan cada uno un conjunto distinto de campos para expresar esa decisión. Usa esta tabla como referencia rápida antes de escribir un hook:

Eventos Patrón de decisión Campos clave
UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, ConfigChange, PreCompact decision de nivel superior decision: "block", reason. Stop y SubagentStop también aceptan hookSpecificOutput.additionalContext para retroalimentación sin error que continúa la conversación
TeammateIdle, TaskCompleted Código de salida o continue: false El código de salida 2 bloquea la acción con retroalimentación de stderr. JSON {"continue": false, "stopReason": "..."} también detiene al compañero por completo, igual que el comportamiento del hook Stop; TaskCompleted lo ignora cuando la herramienta TaskUpdate activó el evento
TaskCreated Código de salida o decision de nivel superior El código de salida 2 o decision: "block" cancela la tarea y devuelve el mensaje a Claude. continue: false se ignora
PreToolUse hookSpecificOutput permissionDecision (allow/deny/ask/defer), permissionDecisionReason
PreModelSwitch hookSpecificOutput o decision de nivel superior permissionDecision (allow/deny/ask), permissionDecisionReason. decision: "block" también cancela el cambio
PermissionRequest hookSpecificOutput decision.behavior (allow/deny)
PermissionDenied hookSpecificOutput retry: true le indica al modelo que puede reintentar la llamada a herramienta denegada; Claude Code lo ignora para denegaciones sin veredicto
WorktreeCreate devolución de ruta El hook de comando imprime la ruta en stdout; el hook HTTP devuelve hookSpecificOutput.worktreePath. Un fallo del hook o la falta de ruta hace fallar la creación
WorktreeRemove Código de salida Cualquier código de salida distinto de cero hace que la eliminación falle si el directorio sigue existiendo después. La salida JSON se descarta
Elicitation, ElicitationResult hookSpecificOutput o decision de nivel superior action (accept/decline/cancel), content (valores de los campos del formulario). decision: "block" también rechaza
MessageDisplay hookSpecificOutput displayContent reemplaza el texto mostrado en pantalla. Solo afecta a la visualización: la transcripción y lo que Claude ve conservan el original
SessionStart, SubagentStart, PostModelSwitch Solo contexto hookSpecificOutput.additionalContext agrega contexto para Claude. SessionStart también acepta initialUserMessage, watchPaths, sessionTitle y reloadSkills. Sin bloqueo ni control de decisión
Setup, Notification, SessionEnd, PostCompact, InstructionsLoaded, StopFailure, CwdChanged, DirectoryAdded, FileChanged Ninguno Sin control de decisión. Se usan para efectos secundarios como registro o limpieza

Algunos eventos también pueden reescribir contenido en lugar de solo permitirlo o bloquearlo:

  • PreToolUse: updatedInput directamente bajo hookSpecificOutput reemplaza los argumentos de una herramienta antes de que se ejecute. Consulta Control de decisión de PreToolUse
  • PermissionRequest: updatedInput dentro del objeto decision. Consulta Control de decisión de PermissionRequest
  • PostToolUse: updatedToolOutput reemplaza el resultado de la herramienta. En una sesión en la nube de Cowork, Claude Code ignora updatedToolOutput cuando la herramienta es AskUserQuestion o ExitPlanMode, y Claude lee la respuesta o el plan originales. Consulta Control de decisión de PostToolUse
  • UserPromptSubmit: no puede reemplazar el prompt; solo inyecta additionalContext junto a él

Para casos de uso de redacción o transformación, intercepta en PreToolUse las entradas salientes de herramientas y en PostToolUse los resultados entrantes de herramientas.

Aquí hay ejemplos de cada patrón en acción:

[tab] Decisión de nivel superior

El único valor para decision es "block". Para permitir que la acción continúe, omite decision de tu JSON, o sal con 0 sin ningún JSON:

{
  "decision": "block",
  "reason": "Test suite must pass before proceeding"
}

[tab] PreToolUse

Usa hookSpecificOutput para un control más rico: permitir, denegar o escalar al usuario. También puedes modificar la entrada de la herramienta antes de que se ejecute o inyectar contexto adicional para Claude. Consulta Control de decisión de PreToolUse para ver el conjunto completo de opciones.

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database writes are not allowed"
  }
}

[tab] PermissionRequest

Usa hookSpecificOutput para permitir o denegar una solicitud de permiso en nombre del usuario. Al permitir, también puedes modificar la entrada de la herramienta o aplicar reglas de permisos para que no se le vuelva a preguntar al usuario. Consulta Control de decisión de PermissionRequest para ver el conjunto completo de opciones.

{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow",
      "updatedInput": {
        "command": "npm run lint"
      }
    }
  }
}

Para ejemplos más extensos, incluidos la validación de comandos Bash, el filtrado de prompts y scripts de aprobación automática, consulta Qué puedes automatizar en la guía y la implementación de referencia del validador de comandos Bash.

Eventos de hooks

Cada evento corresponde a un punto del ciclo de vida de Claude Code en el que pueden ejecutarse hooks. Las secciones siguientes están ordenadas según el ciclo de vida: desde la preparación de la sesión, pasando por el bucle agéntico, hasta el final de la sesión. Cada sección describe cuándo se dispara el evento, qué matchers admite, la entrada JSON que recibe y cómo controlar el comportamiento mediante la salida.

SessionStart

Se ejecuta cuando Claude Code inicia una sesión nueva o reanuda una sesión existente. Es útil para cargar contexto de desarrollo, como issues existentes o cambios recientes en tu base de código, o para configurar variables de entorno. Para contexto estático que no requiere un script, usa CLAUDE.md en su lugar.

SessionStart se ejecuta en cada sesión, así que mantén estos hooks rápidos. Solo se admiten hooks type: "command" y type: "mcp_tool". Consulta Campos de hooks de herramientas MCP para saber cuándo se ejecutan los hooks mcp_tool.

El valor del matcher corresponde a cómo se inició la sesión:

Matcher Cuándo se dispara
startup Sesión nueva
resume --resume, --continue o /resume
clear /clear
compact Compactación automática o manual
fork Una sesión nueva bifurcada de una existente: --fork-session con --resume o --continue, la copia en segundo plano de /fork, /branch o una conversación que mueves al segundo plano

Cuando inicias una sesión interactiva, reanudas una conversación al iniciar con --continue o --resume, o ejecutas /clear, los hooks SessionStart se ejecutan en segundo plano. Puedes escribir de inmediato, y una conversación que reanudaste aparece sin esperar a los hooks. La primera respuesta de Claude sigue esperando a que terminen los hooks, para que su contexto llegue a Claude.

Cuando cambias de conversación con /resume dentro de una sesión, el cambio espera a que terminen los hooks. Si ejecutas /clear o cambias a otra conversación mientras los hooks en segundo plano siguen ejecutándose, nada de lo que devuelvan se aplica a la sesión.

La misma espera se aplica al iniciar, incluida una sesión reanudada: un prompt que envíes mientras los hooks SessionStart siguen ejecutándose no llega a Claude hasta que terminen.

Durante cualquiera de las dos esperas, presiona Esc para recuperar el prompt en la entrada sin enviarlo. Los hooks siguen ejecutándose.

Entrada de SessionStart

Además de los campos de entrada comunes, los hooks SessionStart reciben source y, opcionalmente, model, agent_type y session_title:

Campo Descripción
source Cómo se inició la sesión: "startup" para sesiones nuevas, "resume" para sesiones reanudadas, "clear" después de /clear, "compact" después de la compactación o "fork" para una sesión nueva bifurcada de una existente
model El identificador del modelo activo. Puede omitirse, por ejemplo después de /clear o cuando una sesión se restaura mediante la recuperación de conversaciones, así que comprueba que el campo exista antes de leerlo
agent_type El nombre del agente, presente cuando inicias Claude Code con claude --agent <name>
session_title El título personalizado de la sesión, presente cuando hay uno establecido, por ejemplo con --name, /rename, la salida sessionTitle de un hook o renameSession() del Agent SDK. Un hook que emite sessionTitle puede comprobar primero este campo para evitar sobrescribir un título personalizado existente

Una sesión a la que no le has puesto nombre puede tener igualmente un título generado. Ese título no es un título personalizado y no aparece en session_title.

Cuando source es "resume" o "fork" y la transcripción contiene al menos una respuesta de Claude, los hooks SessionStart también reciben los cuatro campos siguientes. Tu hook puede usarlos para informar cuánto cuesta reanudar una conversación antigua antes de la primera solicitud, por ejemplo en un systemMessage. Estos campos requieren Claude Code v2.1.251 o posterior.

Campo Descripción
seconds_since_last_response Segundos de tiempo real transcurridos desde la última respuesta en la transcripción reanudada
context_tokens Tokens que la primera solicitud de la sesión reanudada vuelve a enviar como su prompt
prompt_cache_likely_expired true cuando la última respuesta es más antigua que la vida útil de la caché de prompts de la sesión o una compactación posterior reemplazó la conversación almacenada en caché
estimated_cache_write_usd Costo estimado en dólares estadounidenses de escribir context_tokens en la caché de prompts con el modelo de la sesión, sin incluir la respuesta

Este ejemplo muestra la entrada de una sesión reanudada 90 minutos después de su última respuesta:

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "SessionStart",
  "source": "resume",
  "model": "claude-opus-5",
  "seconds_since_last_response": 5400,
  "context_tokens": 182340,
  "prompt_cache_likely_expired": true,
  "estimated_cache_write_usd": 1.1396
}

Control de decisiones de SessionStart

Un hook SessionStart puede agregar contexto para Claude, proporcionar el primer mensaje del usuario, establecer el título de la sesión, vigilar archivos y recargar skills. Devuelve el campo correspondiente a cada uno, además de los campos de salida JSON disponibles para todos los hooks:

Campo Descripción
additionalContext Cadena que se agrega al contexto de Claude al comienzo de la conversación, antes del primer prompt. Consulta Agregar contexto para Claude para saber cómo se entrega el texto y qué incluir en él
initialUserMessage Cadena que se usa como primer mensaje del usuario de la sesión, en modo no interactivo con el flag -p. Se convierte en el primer turno aunque no pases ningún prompt. Un prompt que sí pases sigue como el siguiente turno
sessionTitle Establece el título de la sesión, con el mismo efecto que /rename. Se aplica cuando source es "startup", "resume" o "fork"
watchPaths Array de rutas absolutas que se vigilan para eventos FileChanged durante esta sesión
reloadSkills Booleano. Cuando es true, Claude Code vuelve a escanear los directorios de skills y comandos después de que terminen los hooks SessionStart. Consulta Recargar los skills que instala un hook

Esta salida agrega contexto y le pone nombre a la sesión:

{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Current branch: feat/auth-refactor\nUncommitted changes: src/auth.ts, src/login.tsx\nActive issue: #4211 Migrate to OAuth2",
    "sessionTitle": "auth-refactor"
  }
}

Un hook que solo agrega contexto puede imprimirlo sin construir JSON, porque Claude Code agrega la salida stdout en texto plano de un hook SessionStart al contexto de Claude.

Si el hook SessionStart de tu plugin proporciona initialUserMessage o sessionTitle, instala el plugin antes de que comience la sesión. Claude Code ignora ambos campos de un plugin que termina de instalarse después de que se hayan ejecutado los hooks SessionStart.

Recargar los skills que instala un hook

Para que los skills que instala un hook SessionStart estén disponibles en la misma sesión, devuelve reloadSkills. El descubrimiento de skills normalmente se ejecuta antes de que terminen los hooks SessionStart, así que sin él, los archivos que un hook escribe en ~/.claude/skills/ o .claude/skills/ pueden faltar cuando se ejecuta el primer prompt.

Este ejemplo sincroniza un repositorio compartido de skills y solicita el nuevo escaneo:

#!/bin/bash

git -C ~/.claude/skills/team-skills pull --quiet 2>/dev/null || \
  git clone --quiet https://git.example.com/your-org/team-skills.git ~/.claude/skills/team-skills

echo '{"hookSpecificOutput": {"hookEventName": "SessionStart", "reloadSkills": true}}'

La URL del repositorio es un marcador de posición. Reemplázala por tu propio repositorio de skills.

Conservar variables de entorno

Los hooks SessionStart tienen acceso a la variable de entorno CLAUDE_ENV_FILE, que proporciona una ruta de archivo donde puedes conservar variables de entorno para los comandos de shell que Claude ejecute más adelante en la sesión.

Para establecer variables de entorno individuales, escribe sentencias export en CLAUDE_ENV_FILE. Usa la adición (>>) para conservar las variables establecidas por otros hooks:

#!/bin/bash

if [ -n "$CLAUDE_ENV_FILE" ]; then
  echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
  echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
  echo 'export PATH="$PATH:./node_modules/.bin"' >> "$CLAUDE_ENV_FILE"
fi

exit 0

Para capturar todos los cambios de entorno de los comandos de preparación, compara las variables exportadas antes y después:

#!/bin/bash

ENV_BEFORE=$(export -p | sort)

# Run your setup commands that modify the environment
source ~/.nvm/nvm.sh
nvm use 20

if [ -n "$CLAUDE_ENV_FILE" ]; then
  ENV_AFTER=$(export -p | sort)
  comm -13 <(echo "$ENV_BEFORE") <(echo "$ENV_AFTER") >> "$CLAUDE_ENV_FILE"
fi

exit 0

Cada comando de Bash ejecuta el contenido del archivo como código de shell antes del propio comando, así que una línea allí puede usar cualquier cosa que Bash evalúe, como la referencia a $PATH en export PATH="$PATH:./node_modules/.bin".

Variables conservadas en comandos de PowerShell

Los comandos de PowerShell también reciben las variables de CLAUDE_ENV_FILE, en Claude Code v2.1.296 o posterior, pero PowerShell nunca ejecuta el archivo. En su lugar, Claude Code lee las asignaciones del archivo y las copia en el entorno del comando de PowerShell. Solo lo hace cuando cada línea es de alguno de los siguientes tipos, considerando todo lo que escribió cada hook en esta sesión y cualquier script al que hayas establecido CLAUDE_ENV_FILE antes de iniciar:

  • Una línea en blanco o un comentario #
  • Una asignación al comienzo de la línea, escrita como export NAME=value, declare -x NAME=value o NAME=value, con un valor que Bash usaría exactamente tal como está escrito, formado por cualquier combinación de estas partes: texto sin comillas que use solo letras, dígitos y los caracteres _ @ % + = : , . / -, texto entre comillas simples y texto entre comillas dobles en el que cualquier $, acento grave o " interno esté escapado con una barra invertida

Si alguna línea es de otro tipo, como export PATH="$PATH:./node_modules/.bin" con su $PATH sin escapar, un comando source o las cadenas $'...' que imprime direnv export bash, los comandos de PowerShell no reciben ninguna de las variables, y claude --debug registra Session environment is not all plain assignments. Los comandos de Bash siguen recibiéndolas todas. En Windows, los comandos de PowerShell tampoco reciben una variable cuyo valor contenga / o \, porque Git Bash y Windows escriben las rutas de forma diferente. Un comando de PowerShell en el sandbox no recibe ninguna de las variables.

[!NOTE]

CLAUDE_ENV_FILE está disponible para los hooks SessionStart, Setup, CwdChanged y FileChanged. Otros eventos de hooks no tienen acceso a esta variable, y tampoco un hook que se ejecuta en PowerShell, ya sea mediante "shell": "powershell" o de forma predeterminada en Windows sin Git Bash.

Setup

Se dispara solo cuando inicias Claude Code con --init-only, o con --init o --maintenance en modo no interactivo con el flag -p. No se dispara en el inicio normal. Úsalo para la instalación única de dependencias o para la limpieza programada que activas explícitamente desde CI o scripts, de forma separada del inicio normal de la sesión. Para la inicialización por sesión, usa SessionStart en su lugar.

El valor del matcher corresponde al flag de la CLI que activó el hook:

Matcher Cuándo se dispara
init claude --init-only o claude -p --init
maintenance claude -p --maintenance

Cuando ejecutas claude --init-only, Claude Code ejecuta los hooks Setup y los hooks SessionStart con el matcher startup, y luego sale sin iniciar una conversación.

Cuando inicias o continúas una conversación con -p, también necesitas proporcionar un prompt, como argumento o canalizado por stdin. Puedes omitir el prompt cuando un hook SessionStart proporciona initialUserMessage o cuando reanudas una sesión con una llamada a herramienta diferida.

Si tiene éxito, --init-only no imprime nada en la terminal. Para confirmar que los hooks se ejecutaron, inicia con claude --debug-file <path> --init-only, reemplazando <path> por la ubicación de un archivo de log, y revisa el registro en busca de las entradas de los hooks Setup y SessionStart.

Como Setup no se dispara en cada inicio, un plugin que necesita instalar una dependencia no puede depender solo de Setup. El patrón práctico es comprobar la dependencia en el primer uso e instalarla si falta; por ejemplo, un hook o skill que comprueba si existe ${CLAUDE_PLUGIN_DATA}/node_modules y ejecuta npm install si no está. Consulta el directorio de datos persistentes para saber dónde almacenar las dependencias instaladas. Si distribuyes tu plugin a través de un marketplace, es posible que no necesites este patrón: Claude Code instala automáticamente las dependencias de paquetes de Node.js que cumplan los requisitos cuando almacena el plugin en caché.

Entrada de Setup

Además de los campos de entrada comunes, los hooks Setup reciben un campo trigger con el valor "init" o "maintenance":

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "Setup",
  "trigger": "init"
}

Control de decisiones de Setup

Los hooks Setup no pueden bloquear; la ejecución continúa con cualquier código de salida. Con cualquier código de salida, Claude Code descarta los campos de salida JSON de un hook Setup, como systemMessage, continue y hookSpecificOutput.additionalContext. Con -p, el stdout, el stderr y el código de salida de un hook Setup aparecen en la salida de la ejecución solo como eventos hook_response cuando inicias con --output-format stream-json --verbose.

Los hooks Setup tienen acceso a CLAUDE_ENV_FILE. Si tu hook escribe variables en ese archivo, estas se conservan en los comandos de Bash posteriores de la sesión, igual que en los hooks SessionStart. Solo los hooks type: "command" se ejecutan en Setup. Un hook type: "mcp_tool" en Setup siempre se omite, como se describe en Campos de hooks de herramientas MCP.

InstructionsLoaded

Se dispara cuando se carga un archivo CLAUDE.md o .claude/rules/*.md en el contexto. Este evento se dispara al inicio de la sesión para los archivos cargados de forma anticipada y de nuevo más tarde cuando los archivos se cargan de forma diferida, por ejemplo cuando Claude accede a un subdirectorio que contiene un CLAUDE.md anidado o cuando coinciden reglas condicionales con frontmatter paths:. El hook no admite bloqueo ni control de decisiones. Se ejecuta de forma asíncrona con fines de observabilidad.

Este evento no se dispara cuando Claude lee AGENTS.md directamente mediante el ajuste Project instructions. Sí se dispara cuando un CLAUDE.md importa tu AGENTS.md, con load_reason establecido en include como para cualquier otro archivo importado, y cuando CLAUDE.md es un enlace simbólico a él, como una carga normal de CLAUDE.md.

El matcher se evalúa contra load_reason. Por ejemplo, usa "matcher": "session_start" para que se dispare solo con los archivos cargados al inicio de la sesión, o "matcher": "path_glob_match|nested_traversal" para que se dispare solo con las cargas diferidas.

Entrada de InstructionsLoaded

Además de los campos de entrada comunes, los hooks InstructionsLoaded reciben estos campos:

Campo Descripción
file_path Ruta absoluta al archivo de instrucciones que se cargó
memory_type Alcance del archivo: "User", "Project", "Local" o "Managed"
load_reason Por qué se cargó el archivo: "session_start", "nested_traversal", "path_glob_match", "include" o "compact". El valor "compact" se produce cuando los archivos de instrucciones se vuelven a cargar después de un evento de compactación
globs Patrones glob de ruta del frontmatter paths: del archivo, si los hay. Presente solo en las cargas path_glob_match
trigger_file_path Ruta al archivo cuyo acceso activó esta carga, en las cargas diferidas
parent_file_path Ruta al archivo de instrucciones padre que incluyó este, en las cargas include
{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
  "cwd": "/Users/my-project",
  "hook_event_name": "InstructionsLoaded",
  "file_path": "/Users/my-project/CLAUDE.md",
  "memory_type": "Project",
  "load_reason": "session_start"
}

Control de decisiones de InstructionsLoaded

Los hooks InstructionsLoaded no tienen control de decisiones. No pueden bloquear ni modificar la carga de instrucciones. Claude Code descarta sus campos de salida JSON, como systemMessage y continue. Usa este evento para registros de auditoría, seguimiento de cumplimiento u observabilidad.

UserPromptSubmit

Se ejecuta cuando se envía un prompt, antes de que Claude lo procese. Esto te permite agregar contexto adicional basado en el prompt o la conversación, validar prompts o bloquear ciertos tipos de prompts.

Los hooks UserPromptSubmit no se disparan solo con los prompts que escribes. Claude Code también los ejecuta cuando:

Los hooks UserPromptSubmit tienen un tiempo de espera predeterminado de 30 segundos para los tipos command, http y mcp_tool, más corto que el valor predeterminado de 600 segundos para esos tipos en la mayoría de los demás eventos. Como este hook se ejecuta antes de cada prompt y bloquea el procesamiento del modelo hasta que termina, un hook atascado detiene la sesión. Si tu hook necesita más tiempo, establece el campo timeout en la entrada del hook.

Salvo un hook de comando que ejecutes con async: true, un hook UserPromptSubmit de comando, HTTP o herramienta MCP que alcanza su tiempo de espera se cancela y su salida, incluido cualquier additionalContext, se descarta. El prompt sigue llegando a Claude sin ese contexto. Para bloquear el prompt en su lugar, establece onFailure: "block" en un hook de comando o HTTP. La transcripción muestra un aviso que nombra el hook, el tiempo de espera que se agotó e indica que la salida se descartó.

Un hook de callback del Agent SDK en UserPromptSubmit al que se le agota el tiempo de espera bloquea el prompt con un mensaje que nombra el hook y el tiempo de espera, porque un callback en ese punto puede estar actuando como una barrera de políticas que no debe fallar de forma abierta. La sesión continúa.

Esc durante la comprobación de un prompt

Si presionas Esc o interrumpes la sesión mientras los hooks siguen comprobando un prompt, la comprobación se detiene y el prompt no llega a Claude. El texto que escribiste en la terminal vuelve al campo de entrada del prompt, y los hooks lo comprueban de nuevo cuando lo vuelves a enviar.

Un hook puede bloquear el prompt en el mismo momento en que interrumpes. En ese caso el prompt sigue bloqueado: ves el mensaje de bloqueo de ese hook, y el texto no vuelve al campo de entrada.

Entrada de UserPromptSubmit

Además de los campos de entrada comunes, los hooks UserPromptSubmit reciben el campo prompt que contiene el texto enviado. El contenido pegado que se contrajo en un marcador [Pasted text #N] llega expandido en su lugar. En las sesiones donde Claude Code marca el texto pegado para Claude, ese contenido expandido se encuentra entre una línea <pasted_content id="…"> y una línea </pasted_content id="…">, así que ten en cuenta esas líneas si tu hook analiza el prompt.

Los hooks UserPromptSubmit también reciben session_title cuando la sesión tiene un título personalizado, con el mismo significado que el campo session_title de SessionStart.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "Write a function to calculate the factorial of a number"
}

Cuando el prompt es un skill o un comando personalizado que escribiste, como /deploy staging, el hook recibe esa línea tal como la escribiste y no el contenido expandido del skill. Si el hook bloquea el prompt, el comando no se ejecuta, así que los comandos de shell que el skill inyecta con !`<command>` tampoco se ejecutan. Para hacer coincidir por el nombre o los argumentos del comando, usa UserPromptExpansion.

Control de decisiones de UserPromptSubmit

Los hooks UserPromptSubmit pueden controlar si se procesa un prompt enviado y agregar contexto. Todos los campos de salida JSON están disponibles.

Hay dos formas de agregar contexto a la conversación con el código de salida 0:

  • Stdout en texto plano: Claude Code agrega al contexto de Claude el stdout que trata como texto plano
  • JSON con additionalContext: usa el formato JSON siguiente para tener más control. El campo additionalContext se agrega como contexto

Ninguno de los dos canales produce una entrada visible en la transcripción. El stdout plano y el valor de additionalContext se inyectan cada uno como un recordatorio del sistema que comienza con el nombre del hook; Claude lee ambos. Para confirmar la entrega, revisa el registro de depuración.

Para bloquear un prompt, devuelve un objeto JSON con decision establecido en "block":

Campo Descripción
decision "block" detiene el prompt antes de que llegue a Claude. Omítelo para permitir que el prompt continúe
reason Se muestra al usuario cuando decision es "block". No se agrega al contexto
additionalContext Cadena que se agrega al contexto de Claude junto con el prompt enviado. Consulta Agregar contexto para Claude
sessionTitle Establece el título de la sesión. Úsalo para nombrar sesiones automáticamente según el contenido del prompt
suppressOriginalPrompt Si es true cuando el hook bloquea el prompt, deja el texto del prompt fuera del mensaje de bloqueo. Consulta Lo que deja un prompt bloqueado

Un hook que bloquea saliendo con 2 se enruta igual que reason: el mensaje de bloqueo muestra el texto de stderr al usuario, y no se agrega al contexto.

{
  "decision": "block",
  "reason": "Explanation for decision",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "My additional context here",
    "sessionTitle": "My session title",
    "suppressOriginalPrompt": true
  }
}

Lo que deja un prompt bloqueado

Un prompt bloqueado nunca llega a Claude, pero su texto no se elimina de todas partes. De forma predeterminada, el mensaje de bloqueo que se muestra al usuario termina con Original prompt: seguido del texto enviado, y Claude Code escribe ese mensaje en el archivo de transcripción de la sesión en el disco. Para dejar el texto fuera del mensaje, imprime JSON con "suppressOriginalPrompt": true dentro de hookSpecificOutput. Esto funciona tanto si el hook bloquea con decision: "block" como si lo hace saliendo con 2.

suppressOriginalPrompt solo cambia el mensaje de bloqueo. El texto enviado puede seguir apareciendo en archivos locales, como la transcripción de la sesión y tu historial de prompts, así que un hook de bloqueo no es una forma de mantener un secreto fuera del disco. Para limitar o eliminar esos archivos, consulta Almacenamiento en texto plano y Borrar datos locales.

UserPromptExpansion

Se ejecuta cuando un comando escrito por el usuario se expande en un prompt antes de llegar a Claude. Úsalo para impedir que ciertos comandos se invoquen directamente, inyectar contexto para un skill concreto o registrar qué comandos invocan los usuarios. Por ejemplo, un hook que coincide con deploy puede bloquear /deploy a menos que exista un archivo de aprobación, o un hook que coincide con un skill de revisión puede anexar la lista de verificación de revisión del equipo como additionalContext.

Este evento cubre la ruta que PreToolUse no cubre: un hook PreToolUse que coincide con la herramienta Skill solo se dispara cuando Claude llama a la herramienta, pero escribir /skillname directamente omite PreToolUse. UserPromptExpansion se dispara en esa ruta directa.

Coincide con command_name. Deja el matcher vacío para que se dispare con cada comando de tipo prompt.

Entrada de UserPromptExpansion

Además de los campos de entrada comunes, los hooks UserPromptExpansion reciben expansion_type, command_name, command_args, command_source y la cadena prompt original. El campo expansion_type es slash_command para skills y comandos personalizados, o mcp_prompt para prompts de servidores MCP.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../00893aaf.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "UserPromptExpansion",
  "expansion_type": "slash_command",
  "command_name": "example-skill",
  "command_args": "arg1 arg2",
  "command_source": "plugin",
  "prompt": "/example-skill arg1 arg2"
}

Control de decisiones de UserPromptExpansion

Los hooks UserPromptExpansion pueden bloquear la expansión o agregar contexto. Todos los campos de salida JSON están disponibles.

Campo Descripción
decision "block" impide que el comando se expanda. Omítelo para permitir que continúe
reason Se muestra al usuario cuando decision es "block"
additionalContext Cadena que se agrega al contexto de Claude junto con el prompt expandido. Consulta Agregar contexto para Claude

Un hook que bloquea saliendo con 2 se enruta igual que reason: el mensaje de bloqueo muestra el texto de stderr al usuario.

{
  "decision": "block",
  "reason": "This slash command is not available",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptExpansion",
    "additionalContext": "Additional context for this expansion"
  }
}

MessageDisplay

Se ejecuta mientras un mensaje del asistente se transmite a la pantalla. Claude Code muestra el mensaje por incrementos: cada vez que un lote de líneas recién completadas está listo para renderizarse, el hook se ejecuta una vez con esas líneas y Claude Code renderiza en su lugar el texto de reemplazo del hook. Un mensaje largo produce varias llamadas; un mensaje corto puede producir solo una.

Usa MessageDisplay para:

  • quitar el markdown para una visualización minimalista
  • transformar el texto que una aplicación del Agent SDK muestra a sus usuarios
  • ocultar claves de API o nombres de host internos de las respuestas de Claude

Claude Code retiene cada lote hasta que tu hook devuelve un resultado, así que mantén el hook rápido. Si el hook falla o se agota su tiempo de espera, Claude Code muestra el texto original. El tiempo de espera predeterminado para este evento es de 10 segundos; si tu hook necesita más tiempo, establece el campo timeout en la entrada del hook.

MessageDisplay es solo de visualización: el texto de reemplazo cambia únicamente lo que se renderiza en pantalla. La transcripción y lo que ve Claude conservan el texto original, así que Claude nunca ve el reemplazo, y el modo detallado muestra el original. El hook recibe solo el texto de los mensajes del asistente, así que los resultados de herramientas y el texto que escribes se renderizan sin cambios.

MessageDisplay no admite matchers y se dispara con cada mensaje del asistente que transmite texto; los mensajes sin texto, como las respuestas que solo contienen llamadas a herramientas, no lo activan.

En las ejecuciones no interactivas, incluidas las consultas del Agent SDK y claude -p, MessageDisplay se ejecuta una vez por mensaje del asistente en lugar de una vez por lote de líneas. La única llamada llega después de que el mensaje se completa y contiene el texto completo del mensaje: index es 0, final es true y delta contiene el mensaje entero. Un hook que recopila el texto de delta de cada mensaje recibe el mismo texto total en ambos modos.

Entrada de MessageDisplay

Además de los campos de entrada comunes, los hooks MessageDisplay reciben identificadores del turno y del mensaje, la posición de esta llamada dentro del mensaje y el texto nuevo en delta. Los límites de los lotes dependen de cómo se transmite el texto, así que usa index y final para seguir el avance a lo largo de un mensaje en lugar de esperar que las líneas se agrupen de una manera concreta.

Campo Descripción
turn_id UUID del turno actual
message_id UUID del mensaje del asistente que se está mostrando. Es estable en todos los lotes del mismo mensaje. No es el id msg_… de la API, así que no puede correlacionarse con los ids de mensajes de la transcripción
index Índice, empezando en cero, de este lote dentro del mensaje
final true en el último lote del mensaje. Cada mensaje tiene exactamente un lote final
delta Las líneas recién completadas desde el lote anterior, incluidos los saltos de línea finales. Siempre son líneas completas, excepto el lote final, que puede terminar a mitad de línea. En las ejecuciones interactivas, el delta del lote final está vacío cuando el mensaje termina con un salto de línea, así que trata final, y no un delta no vacío, como la señal de fin de mensaje. En las ejecuciones del Agent SDK y claude -p, la única llamada contiene el mensaje completo
{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
  "cwd": "/Users/my-project",
  "hook_event_name": "MessageDisplay",
  "turn_id": "0c9e6a2f-7d41-4f4e-9a15-3f4f7c2b8d10",
  "message_id": "5b2a9c8e-1f63-4d8a-b7c4-9e0d2a6f1c3b",
  "index": 0,
  "final": false,
  "delta": "Here is the plan:\n"
}

Salida de MessageDisplay

Además de los campos de salida JSON disponibles para todos los hooks, los hooks MessageDisplay pueden devolver displayContent para reemplazar el delta en pantalla:

Campo Descripción
displayContent Texto que se muestra en lugar del delta. Omítelo para mostrar el original

Los hooks MessageDisplay no tienen control de decisiones. No pueden bloquear el mensaje ni cambiar lo que se almacena en la transcripción o se envía a Claude. Claude Code actúa sobre el displayContent de su salida JSON y descarta systemMessage y continue.

Este ejemplo quita el formato markdown de las respuestas de Claude para una visualización en texto plano. El script lee cada lote desde stdin, elimina los marcadores de negrita y los acentos graves del código en línea de delta y devuelve el resultado como displayContent.

[tab] macOS/Linux

Registra un hook de comando para el evento en tu archivo de configuración:

{
  "hooks": {
    "MessageDisplay": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

Guarda este script en .claude/hooks/plain-display.sh en tu proyecto y hazlo ejecutable con chmod +x:

#!/bin/bash
jq '{hookSpecificOutput: {hookEventName: "MessageDisplay", displayContent: (.delta | gsub("\\*\\*"; "") | gsub("`"; ""))}}'

[tab] Windows (PowerShell)

Registra un hook de comando que ejecute el script mediante PowerShell:

{
  "hooks": {
    "MessageDisplay": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "powershell.exe",
            "args": [
              "-NoProfile",
              "-ExecutionPolicy",
              "Bypass",
              "-File",
              "${CLAUDE_PROJECT_DIR}/.claude/hooks/plain-display.ps1"
            ]
          }
        ]
      }
    ]
  }
}

El flag -NoProfile omite la carga de tu perfil de PowerShell para que el hook se inicie rápido, y -ExecutionPolicy Bypass permite que PowerShell ejecute el archivo de script local.

Guarda este script en .claude/hooks/plain-display.ps1 en tu proyecto:

$batch = [Console]::In.ReadToEnd() | ConvertFrom-Json
$text = $batch.delta -replace '\*\*', '' -replace '`', ''
@{
  hookSpecificOutput = @{
    hookEventName = "MessageDisplay"
    displayContent = $text
  }
} | ConvertTo-Json

Los lotes sin markdown pasan sin cambios. Si el script falla, por ejemplo porque falta jq, Claude Code muestra el texto original e indica el fallo solo en la salida de depuración, no en la sesión.

PreToolUse

Se ejecuta después de que Claude crea los parámetros de la herramienta y antes de procesar la llamada a herramienta. Coincide con cualquier nombre de herramienta excepto EndConversation: herramientas integradas como Bash, PowerShell, Edit, Write, Read, Glob, Grep, Agent, Workflow, WebFetch, WebSearch, AskUserQuestion y ExitPlanMode, y cualquier nombre de herramienta MCP.

Para ejecutar un hook cuando un archivo concreto cambia en el disco, sin importar qué lo haya escrito, usa FileChanged en lugar de hacer coincidir por nombre las herramientas de edición de archivos. A diferencia de PreToolUse, Claude Code ejecuta los hooks FileChanged después del cambio, y no tienen control de decisiones, así que no pueden bloquear la escritura.

[!WARNING]

PreToolUse solo se ejecuta cuando Claude llama a una herramienta. De forma predeterminada, los archivos que referencias con @ en tu prompt se agregan sin ninguna llamada a herramienta. Claude Code inserta su contenido al construir el prompt, así que no se dispara ningún hook PreToolUse para ellos, incluidos los hooks que coinciden con Read. PreToolUse tampoco se dispara para EndConversation.

Puedes bloquear rutas concretas de las referencias @ o desactivar la inserción:

  • Bloquear rutas concretas: agrega una regla de denegación de Read para ellas
  • Desactivar la inserción: establece CLAUDE_CODE_DISABLE_FILE_MENTIONS=1 en tu shell, en la configuración de usuario o en la configuración administrada. Claude recibe entonces las rutas mencionadas con una nota para leerlas con sus herramientas de archivos. Si lo hace, se ejecutan tus hooks PreToolUse que coincidan. La variable requiere Claude Code v2.1.299 o posterior, y Claude Code la ignora en la configuración de proyecto y local.

Usa el control de decisiones de PreToolUse para permitir, denegar, preguntar o diferir la llamada a herramienta.

Un hook de callback del Agent SDK en PreToolUse que supera su tiempo de espera bloquea la llamada a herramienta.

Entrada de PreToolUse

Además de los campos de entrada comunes, los hooks PreToolUse reciben tool_name, tool_input y tool_use_id.

Para una herramienta MCP, la entrada también incluye mcp_server, un objeto con el name del servidor y un source que indica de dónde proviene la definición del servidor. Los valores de source incluyen plugin, sdk y alcances de configuración como user y project. McpServerProvenance en la referencia del Agent SDK los enumera todos e indica cómo tratar uno que no reconozcas. Basa las decisiones de confianza en source en lugar de en name o en el prefijo de nombre de herramienta mcp__<server>__. El campo mcp_server requiere Claude Code v2.1.274 o posterior.

Para las herramientas de archivos Write, Edit y Read, tool_input.file_path siempre es absoluto:

  • Claude Code expande ~ y las rutas relativas antes de que se ejecuten los hooks, así que un hook que hace coincidir rutas no puede eludirse mediante ~ o una forma relativa de escribir la misma ruta
  • En Windows, la ruta llega con barras invertidas como separadores, incluso cuando tu hook se ejecuta en Git Bash, donde $PWD tiene el aspecto de /c/project
  • Una comparación escrita con barras normales, como una comprobación de /src/, nunca coincide con una ruta con barras invertidas, y la llamada a herramienta continúa como si el hook no tuviera nada que bloquear
  • Normaliza los separadores antes de comparar: FILE_PATH="${FILE_PATH//\\//}" en Bash, o file_path.replace("\\", "/") en Python, y luego haz coincidir un segmento de ruta como /src/ en lugar de anclar con ^, ya que la ruta es absoluta

Una llamada a Write en Windows entrega:

{
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "C:\\project\\src\\index.ts",
    "content": "..."
  },
  ...
}

Los campos de tool_input dependen de la herramienta:

Bash

Ejecuta comandos de shell.

Campo Tipo Ejemplo Descripción
command string "npm test" El comando de shell que se ejecutará
description string "Run test suite" Descripción opcional de lo que hace el comando
timeout number 120000 Tiempo de espera opcional en milisegundos. Los valores por encima del máximo se reducen al máximo en lugar de rechazarse
run_in_background boolean false Si se ejecuta el comando en segundo plano

Cuando un comando de Bash cambia archivos en un repositorio de Git, Claude Code puede registrar qué cambió. Registra los cambios en todos los modos de permisos cuando el ajuste bashEditDiffEnabled activa el registro; la entrada de ese ajuste indica qué archivos pueden establecerlo. De lo contrario, solo los registra en el modo automático y en el modo bypassPermissions, y solo cuando Claude Code indica a Claude que edite archivos mediante Bash. Establece bashEditDiffEnabled en false para desactivar el registro. Los comandos en segundo plano y los comandos de solo lectura no incluyen diff.

Tu hook PostToolUse recibe entonces los archivos cambiados en tool_response.bashEditDiff. La lista abarca lo que cambió dentro del repositorio mientras se ejecutaba el comando. Los archivos que Git ignora y los archivos de submódulos no se incluyen. Requiere Claude Code v2.1.269 o posterior.

[!NOTE]

La lista se genera en la medida de lo posible y está en beta pública. Claude Code puede pasar por alto un cambio, incluir un archivo que otro proceso cambió al mismo tiempo o detenerse en sus límites de tamaño. La forma del campo puede cambiar. Usa la lista para encontrar qué revisar, no para aplicar una política.

changedFiles y files enumeran lo que cambió el comando; los campos restantes indican qué tan completa y qué tan fiable es esa lista.

Campo Tipo Ejemplo Descripción
changedFiles array ["/path/to/src/app.ts"] Rutas absolutas de los archivos que cambió el comando, 200 como máximo. Presente siempre que files contenga un diff o moreFiles sea mayor que cero
files array [{"filePath": "/path/to/src/app.ts", "hunks": [...]}] Diffs de hasta 5 archivos cambiados, para visualización. created o deleted es true para un archivo que el comando agregó o eliminó
moreFiles number 2 Cantidad de archivos cambiados sin diff en files
unavailable boolean true Se establece cuando el diff está incompleto o no se pudo obtener
skipped boolean true Se establece para un comando de Git que mueve el árbol de trabajo, como git checkout o git stash, por lo que Claude Code no obtiene ningún diff
shared boolean true Se establece cuando otra llamada a la herramienta Bash, como la de un subagente, se ejecutó en el mismo repositorio al mismo tiempo, por lo que algunos cambios enumerados pueden ser de ese comando
PowerShell

Ejecuta comandos de PowerShell. Consulta la herramienta PowerShell para conocer la disponibilidad por plataforma.

Los campos coinciden con los de la herramienta Bash, con la cadena del comando en command:

Campo Tipo Ejemplo Descripción
command string "Get-ChildItem -Recurse" El comando de PowerShell que se ejecutará
description string "List files recursively" Descripción opcional de lo que hace el comando
timeout number 120000 Tiempo de espera opcional en milisegundos
run_in_background boolean false Si se ejecuta el comando en segundo plano

Usa Bash|PowerShell como matcher en los hooks que inspeccionan comandos de shell, para que cubran ambas herramientas:

  • En Windows, siempre que la herramienta PowerShell esté habilitada, Claude trata PowerShell como el shell principal y dirige los comandos de shell a través de él.
  • En Windows sin Git Bash, la herramienta se habilita automáticamente y Claude Code no registra la herramienta Bash en absoluto.
  • Un hook que coincide solo con Bash nunca se dispara allí.
Write

Crea o sobrescribe un archivo.

Campo Tipo Ejemplo Descripción
file_path string "/path/to/file.txt" Ruta absoluta al archivo que se escribirá
content string "file content" Contenido que se escribirá en el archivo
Edit

Reemplaza una cadena en un archivo existente.

Campo Tipo Ejemplo Descripción
file_path string "/path/to/file.txt" Ruta absoluta al archivo que se editará
old_string string "original text" Texto que se buscará y reemplazará
new_string string "replacement text" Texto de reemplazo
replace_all boolean false Si se reemplazan todas las apariciones
Read

Lee el contenido de un archivo.

Campo Tipo Ejemplo Descripción
file_path string "/path/to/file.txt" Ruta absoluta al archivo que se leerá
offset number 10 Número de línea opcional desde el cual empezar a leer
limit number 50 Número opcional de líneas que se leerán
Glob

Busca archivos que coincidan con un patrón glob.

Campo Tipo Ejemplo Descripción
pattern string "**/*.ts" Patrón glob con el que se comparan los archivos
path string "/path/to/dir" Directorio opcional donde buscar. De forma predeterminada, el directorio de trabajo actual
Grep

Busca en el contenido de los archivos con expresiones regulares.

Campo Tipo Ejemplo Descripción
pattern string "TODO.*fix" Patrón de expresión regular que se buscará
path string "/path/to/dir" Archivo o directorio opcional donde buscar
glob string "*.ts" Patrón glob opcional para filtrar archivos
output_mode string "content" "content", "files_with_matches" o "count". De forma predeterminada, "files_with_matches"
-i boolean true Búsqueda sin distinguir mayúsculas de minúsculas
multiline boolean false Habilita la coincidencia multilínea
WebFetch

Obtiene y procesa contenido web.

Campo Tipo Ejemplo Descripción
url string "https://example.com/api" URL de la que se obtendrá el contenido
prompt string "Extract the API endpoints" Prompt que se ejecutará sobre el contenido obtenido
offset number 100000 Número opcional de caracteres que se omitirán desde el inicio de la página. Claude lo establece para seguir leyendo una página larga. Requiere Claude Code v2.1.290 o posterior

Busca en la web.

Campo Tipo Ejemplo Descripción
query string "react hooks best practices" Consulta de búsqueda
allowed_domains array ["docs.example.com"] Opcional: incluye solo resultados de estos dominios
blocked_domains array ["spam.example.com"] Opcional: excluye resultados de estos dominios
mode string "extended" Opcional: tipo de búsqueda. "standard" solicita una búsqueda más ligera y "extended", la búsqueda completa. Algunas sesiones no envían mode, y una llamada sin él ejecuta la búsqueda completa. Requiere Claude Code v2.1.282 o posterior
Agent

Genera un subagente.

Campo Tipo Ejemplo Descripción
prompt string "Find all API endpoints" La tarea que realizará el agente
description string "Find API endpoints" Descripción breve de la tarea
subagent_type string "Explore" Tipo de agente especializado que se usará
model string "sonnet" Alias de modelo opcional para sobrescribir el predeterminado

Cuando se completa una llamada a Agent en primer plano, tu hook PostToolUse recibe el resultado del subagente y la telemetría de la ejecución en tool_response. Lee estos campos para inspeccionar la ejecución; para los totales de tokens y costos entre subagentes, usa los contadores de tokens y costos filtrados por query_source "subagent", ya que totalTokens y usage solo cubren la solicitud final:

Campo Tipo Ejemplo Descripción
status string "completed" "completed" para subagentes en primer plano, "async_launched" para subagentes en segundo plano. Para saber qué subagentes se ejecutan de cada forma, consulta Ejecutar subagentes en primer o segundo plano
agentId string "a4d2c8f1e0b3a297" Identificador de la ejecución del subagente
content array [{"type": "text", "text": "Found 12 endpoints..."}] Los bloques de texto finales del subagente o, en el caso de un subagente cuyo informe pasa por SubagentHandback, una nota breve sobre esa entrega en su lugar
resolvedModel string "claude-sonnet-4-5" Modelo con el que comenzó el subagente, que puede diferir del modelo solicitado
modelsUsed array ["claude-sonnet-4-5", "claude-haiku-4-5"] Modelos usados en orden, con las repeticiones consecutivas contraídas; solo se establece cuando el modelo se cambió a mitad de la ejecución
totalTokens number 12450 Cantidad de tokens de la solicitud final a la API del subagente: tokens de entrada, de salida y de caché combinados. No es un total de toda la ejecución
totalDurationMs number 48211 Duración en tiempo real de la ejecución del subagente
totalToolUseCount number 7 Cantidad de llamadas a herramientas que hizo el subagente
usage object {"input_tokens": 8320, ...} Desglose de tokens por tipo de la solicitud final a la API: input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens

En Claude Code v2.1.271 o posterior, un subagente que se ejecuta con la herramienta SubagentHandback, que Claude Code proporciona en el modo automático, entrega su informe mediante esa herramienta en lugar de devolverlo como texto. El campo content de su resultado completed contiene entonces una nota breve sobre esa entrega en lugar del informe en sí. Para leer el informe, haz que un hook PreToolUse o PostToolUse coincida con SubagentHandback y lee tool_input.message.

Para los subagentes en segundo plano, la herramienta devuelve un resultado cuando la tarea pasa al segundo plano, así que tool_response no incluye campos de uso: un lanzamiento en segundo plano devuelve el resultado de inmediato, y una tarea en primer plano que Claude Code pasa al segundo plano a mitad de la ejecución lo devuelve en esa transición. Tiene status: "async_launched", agentId, description, prompt, outputFile y resolvedModel.

En una respuesta completed, resolvedModel indica el modelo con el que comenzó el subagente, que puede diferir del valor model de tool_input, por ejemplo cuando se aplica availableModels u otra sobrescritura. En una respuesta async_launched, resolvedModel indica el modelo en uso cuando el agente pasó a segundo plano, así que un cambio que se produjo antes de pasar a segundo plano se refleja ahí.

AskUserQuestion

Le hace al usuario de una a cuatro preguntas de opción múltiple.

Campo Tipo Ejemplo Descripción
questions array [{"question": "Which framework?", "header": "Framework", "options": [{"label": "React", "description": "Component library"}, {"label": "Vue", "description": "Progressive framework"}], "multiSelect": false}] Preguntas que se presentarán, cada una con una cadena question, un header breve, un array options y un flag multiSelect opcional
answers object {"Which framework?": "React"} Opcional. Asigna el texto de cada pregunta a la etiqueta de la opción seleccionada. Las respuestas de selección múltiple unen las etiquetas con comas. Claude no establece este campo; proporciónalo mediante updatedInput para responder de forma programática
ExitPlanMode

Presenta un plan y le pide al usuario que lo apruebe antes de que Claude salga del modo plan. Claude escribe el plan en un archivo en el disco antes de llamar a la herramienta, así que el tool_input literal del modelo suele estar vacío. Claude Code inyecta el contenido del plan y la ruta del archivo antes de pasar la entrada a los hooks.

Campo Tipo Ejemplo Descripción
plan string "## Refactor auth\n1. Extract..." Contenido del plan en Markdown. Se inyecta desde el archivo del plan en el disco
planFilePath string "/Users/.../plans/refactor-auth.md" Ruta al archivo del plan. Se inyecta
allowedPrompts array [{"tool": "Bash", "prompt": "run tests"}] Obsoleto. Claude Code acepta el campo pero lo ignora. Antes de la v2.1.205, contenía permisos basados en prompts que Claude solicitaba para implementar el plan

En PostToolUse, tool_response es un objeto con los campos plan y filePath que contienen el plan aprobado, además de flags de estado internos. Lee tool_response.plan para obtener el contenido del plan en lugar de volver a leer el archivo desde el disco.

Control de decisiones de PreToolUse

Los hooks de PreToolUse pueden controlar si una llamada a herramienta continúa. A diferencia de otros hooks que usan un campo decision de nivel superior, PreToolUse devuelve su decisión dentro de un objeto hookSpecificOutput. Esto le da un control más rico: cuatro resultados (permitir, denegar, preguntar o diferir), además de la capacidad de modificar la entrada de la herramienta antes de la ejecución. Para permitir o reescribir un comando de Monitor, consulta Hacer coincidir comandos de Monitor.

Campo Descripción
permissionDecision "allow" omite la solicitud de permiso, excepto para las acciones que ningún modo aprueba automáticamente y las lecturas desde rutas de red. Tampoco omite la aprobación de llamadas a herramientas que una aplicación del Agent SDK marca como always_ask, a menos que el modo de permisos apruebe la llamada. AskUserQuestion y ExitPlanMode necesitan updatedInput junto con "allow". Para el modo plan, consulta Aprobaciones de hooks en modo plan. "deny" impide la llamada a herramienta. "ask" pide al usuario que confirme. "defer" termina el turno para que la herramienta pueda reanudarse más adelante, y requiere el modo no interactivo con el flag -p. Las reglas de denegación y de pregunta se siguen evaluando independientemente de lo que devuelva el hook
permissionDecisionReason Para "ask", se muestra al usuario en la solicitud de permiso. Cuando Claude Code deniega la llamada en una ejecución -p en la que nadie puede responder esa solicitud, Claude lee el motivo en el resultado de la herramienta. Para "deny", se muestra a Claude. Para "allow" y "defer", solo se escribe en el registro de depuración
updatedInput Modifica los parámetros de entrada de la herramienta antes de la ejecución. Reemplaza el objeto de entrada completo, así que incluye los campos sin cambios junto con los modificados. Claude Code evalúa las reglas de permisos y la aptitud para pasar automáticamente a segundo plano de un comando de Bash con la entrada que devuelve tu hook, no con la entrada que envió Claude. Combínalo con "allow" para aprobar automáticamente, o con "ask" para mostrar la entrada modificada al usuario. Para "defer", se ignora. Sin permissionDecision, el modo automático puede denegar la llamada modificada
additionalContext Cadena que se agrega al contexto de Claude junto con el resultado de la herramienta. Se ignora cuando permissionDecision es "defer". Consulta Agregar contexto para Claude

Cuando varios hooks PreToolUse devuelven decisiones diferentes, la precedencia es deny > defer > ask > allow.

Un hook que bloquea saliendo con 2 se enruta igual que "deny": Claude ve el mensaje de stderr como el motivo de la denegación.

Cuando un hook devuelve "ask", la solicitud de permiso que se muestra al usuario incluye una etiqueta que identifica de dónde proviene el hook: [settings] para un hook de cualquier archivo de configuración o del frontmatter de un agente, [plugin:<name>] para el hook de un plugin o [skill] para un hook del frontmatter de un skill. Esto ayuda a los usuarios a entender qué fuente de configuración solicita la confirmación.

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "My reason here",
    "updatedInput": {
      "field_to_modify": "new value"
    },
    "additionalContext": "Current environment: production. Proceed with caution."
  }
}

[!NOTE]

PreToolUse usaba anteriormente los campos de nivel superior decision y reason, pero están obsoletos para este evento. Usa hookSpecificOutput.permissionDecision y hookSpecificOutput.permissionDecisionReason en su lugar. Los valores obsoletos "approve" y "block" se asignan a "allow" y "deny", respectivamente. Otros eventos como PostToolUse y Stop siguen usando decision y reason de nivel superior como su formato actual.

updatedInput de PreToolUse en modo automático

Cuando tu hook devuelve updatedInput sin permissionDecision en el modo automático, Claude Code puede denegar la llamada modificada. El servidor revisó la entrada que escribió Claude, no la entrada que devolvió tu hook. En las sesiones interactivas de terminal y en las sesiones de VS Code con conexión directa a la API de Anthropic, la denegación incluye este texto:

a hook changed this call's input after auto mode reviewed it

Después de una denegación con este texto, Claude puede volver a emitir la llamada modificada para que el servidor la revise. Si Claude repite exactamente la entrada modificada en su siguiente respuesta, tu hook se sigue ejecutando:

  • Puede denegar la llamada o enviarla a la solicitud de permiso
  • Cualquier updatedInput que devuelva se descarta, así que lo que se ejecuta es lo que revisó el servidor

No necesitas cambiar tu hook por esta denegación, a menos que Claude te indique en su respuesta que el modo automático no pudo comprobar la llamada.

En las sesiones que muestran este texto, un hook command en la configuración o en el archivo de hooks de un plugin puede cambiar las llamadas a Bash, PowerShell, Monitor, Edit, Write y NotebookEdit sin esta denegación. La denegación puede seguir apareciendo, por ejemplo cuando algún hook cambió una llamada anterior en la misma respuesta de Claude o cuando un comando de Bash pide ejecutarse fuera del sandbox.

Decisiones ask de PreToolUse en modo automático

En el modo automático, el "ask" de un hook PreToolUse normalmente envía la llamada a herramienta a la solicitud de permiso, y el clasificador no la decide. Algunas llamadas siguen otro camino:

  • Llamadas que también señalan las propias comprobaciones de Claude Code: incluyen escrituras en rutas protegidas y comandos que piden ejecutarse fuera del sandbox. El clasificador puede denegar una de estas llamadas antes de que aparezca cualquier solicitud. Si la aprueba, la llamada normalmente sigue pasando a la solicitud.
  • Llamadas a SubagentHandback: esta herramienta entrega el informe de un subagente. No aparece ninguna solicitud para ella, y la llamada se ejecuta aunque el clasificador se oponga.

Para detener una llamada a SubagentHandback, devuelve "deny". Claude Code le indica entonces al subagente, hasta tres veces, que su informe no se entregó y que vuelva a llamar a la herramienta. Si tu hook sigue denegando, el subagente termina sin entregar un informe mediante esta herramienta, y la conversación que lo espera recibe una nota que lo indica.

Herramientas que requieren interacción del usuario

AskUserQuestion y ExitPlanMode requieren interacción del usuario. En el modo no interactivo con el flag -p, Claude Code las ofrece solo cuando la ejecución tiene un host de permisos que reciba la solicitud, como un callback canUseTool del Agent SDK.

Un hook PreToolUse cumple ese requisito cuando hace lo siguiente:

  1. Lee la entrada de la herramienta desde stdin
  2. Recopila la respuesta mediante tu propia interfaz
  3. Devuelve permissionDecision: "allow" junto con un updatedInput que contiene la respuesta, para que la herramienta se ejecute sin pedir confirmación

Devolver solo "allow" no es suficiente para estas herramientas.

Para AskUserQuestion, devuelve el array questions original y agrega un objeto answers que asigne el texto de cada pregunta a la respuesta elegida. Esta salida responde una pregunta con React:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": {
      "questions": [
        {
          "question": "Which framework?",
          "header": "Framework",
          "options": [{"label": "React", "description": "Component library"}, {"label": "Vue", "description": "Progressive framework"}],
          "multiSelect": false
        }
      ],
      "answers": {"Which framework?": "React"}
    }
  }
}

Una herramienta MCP cuyo servidor la marca con _meta["anthropic/requiresUserInteraction"] es más estricta: un hook no puede omitir su solicitud de aprobación con "allow", con o sin updatedInput, porque Claude Code no puede confirmar que el hook haya recopilado la interacción que necesita la herramienta.

Diferir una llamada a herramienta para más tarde

"defer" es para integraciones que ejecutan claude -p como subproceso y leen su salida JSON, como una app de Agent SDK o una UI personalizada creada sobre Claude Code. Permite que ese proceso que hace la llamada pause a Claude en una llamada a herramienta, recopile la entrada a través de su propia interfaz y reanude donde lo dejó.

Claude Code respeta "defer" solo en modo no interactivo con el flag -p, y solo cuando Claude hace una única llamada a herramienta en el turno:

  • En sesiones interactivas: registra una advertencia e ignora el resultado del hook
  • Varias llamadas a herramientas a la vez: "defer" se ignora con una advertencia y la herramienta continúa por el flujo de permisos normal

La herramienta AskUserQuestion es el caso típico: Claude quiere preguntarle algo al usuario, pero no hay una terminal donde responder. Una ejecución -p ofrece AskUserQuestion solo cuando tiene un host de permisos, como una herramienta MCP que pasas con --permission-prompt-tool, así que inicia la ejecución con uno. El recorrido completo funciona así:

  1. Claude llama a AskUserQuestion. Se dispara el hook PreToolUse.
  2. El hook devuelve permissionDecision: "defer". La herramienta no se ejecuta. El turno termina con un resultado que lleva stop_reason: "tool_deferred", y la llamada a herramienta pendiente queda en la transcripción.
  3. El proceso llamador lee deferred_tool_use del resultado del SDK, muestra la pregunta en su propia interfaz y espera una respuesta.
  4. El proceso llamador ejecuta claude -p --resume <session-id> con el mismo host de permisos. La misma llamada a herramienta vuelve a disparar PreToolUse.
  5. El hook devuelve permissionDecision: "allow" con la respuesta en updatedInput. La herramienta se ejecuta y Claude continúa.

El campo deferred_tool_use lleva el id, el name y el input de la herramienta:

{
  "type": "result",
  "subtype": "success",
  "stop_reason": "tool_deferred",
  "session_id": "abc123",
  "deferred_tool_use": {
    "id": "toolu_01abc",
    "name": "AskUserQuestion",
    "input": { "questions": [{ "question": "Which framework?", "header": "Framework", "options": [{"label": "React", "description": "Component library"}, {"label": "Vue", "description": "Progressive framework"}], "multiSelect": false }] }
  }
}

No hay tiempo de espera ni límite de reintentos. La sesión permanece en disco hasta que la limpieza por retención de cleanupPeriodDays la elimina, después de 30 días de forma predeterminada.

Si la respuesta no está lista cuando reanudas, el hook puede devolver "defer" otra vez. Si enviaste un prompt con esa reanudación, Claude lo responde sin el resultado de la herramienta y no ejecuta la llamada. Reanuda sin un prompt hasta que el hook devuelva "allow" o "deny".

[!NOTE]

Para reanudar una sesión diferida en modo plan, pasa --permission-prompt-tool junto con --resume para que Claude Code pueda presentar el plan para su aprobación. Para las demás condiciones, consulta Reanudar en modo plan con -p. Requiere Claude Code v2.1.246 o posterior.

Cuando reanudas con -p, Claude Code no restaura ningún otro modo de permisos almacenado. Inicia la ejecución en el modo de permisos en el que se iniciaría una nueva ejecución de claude -p, así que vuelve a pasar --permission-mode o --dangerously-skip-permissions si la sesión diferida usaba uno.

Reanudar cuando una herramienta diferida no está disponible

Un resultado con stop_reason: "tool_deferred_unavailable" significa que la herramienta de una llamada diferida no está disponible en la sesión reanudada, por ejemplo porque su servidor MCP no está conectado. Tu hook PreToolUse no se dispara, y el campo deferred_tool_use del resultado indica el nombre de la herramienta.

Mientras la llamada está pendiente, cualquier prompt que envíes no llega a Claude y la ejecución termina con el código 1. El campo result comienza con <tool> isn't available, so its deferred call didn't run. Ejecuta la llamada o descártala para que no se ejecute:

  • Para ejecutar la llamada: haz que la herramienta esté disponible y reanuda de nuevo.
  • Para descartar la llamada: reanuda con un prompt y con CLAUDE_CODE_DROP_UNAVAILABLE_DEFERRED_TOOL establecida en 1. Claude recibe un error como resultado de la llamada y responde tu prompt. Cuando reanudes más tarde, no habrá ninguna llamada pendiente.

Cuando descartas la llamada, la ejecución escribe el resultado tool_deferred_unavailable con un campo result vacío y, luego, la respuesta de Claude en un segundo resultado. Termina con el código 0 a menos que esa respuesta sea un error. Si estableces la variable y no envías ningún prompt, la ejecución igualmente descarta la llamada y termina con el código 1.

Antes de v2.1.319, Claude Code respondía un prompt sin el resultado de la herramienta y descartaba la llamada. En esas versiones, envía un nuevo prompt una vez que la herramienta esté disponible.

PermissionRequest

Se ejecuta cuando Claude Code está a punto de pedirte permiso para usar una herramienta. En las sesiones que no pueden mostrar una solicitud, como los subagentes en segundo plano en modo no interactivo, Claude Code sigue ejecutando estos hooks y, si ningún hook devuelve una decisión, deniega la llamada a herramienta. Para una llamada que llega a un --permission-prompt-tool o al callback canUseTool del Agent SDK, los hooks se ejecutan junto con tu host, y se aplica lo que decida primero. Usa el control de decisiones de PermissionRequest para permitir o denegar en nombre del usuario.

Usa este evento cuando necesites una señal en el momento en que Claude pide permiso para usar una herramienta. Claude Code ejecuta un hook Notification con el tipo permission_prompt solo después de que la solicitud haya esperado unos seis segundos.

Claude Code no ejecuta hooks PermissionRequest para la solicitud de red de un comando en el sandbox. Para obtener una señal para esa solicitud, usa el tipo de notificación permission_prompt.

Coincide con el nombre de la herramienta, con los mismos valores que PreToolUse.

Entrada de PermissionRequest

Los hooks PermissionRequest reciben los campos tool_name y tool_input como los hooks PreToolUse, pero sin tool_use_id. Para una herramienta MCP, también reciben el objeto mcp_server. Un array opcional permission_suggestions contiene las actualizaciones de permisos que Claude Code sugiere para esta solicitud, como agregar una regla de permiso o cambiar el modo de permisos.

El array permission_suggestions no es una lista exacta de las opciones que ves, porque cada diálogo de permisos construye sus propias opciones. Algunos diálogos, como el de ediciones de archivos, no leen el array en absoluto y derivan sus opciones de la propia solicitud. Un diálogo que sí lo lee puede aun así omitir una opción cuya sugerencia permanece en el array, por ejemplo cuando allowManagedPermissionRulesOnly oculta las opciones para guardar reglas. También puede ofrecer opciones que no tienen una entrada de sugerencia, como Yes, and switch to auto mode, que cambia el modo de permisos directamente en lugar de hacerlo mediante una actualización de permisos.

Los hooks PreToolUse se ejecutan antes de cada llamada a herramienta, necesite o no permiso. Los hooks PermissionRequest se ejecutan solo cuando Claude Code está a punto de pedirte permiso, o cuando de otro modo denegaría automáticamente una llamada que no puede mostrar una solicitud. Ninguno de los dos eventos se dispara para EndConversation.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "PermissionRequest",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf node_modules",
    "description": "Remove node_modules directory"
  },
  "permission_suggestions": [
    {
      "type": "addRules",
      "rules": [{ "toolName": "Bash", "ruleContent": "rm -rf node_modules" }],
      "behavior": "allow",
      "destination": "localSettings"
    }
  ]
}

Control de decisiones de PermissionRequest

Los hooks PermissionRequest pueden permitir o denegar solicitudes de permiso. Además de los campos de salida JSON disponibles para todos los hooks, tu script de hook puede devolver un objeto decision con estos campos específicos del evento:

Campo Descripción
behavior "allow" concede el permiso, "deny" lo deniega. Las reglas deny y ask se siguen evaluando, por lo que un hook que devuelve "allow" no sobrescribe una regla deny coincidente
updatedInput Solo para "allow": modifica los parámetros de entrada de la herramienta antes de la ejecución. Reemplaza todo el objeto de entrada, así que incluye los campos sin cambios junto con los modificados. La entrada modificada se vuelve a evaluar contra las reglas deny y ask. AskUserQuestion y ExitPlanMode necesitan este campo
updatedPermissions Solo para "allow": array de entradas de actualización de permisos que se aplicarán, como agregar una regla de permiso o cambiar el modo de permisos de la sesión
message Solo para "deny": le indica a Claude por qué se denegó el permiso
interrupt Solo para "deny": si es true, detiene a Claude

Para permitir o reescribir un comando de Monitor, consulta Hacer coincidir comandos de Monitor.

Un hook que sale con 2 sin un objeto decision deja sin cambios el flujo de permisos, y su stderr se descarta. Para conceder o denegar la solicitud, devuelve el objeto decision.

{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow",
      "updatedInput": {
        "command": "npm run lint"
      }
    }
  }
}

Entradas de actualización de permisos

El campo de salida updatedPermissions y el campo de entrada permission_suggestions usan el mismo array de objetos de entrada. Cada entrada tiene un type que determina sus demás campos, y un destination que controla dónde se escribe el cambio.

type Campos Efecto
addRules rules, behavior, destination Agrega reglas de permisos. rules es un array de objetos {toolName, ruleContent?}. Omite ruleContent para que coincida con toda la herramienta. behavior es "allow", "deny" o "ask"
replaceRules rules, behavior, destination Reemplaza todas las reglas del behavior indicado en el destination con las rules proporcionadas
removeRules rules, behavior, destination Elimina las reglas coincidentes del behavior indicado
setMode mode, destination Cambia el modo de permisos. Los modos válidos son default, auto, acceptEdits, dontAsk, bypassPermissions, plan y manual como alias de default
addDirectories directories, destination Agrega directorios de trabajo. directories es un array de cadenas de rutas
removeDirectories directories, destination Elimina directorios de trabajo

En rules, para una regla de permisos escrita como Tool(specifier), toolName es el nombre de la herramienta y ruleContent es el especificador:

  • Bash(npm test): { "toolName": "Bash", "ruleContent": "npm test" }
  • Bash o Bash(*): { "toolName": "Bash" }, porque ambas coinciden con toda la herramienta
  • mcp__github__get_issue: { "toolName": "mcp__github__get_issue" }

No incluyas ruleContent en una regla allow para una herramienta MCP. Si lo agregas, la regla no coincide con ninguna llamada.

[!NOTE]

setMode con bypassPermissions solo tiene efecto si iniciaste la sesión con el modo bypass ya disponible: --dangerously-skip-permissions, --permission-mode bypassPermissions, --allow-dangerously-skip-permissions o permissions.defaultMode: "bypassPermissions" en la configuración de usuario, --settings o la configuración administrada. De lo contrario, la actualización no tiene efecto. La actualización tampoco tiene efecto cuando permissions.disableBypassPermissionsMode desactiva el modo, o cuando la sesión se inicia en modo restringido.

bypassPermissions nunca se persiste como defaultMode, independientemente de destination.

El campo destination de cada entrada determina si el cambio permanece en memoria o se persiste en un archivo de configuración.

destination Escribe en
session solo en memoria, se descarta cuando termina la sesión
localSettings .claude/settings.local.json
projectSettings .claude/settings.json
userSettings ~/.claude/settings.json

Un hook puede repetir una de las permission_suggestions que recibió como su propia salida updatedPermissions.

PostToolUse

Se ejecuta inmediatamente después de que una herramienta se completa correctamente.

Coincide con el nombre de la herramienta, con los mismos valores que PreToolUse.

Usa una coincidencia más amplia cuando el nombre de la herramienta no sea el filtro adecuado:

  • Para ejecutar un hook después de que cualquier herramienta se complete correctamente, omite el matcher o establécelo en "*". Así, tu hook puede descubrir por sí mismo qué cambió, por ejemplo ejecutando git status --porcelain, que también enumera los archivos sin seguimiento que git diff pasa por alto. Para las llamadas a herramientas que fallan, agrega el mismo hook en PostToolUseFailure.
  • Para ejecutar un hook cuando un archivo específico cambia en el disco, sin importar qué lo escribió, usa FileChanged. Claude Code no ejecuta un hook PostToolUse que coincida con Edit|Write cuando un comando Bash o un proceso externo a Claude Code reescribe el mismo archivo.

PostToolUse después de una interrupción

Si un hook PostToolUse se detiene antes de tiempo después de que envías un mensaje o interrumpes durante una llamada a herramienta, verifica si presionaste Ctrl+Enter, Enter o Esc:

  • Ctrl+Enter: si presionarla termina el turno mientras una herramienta se está ejecutando y la herramienta termina, tus hooks PostToolUse se ejecutan sobre su resultado y tienen 10 segundos para terminar. La tecla actúa como Esc si pusiste en cola un comando ! antes de tus mensajes o si las tareas en segundo plano están desactivadas, por ejemplo con CLAUDE_CODE_DISABLE_BACKGROUND_TASKS.
  • Enter: un mensaje que pones en cola con Enter no interrumpe el turno, así que los hooks conservan su timeout habitual.
  • Esc: cancelas los hooks de inmediato, así que envía el mensaje con Enter o Ctrl+Enter cuando los hooks tengan que ejecutarse.

Cuando se cumplen los 10 segundos, Claude Code finaliza un hook de comando que aún se esté ejecutando, sea cual sea su timeout. Para permitir que un hook de comando más largo termine, ejecútalo con async: true, lo que significa que el hook no puede bloquear ni reescribir el resultado.

Antes de v2.1.319, los hooks no se iniciaban después de que interrumpías el turno con un mensaje, y un hook en ejecución se detenía.

Entrada de PostToolUse

Los hooks PostToolUse se disparan después de que una herramienta ya se ejecutó correctamente. La entrada incluye tanto tool_input, los argumentos enviados a la herramienta, como tool_response, el resultado que devolvió. El esquema exacto de ambos depende de la herramienta. Las rutas de tool_input de las herramientas de archivos llegan en el mismo formato que para PreToolUse: siempre absolutas, con los separadores nativos de la plataforma, es decir, barras invertidas en Windows. Para una herramienta MCP, la entrada también incluye el objeto mcp_server.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "PostToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.txt",
    "content": "file content"
  },
  "tool_response": {
    "filePath": "/path/to/file.txt",
    "type": "create"
  },
  "tool_use_id": "toolu_01ABC123...",
  "duration_ms": 12
}
Campo Descripción
duration_ms Opcional. Tiempo de ejecución de la herramienta en milisegundos. Excluye el tiempo dedicado a las solicitudes de permiso y a los hooks PreToolUse

Control de decisiones de PostToolUse

Los hooks PostToolUse pueden proporcionar retroalimentación a Claude después de la ejecución de la herramienta. Además de los campos de salida JSON disponibles para todos los hooks, tu script de hook puede devolver estos campos específicos del evento:

Campo Descripción
decision "block" agrega el reason junto al resultado de la herramienta. Claude sigue viendo la salida original; para reemplazarla, usa updatedToolOutput
reason Explicación que se muestra a Claude cuando decision es "block"
additionalContext Cadena que se agrega al contexto de Claude junto con el resultado de la herramienta. Consulta Agregar contexto para Claude
classifierContext Nota breve sobre el resultado de esta llamada destinada al clasificador del modo automático en lugar de a Claude. Consulta Anotar un resultado para el clasificador del modo automático. Requiere Claude Code v2.1.236 o posterior
updatedToolOutput Reemplaza la salida de la herramienta con el valor proporcionado antes de enviarla a Claude. El valor debe coincidir con la forma de la salida de la herramienta. En una sesión en la nube de Cowork, Claude Code ignora este campo cuando la herramienta es AskUserQuestion o ExitPlanMode, y Claude lee la respuesta o el plan originales
updatedMCPToolOutput Reemplaza la salida solo para herramientas MCP. Es preferible updatedToolOutput, que también funciona para las herramientas integradas

El siguiente ejemplo reemplaza la salida de una llamada a Bash. El valor de reemplazo coincide con la forma de la salida de la herramienta Bash:

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "Additional information for Claude",
    "updatedToolOutput": {
      "stdout": "[redacted]",
      "stderr": "",
      "interrupted": false,
      "isImage": false
    }
  }
}

[!WARNING]

updatedToolOutput solo cambia lo que ve Claude. La herramienta ya se ejecutó cuando se dispara el hook, por lo que cualquier archivo escrito, comando ejecutado o solicitud de red enviada ya tuvo efecto. La telemetría, como los spans de herramientas de OpenTelemetry y los eventos de analítica, también captura la salida original antes de que se ejecute el hook. Para impedir o modificar una llamada a herramienta antes de que se ejecute, usa en su lugar un hook PreToolUse.

El valor de reemplazo debe coincidir con la forma de la salida de la herramienta. Las herramientas integradas devuelven objetos estructurados en lugar de cadenas simples. Por ejemplo, Bash devuelve un objeto con los campos stdout, stderr, interrupted e isImage. Para las herramientas integradas, un valor que no coincide con el esquema de salida de la herramienta se ignora y se usa la salida original. La salida de las herramientas MCP se transmite sin validación de esquema. Eliminar detalles de error que Claude necesita puede hacer que proceda con una suposición falsa.

Anotar un resultado para el clasificador del modo automático

Devuelve classifierContext para enviar una nota breve sobre el resultado de la llamada a herramienta al clasificador del modo automático en lugar de a Claude. La nota sirve para la revisión que el clasificador hace de las acciones posteriores. El campo requiere Claude Code v2.1.236 o posterior.

El siguiente ejemplo le indica al clasificador de dónde provino la salida de una consulta:

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "classifierContext": "This query ran against the staging database, not production."
  }
}

El peso que el clasificador le da a la nota depende de dónde configuraste el hook:

  • Hooks configurados en Claude Code: para los hooks de archivos de configuración, plugins, skills y frontmatter de agentes, el clasificador trata la nota como contexto no verificado proporcionado por la aplicación. La nota nunca establece la intención del usuario, y si afirma que aprobaste o solicitaste algo, el clasificador contrasta esa afirmación con tus propios mensajes en la conversación
  • Callbacks en proceso del Agent SDK: cuando una aplicación que integra Claude Code registra el hook como un callback del SDK de TypeScript y devuelve la nota durante la sesión en vivo, el clasificador puede considerar como intención del usuario una declaración del usuario transmitida en la nota. Dicha declaración puede satisfacer un requisito de consentimiento que el clasificador aceptaría de un mensaje que tú envías, pero nunca levanta un bloqueo que tu propio mensaje tampoco podría levantar. Después de que se reanuda una sesión, Claude Code trata las notas restauradas como contexto no verificado. Cuando hooks de ambos grupos anotan la misma llamada, el clasificador trata la nota combinada como no verificada

Claude Code aplica estos límites al entregar la nota:

  • Longitud: Claude Code limita las notas de una llamada a herramienta a 2,000 caracteres y trunca el resto. El límite se comparte entre todos los hooks que responden a esa llamada
  • Solo respuestas síncronas: Claude Code ignora el campo en la respuesta de un hook que se ejecuta en segundo plano, porque esa respuesta llega después de que Claude Code registra el resultado de la herramienta
  • Llamadas que el clasificador no registra: la transcripción del clasificador omite las consultas de solo lectura, como las lecturas de archivos y las búsquedas. Claude Code descarta una nota adjunta a una de esas llamadas
  • Interacción con reescrituras: cuando la nota describe una salida que estás reemplazando con updatedToolOutput, devuelve ambos campos en la misma respuesta del hook. Claude Code descarta la nota si esa reescritura se rechaza o si la reescritura de otro hook la reemplaza. Claude Code entrega una nota que devuelves sin una reescritura incluso cuando otro hook reescribe la salida

[!WARNING]

El clasificador lee el contenido que colocas en classifierContext como información de la aplicación que aloja la sesión, así que no copies en él salida de herramientas no confiable ni texto de terceros. Limita la nota a una afirmación breve sobre esta única llamada, como un hecho sobre su origen o una declaración del usuario al respecto; no uses el campo para entregar mensajes no relacionados ni un flujo de eventos.

PostToolUseFailure

Se ejecuta cuando falla una herramienta que comenzó a ejecutarse: la herramienta lanzó un error o una herramienta MCP devolvió un resultado de error. Úsalo para registrar fallos, enviar alertas o proporcionar retroalimentación correctiva a Claude.

Coincide con el nombre de la herramienta, con los mismos valores que PreToolUse.

[!NOTE]

Este evento no se dispara para llamadas a herramientas rechazadas antes de la ejecución: un nombre de herramienta desconocido, una entrada que no supera la validación del esquema o la validación específica de la herramienta, o una denegación de permiso. Los rechazos de validación se devuelven como resultados tool_use_error y ocurren antes de que se ejecuten los hooks, por lo que no disparan ni PreToolUse ni PostToolUseFailure. Las denegaciones de permiso disparan PreToolUse pero no este evento; consulta PermissionDenied.

Entrada de PostToolUseFailure

Los hooks PostToolUseFailure reciben los mismos campos tool_name y tool_input que PostToolUse, junto con información del error como campos de nivel superior. Para una herramienta MCP, también reciben el objeto mcp_server. Por ejemplo, un comando npm test fallido podría entregar:

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "PostToolUseFailure",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite"
  },
  "tool_use_id": "toolu_01ABC123...",
  "error": "Exit code 1\nError: Cannot find module 'express'",
  "is_interrupt": false,
  "duration_ms": 4187
}
Campo Descripción
error Cadena que describe qué salió mal. El formato depende de la herramienta que falló
is_interrupt Booleano opcional. Es true cuando el fallo llegó a Claude Code como una interrupción en lugar de como un error reportado por la herramienta. Cancelar una herramienta en ejecución no dispara este hook; en su lugar, el resultado de la herramienta contiene el mensaje de interrupción
duration_ms Opcional. Tiempo de ejecución de la herramienta en milisegundos. Excluye el tiempo dedicado a las solicitudes de permiso y a los hooks PreToolUse

La cadena error suele ser el mismo texto que Claude recibe como resultado de la herramienta fallida. Su formato varía según la herramienta y el fallo. Basa tu hook en tool_name, is_interrupt y la primera línea Exit code N; trata el resto de la cadena como texto para mostrar, no como un formato estable.

  • Para Bash y PowerShell, un comando que se ejecutó y terminó produce una primera línea Exit code N, seguida de cualquier salida que haya producido el comando como un único bloque con stdout y stderr intercalados
  • Un payload también puede contener un mensaje de fallo simple sin línea de código de salida, cuando Claude Code no pudo iniciar el proceso del shell en sí
  • Claude Code trunca por la mitad las cadenas largas alrededor de un marcador ... [N characters truncated] ..., y puede insertar líneas propias, como Command timed out after 2m 0s

Control de decisiones de PostToolUseFailure

Los hooks PostToolUseFailure pueden proporcionar contexto a Claude después de un fallo de una herramienta. Además de los campos de salida JSON disponibles para todos los hooks, tu script de hook puede devolver estos campos específicos del evento:

Campo Descripción
additionalContext Cadena que se agrega al contexto de Claude junto con el error. Consulta Agregar contexto para Claude
{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUseFailure",
    "additionalContext": "Additional information about the failure for Claude"
  }
}

PostToolBatch

Se ejecuta una vez después de que se haya resuelto cada llamada a herramienta de un lote, antes de que Claude Code envíe la siguiente solicitud al modelo. PostToolUse se dispara una vez por herramienta, lo que significa que se dispara de forma concurrente cuando Claude hace llamadas a herramientas en paralelo. PostToolBatch se dispara exactamente una vez con el lote completo, por lo que es el lugar adecuado para inyectar contexto que depende del conjunto de herramientas que se ejecutaron y no de una sola herramienta. No hay matcher para este evento.

Entrada de PostToolBatch

Además de los campos de entrada comunes, los hooks PostToolBatch reciben tool_calls, un array que describe cada llamada a herramienta del lote:

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "PostToolBatch",
  "tool_calls": [
    {
      "tool_name": "Read",
      "tool_input": {"file_path": "/.../ledger/accounts.py"},
      "tool_use_id": "toolu_01...",
      "tool_response": "1\tfrom __future__ import annotations\n2\t..."
    },
    {
      "tool_name": "Read",
      "tool_input": {"file_path": "/.../ledger/transactions.py"},
      "tool_use_id": "toolu_02...",
      "tool_response": "1\tfrom __future__ import annotations\n2\t..."
    }
  ]
}

tool_response contiene el mismo contenido que el modelo recibe en el bloque tool_result correspondiente. El valor es una cadena serializada o un array de bloques de contenido, exactamente como lo emitió la herramienta. Para Read, eso significa texto con números de línea como prefijo en lugar del contenido sin procesar del archivo. Las respuestas pueden ser grandes, así que analiza solo los campos que necesites.

[!NOTE]

La forma de tool_response es distinta de la de PostToolUse. PostToolUse pasa el objeto Output estructurado de la herramienta, como {filePath: "...", type: "create"} para Write; PostToolBatch pasa el contenido serializado de tool_result que ve el modelo.

Control de decisiones de PostToolBatch

Los hooks PostToolBatch pueden inyectar contexto para Claude. Además de los campos de salida JSON disponibles para todos los hooks, tu script de hook puede devolver estos campos específicos del evento:

Campo Descripción
additionalContext Cadena de contexto que se inyecta una vez antes de la siguiente llamada al modelo. Consulta Agregar contexto para Claude para conocer los detalles de entrega, qué incluir y cómo las sesiones reanudadas manejan los valores anteriores
{
  "hookSpecificOutput": {
    "hookEventName": "PostToolBatch",
    "additionalContext": "These files are part of the ledger module. Run pytest before marking the task complete."
  }
}

Devolver decision: "block" o continue: false detiene el bucle agéntico antes de la siguiente llamada al modelo. El mensaje de bloqueo proviene del reason o stopReason del JSON, o de stderr al salir con 2. Lo ves como una advertencia en la transcripción, y permanece en la conversación, así que Claude lo ve cuando la conversación continúa.

PermissionDenied

Se ejecuta cuando el modo automático deniega una llamada a herramienta, incluso cuando la deniega sin un veredicto del clasificador porque una verificación de seguridad independiente del modo automático rechazó la propia solicitud del clasificador o porque su respuesta no se pudo analizar. Este hook solo se dispara en el modo automático: no se ejecuta cuando deniegas manualmente un diálogo de permisos, cuando un hook PreToolUse bloquea una llamada o cuando coincide una regla deny. Úsalo para registrar denegaciones, ajustar la configuración o indicarle al modelo que puede reintentar la llamada a herramienta.

Coincide con el nombre de la herramienta, con los mismos valores que PreToolUse.

Entrada de PermissionDenied

Además de los campos de entrada comunes, los hooks PermissionDenied reciben tool_name, tool_input, tool_use_id y reason. Para una herramienta MCP, también reciben el objeto mcp_server.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "auto",
  "hook_event_name": "PermissionDenied",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /tmp/build",
    "description": "Clean build directory"
  },
  "tool_use_id": "toolu_01ABC123...",
  "reason": "[Irreversible Local Destruction]"
}
Campo Descripción
reason El motivo de la denegación. Para un veredicto del clasificador, en la mayoría de las sesiones nombra la regla coincidente entre corchetes, como [Data Exfiltration]; consulta Revisar denegaciones para conocer las otras formas. Para una denegación sin veredicto, comienza con Auto mode could not evaluate this action and is blocking it for safety. Para una denegación porque el modelo del clasificador no estaba disponible, es el texto fijo Classifier unavailable

Control de decisiones de PermissionDenied

Los hooks PermissionDenied pueden indicarle al modelo que puede reintentar la llamada a herramienta denegada. Devuelve un objeto JSON con hookSpecificOutput.retry establecido en true:

{
  "hookSpecificOutput": {
    "hookEventName": "PermissionDenied",
    "retry": true
  }
}

Cuando retry es true, Claude Code agrega un mensaje a la conversación que le indica al modelo que puede reintentar la llamada a herramienta. Claude Code no revierte la denegación en sí. Si tu hook no devuelve JSON, o devuelve retry: false, la denegación se mantiene y el modelo recibe el mensaje de rechazo original.

Claude Code ignora retry: true cuando el clasificador no produjo ningún veredicto sobre la acción: su respuesta no se pudo analizar, o una verificación de seguridad independiente del modo automático rechazó la propia solicitud del clasificador. Para esas denegaciones, Claude Code ya le indica al modelo en el mensaje de rechazo si debe reintentar más tarde o seguir adelante.

Notification

Se ejecuta cuando Claude Code envía notificaciones. Coincide con el tipo de notificación. Omite el matcher para ejecutar hooks para todos los tipos de notificación.

Recibes estos eventos de hook incluso con las notificaciones de escritorio desactivadas: el ajuste preferredNotifChannel, incluido notifications_disabled, solo cambia cómo se te alerta, no si tu hook se ejecuta.

Matcher Cuándo se dispara
permission_prompt Claude necesita que apruebes el uso de una herramienta o la solicitud de red de un comando en el sandbox, y la solicitud lleva esperando unos seis segundos
idle_prompt Claude terminó de responder hace unos 60 segundos y no has escrito nada desde entonces
auth_success Se completa la autenticación
auth_storage_failure Tu inicio de sesión se completa, pero Claude Code no puede guardar las nuevas credenciales en su almacenamiento de credenciales. Se dispara en lugar de auth_success
elicitation_dialog Un servidor MCP abre un formulario de elicitación y no has escrito nada durante unos seis segundos
elicitation_url_dialog Un servidor MCP te pide que abras una URL en el navegador y no has escrito nada durante unos seis segundos
elicitation_complete Un servidor MCP informa que una elicitación en modo URL se completó
elicitation_response Se envía una respuesta de elicitación MCP de vuelta al servidor
agent_needs_input Una sesión en segundo plano empieza a esperar tu entrada mientras la vista de agentes está abierta en una terminal. También se dispara cuando una sesión de terminal te muestra la pregunta de configuración de terminal de un compañero de un equipo de agentes o el aviso del modo automático sobre los cargos por solicitudes del clasificador y no has escrito nada durante unos seis segundos
agent_completed Una sesión en segundo plano termina o falla. Se dispara solo mientras la vista de agentes está abierta en una terminal
quota_auto_resume_fired Claude Code continúa tu tarea después de que un límite de uso de claude.ai la pausara: en el momento del restablecimiento, o antes cuando algo que haces en Claude Code durante la espera, como agregar créditos de uso, mejorar tu plan o cambiar de modelo, vuelve a hacer disponible el uso, con la excepción del ajuste de modelo
quota_auto_resume_stale Un límite de uso de claude.ai se restableció mientras tu computadora estuvo suspendida durante más de unos 30 minutos. Claude Code espera a que presiones Enter en lugar de continuar. Tras una suspensión más corta, continúa y dispara quota_auto_resume_fired en su lugar
quota_auto_resume_disabled Claude Code termina su espera por un límite de uso de claude.ai sin continuar tu tarea: autoContinueAtUsageLimit se desactivó o el restablecimiento se movió a más de 24 horas durante una espera que Claude Code inició por su cuenta, la tarea continuada siguió alcanzando el límite, o la continuación se bloqueó antes de llegar al modelo. No se dispara cuando presionas Esc o Ctrl+C, o eliges Don't continue automatically
plugin_notification Un mod genera una notificación con $.ui.notify. El title del hook es el nombre del mod, a menos que el mod haya establecido uno

Los tipos quota_auto_resume_fired, quota_auto_resume_stale y quota_auto_resume_disabled requieren Claude Code v2.1.234 o posterior. El tipo auth_storage_failure requiere v2.1.288 o posterior.

En sesiones de terminal, permission_prompt para la solicitud de red de un comando en el sandbox requiere Claude Code v2.1.246 o posterior.

agent_needs_input para la pregunta de configuración de terminal de un compañero requiere Claude Code v2.1.248 o posterior.

[!NOTE]

Los tipos permission_prompt, idle_prompt, elicitation_dialog y elicitation_url_dialog comparten su temporización con las notificaciones de escritorio, por lo que en las sesiones de terminal solo los ves cuando parece que estás lejos de la terminal:

  • Espera permission_prompt una vez que no hayas escrito nada durante unos seis segundos. El temporizador comienza cuando aparece la solicitud de permiso, y cada pulsación de tecla lo aplaza. Para ejecutar un hook inmediatamente cuando Claude pide permiso para usar una herramienta, usa PermissionRequest en su lugar.
  • Espera idle_prompt unos 60 segundos después de que Claude termine de responder, y solo si no has escrito nada desde entonces y ningún agente en segundo plano, como un subagente en segundo plano, sigue ejecutándose. Claude Code no envía idle_prompt mientras espera a que se restablezca un límite de uso de claude.ai. Cuando la espera termina por sí sola, se dispara en su lugar uno de los tipos quota_auto_resume_*.
  • Espera elicitation_dialog para un formulario de elicitación, o elicitation_url_dialog para una solicitud de URL del navegador, una vez que no hayas escrito nada durante unos seis segundos. Ambos comparten el mismo umbral de seis segundos que permission_prompt: el temporizador comienza cuando aparece el diálogo, y cada pulsación de tecla lo aplaza.

Una solicitud de permiso o elicitación que llega mientras otro diálogo está en pantalla mantiene el mismo umbral de seis segundos, contado desde que llega la solicitud. Su notificación puede llegarte mientras la solicitud sigue esperando detrás del diálogo abierto.

Claude Code temporiza permission_prompt de forma diferente en las sesiones en las que envía las solicitudes de permiso al callback canUseTool del Agent SDK, que es como Claude Desktop y la extensión de VS Code alojan Claude Code:

  • Espera permission_prompt unos seis segundos después de que Claude pida permiso. Claude Code no lo aplaza mientras escribes.
  • Si tú o un hook PermissionRequest responden antes, Claude Code no ejecuta permission_prompt.
  • Establece CLAUDE_CODE_DISABLE_PERMISSION_PROMPT_NOTIFY_HOOKS en 1 para desactivar permission_prompt en estas sesiones.

Antes de v2.1.233, permission_prompt no se disparaba en estas sesiones.

Usa matchers separados para ejecutar distintos handlers según el tipo de notificación. Esta configuración activa un script de alerta específico de permisos cuando Claude necesita aprobación de permisos y una notificación diferente cuando Claude ha estado inactivo:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/permission-alert.sh"
          }
        ]
      },
      {
        "matcher": "idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/idle-notification.sh"
          }
        ]
      }
    ]
  }
}

Entrada de Notification

Además de los campos de entrada comunes, los hooks Notification reciben message con el texto de la notificación, un title opcional y notification_type, que indica qué tipo se disparó.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "Notification",
  "message": "Claude needs your permission",
  "title": "Permission needed",
  "notification_type": "permission_prompt"
}

Los hooks Notification no pueden bloquear ni modificar notificaciones. Claude Code descarta sus campos systemMessage y continue, pero sigue emitiendo terminalSequence, que es en lo que se basa el ejemplo de notificación de escritorio. Los hooks Notification están pensados para efectos secundarios, como reenviar la notificación a un servicio externo.

SubagentStart

Se ejecuta cuando Claude genera un subagente con la herramienta Agent, cuando Claude reanuda un subagente y cada vez que un compañero en proceso de un equipo de agentes maneja un mensaje nuevo. Admite matchers para filtrar por nombre de tipo de agente. Para los agentes integrados, es el nombre del agente, como general-purpose, Explore o Plan. Para los subagentes personalizados, es el campo name del frontmatter del agente, no el nombre del archivo.

Para los subagentes incluidos en un plugin, el tipo de agente es el identificador con alcance del plugin, como my-plugin:reviewer, no el nombre simple del frontmatter. Los dos puntos hacen que un nombre con alcance de plugin se evalúe como expresión regular, así que ancla el matcher con ^ y $ para obtener una coincidencia exacta: ^my-plugin:reviewer$.

Entrada de SubagentStart

Además de los campos de entrada comunes, los hooks SubagentStart reciben agent_id con el identificador único del subagente y agent_type con el nombre del agente por el que filtra el matcher.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "SubagentStart",
  "agent_id": "agent-abc123",
  "agent_type": "Explore"
}

Los hooks SubagentStart no pueden bloquear la creación de subagentes, pero pueden inyectar contexto en el subagente. Además de los campos de salida JSON disponibles para todos los hooks, puedes devolver:

Campo Descripción
additionalContext Cadena que se agrega al contexto del subagente al inicio de su conversación, antes de su primer prompt. Consulta Agregar contexto para Claude
{
  "hookSpecificOutput": {
    "hookEventName": "SubagentStart",
    "additionalContext": "Follow security guidelines for this task"
  }
}

Cuando el hook se ejecuta de nuevo para el mismo subagente, Claude Code inyecta el contexto devuelto solo cuando el contexto del subagente aún no contiene la copia de una ejecución anterior. La copia inyectada al inicio permanece en su lugar, lo que deja intacta la caché de prompts del subagente. Después de que la compactación automática descarta esa copia, Claude Code vuelve a inyectar el contexto de la siguiente ejecución.

SubagentStop

Se ejecuta cuando un subagente de Claude Code terminó de responder. Coincide con el tipo de agente, con los mismos valores que SubagentStart.

Entrada de SubagentStop

Además de los campos de entrada comunes, los hooks SubagentStop reciben stop_hook_active, agent_id, agent_type, agent_transcript_path y last_assistant_message. El campo agent_type es el valor que se usa para el filtrado del matcher. transcript_path es la transcripción de la sesión principal, mientras que agent_transcript_path es la transcripción propia del subagente, almacenada en una carpeta anidada subagents/. El campo last_assistant_message contiene el contenido de texto de la respuesta final del subagente, de modo que los hooks pueden acceder a él sin analizar el archivo de transcripción.

No todos los eventos SubagentStop provienen de un subagente que Claude generó. Claude Code también ejecuta agentes internos para algunas de sus propias funciones, como las sugerencias de prompts y las preguntas laterales con /btw, y SubagentStop también se dispara cuando uno de ellos termina. Para esos eventos, agent_type es el nombre del agente con el que se ejecuta la propia sesión, como uno establecido con --agent o con el ajuste agent, y una cadena vacía cuando la sesión se ejecuta sin ninguno.

Un matcher que nombra tipos de agente no coincide con un agent_type vacío. Un hook cuyo matcher se omite, es "" o "*", o es una expresión regular que coincide con una cadena vacía, también se ejecuta para eventos con un agent_type vacío.

En Claude Code v2.1.271 o posterior, un subagente que se ejecuta con la herramienta SubagentHandback entrega su informe mediante esa herramienta antes de detenerse. En ese caso, el campo last_assistant_message contiene el texto de cierre del subagente, si lo hay, que no es el informe entregado. El informe es la entrada message de esa llamada, que un hook PreToolUse o PostToolUse con matcher en SubagentHandback recibe como tool_input.message.

Los hooks SubagentStop también reciben los arrays background_tasks y session_crons descritos en Entrada de Stop. Ambos arrays tienen como alcance la sesión principal, no el subagente.

{
  "session_id": "abc123",
  "transcript_path": "~/.claude/projects/.../abc123.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "SubagentStop",
  "stop_hook_active": false,
  "agent_id": "def456",
  "agent_type": "Explore",
  "agent_transcript_path": "~/.claude/projects/.../abc123/subagents/agent-def456.jsonl",
  "last_assistant_message": "Analysis complete. Found 3 potential issues...",
  "background_tasks": [],
  "session_crons": []
}

Los hooks SubagentStop usan el mismo formato de control de decisiones que los hooks Stop, incluido hookSpecificOutput.additionalContext con hookEventName establecido en "SubagentStop", para retroalimentación que no es de error y que mantiene el subagente en ejecución. Devolver decision: "block" con un reason mantiene el subagente en ejecución y le entrega reason como su siguiente instrucción. Un hook que bloquea al salir con 2 entrega su mensaje de stderr de la misma forma. Para inyectar contexto en la sesión principal después de que un subagente regresa, usa en su lugar un hook PostToolUse sobre la herramienta Agent.

TaskCreated

Se ejecuta cuando se está creando una tarea mediante la herramienta TaskCreate. Úsalo para aplicar convenciones de nomenclatura, exigir descripciones de tareas o impedir que se creen determinadas tareas. En una sesión sin las herramientas Task, este evento no se dispara.

Los hooks TaskCreated no admiten matchers y se disparan en cada ocurrencia.

Entrada de TaskCreated

Además de los campos de entrada comunes, los hooks TaskCreated reciben task_id, task_subject y, opcionalmente, task_description, teammate_name y team_name.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "TaskCreated",
  "task_id": "task-001",
  "task_subject": "Implement user authentication",
  "task_description": "Add login and signup endpoints",
  "teammate_name": "implementer",
  "team_name": "session-a1b2c3d4"
}
Campo Descripción
task_id Identificador de la tarea que se está creando
task_subject Título de la tarea
task_description Descripción detallada de la tarea. Puede estar ausente
teammate_name Nombre del compañero que crea la tarea. Puede estar ausente
team_name Obsoleto. Nombre del equipo derivado de la sesión; se eliminará en una versión futura
agent_id En este evento, el campo de entrada común identifica al subagente o al compañero en proceso que crea la tarea. Puede estar ausente. Requiere Claude Code v2.1.290 o posterior

Control de decisiones de TaskCreated

Un hook TaskCreated puede bloquear la creación con el código de salida 2 o con una decisión JSON. En cualquier caso, Claude Code elimina la tarea y devuelve tu mensaje a Claude como el error de la herramienta. Claude Code ignora continue: false de este evento y Claude sigue trabajando.

  • Código de salida 2: Claude Code devuelve el texto de stderr como mensaje.
  • JSON {"decision": "block", "reason": "..."}: Claude Code devuelve reason como mensaje.

Este ejemplo bloquea las tareas cuyos asuntos no siguen el formato requerido:

#!/bin/bash
INPUT=$(cat)
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject')

if [[ ! "$TASK_SUBJECT" =~ ^\[TICKET-[0-9]+\] ]]; then
  echo "Task subject must start with a ticket number, e.g. '[TICKET-123] Add feature'" >&2
  exit 2
fi

exit 0

TaskCompleted

Se ejecuta cuando una tarea se está marcando como completada. Se dispara en dos situaciones: cuando cualquier agente marca explícitamente una tarea como completada mediante la herramienta TaskUpdate, o cuando un compañero de un equipo de agentes termina su turno con tareas en curso. Úsalo para aplicar criterios de finalización, como que las pruebas o las comprobaciones de lint pasen antes de que una tarea pueda cerrarse.

Los hooks TaskCompleted no admiten matchers y se disparan en cada ocurrencia.

Entrada de TaskCompleted

Además de los campos de entrada comunes, los hooks TaskCompleted reciben task_id, task_subject y, opcionalmente, task_description, teammate_name y team_name.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "TaskCompleted",
  "task_id": "task-001",
  "task_subject": "Implement user authentication",
  "task_description": "Add login and signup endpoints",
  "teammate_name": "implementer",
  "team_name": "session-a1b2c3d4"
}
Campo Descripción
task_id Identificador de la tarea que se está completando
task_subject Título de la tarea
task_description Descripción detallada de la tarea. Puede estar ausente
teammate_name Nombre del compañero que completa la tarea. Puede estar ausente
team_name Obsoleto. Nombre del equipo derivado de la sesión; se eliminará en una versión futura
agent_id En este evento, el campo de entrada común identifica al subagente o al compañero en proceso que completa la tarea. Puede estar ausente. Requiere Claude Code v2.1.290 o posterior

Control de decisiones de TaskCompleted

Los hooks TaskCompleted admiten dos formas de controlar la finalización de tareas:

  • Código de salida 2: la tarea no se marca como completada y el mensaje de stderr se devuelve al modelo como retroalimentación.
  • JSON {"continue": false, "stopReason": "..."}: cuando el evento lo desencadenó un compañero que terminó su turno, detiene al compañero por completo, igual que el comportamiento del hook Stop. El stopReason se muestra al usuario. Cuando el evento lo desencadenó la herramienta TaskUpdate, Claude Code ignora continue: false; el código de salida 2 sigue bloqueando la finalización.

Este ejemplo ejecuta pruebas y bloquea la finalización de la tarea si fallan:

#!/bin/bash
INPUT=$(cat)
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject')

# Run the test suite
if ! npm test 2>&1; then
  echo "Tests not passing. Fix failing tests before completing: $TASK_SUBJECT" >&2
  exit 2
fi

exit 0

Stop

Se ejecuta cuando el agente principal de Claude Code terminó de responder. No se ejecuta si la detención se produjo por una interrupción del usuario. Los errores de la API disparan StopFailure en su lugar.

[!TIP]

El comando /goal es un atajo integrado para un hook Stop basado en prompts con alcance de sesión. Úsalo cuando quieras que Claude siga trabajando hacia una condición sin escribir configuración de hooks.

Entrada de Stop

Además de los campos de entrada comunes, los hooks Stop reciben stop_hook_active, last_assistant_message, background_tasks y session_crons. El campo stop_hook_active es true cuando Claude Code ya está continuando como resultado de un hook de detención. Comprueba este valor o procesa la transcripción para evitar bloquear por una condición que nunca se resolverá.

Claude Code aplica un límite de 8 continuaciones consecutivas: después de que los hooks de detención hayan continuado el turno ocho veces seguidas, Claude Code sobrescribe el siguiente bloqueo y termina el turno. El recuento de continuaciones consecutivas se reinicia cada vez que Claude llama a una herramienta. Para aumentar el límite, establece CLAUDE_CODE_STOP_HOOK_BLOCK_CAP.

El campo last_assistant_message contiene el contenido de texto de la respuesta final de Claude, de modo que los hooks pueden acceder a él sin analizar el archivo de transcripción. Para los hooks que actúan sobre el turno recién completado, como los hooks de lectura en voz alta o de notificación, usa este campo en lugar de leer transcript_path: no se garantiza que el archivo de transcripción incluya el mensaje final en el momento de Stop en todas las versiones.

Los arrays background_tasks y session_crons permiten a los hooks distinguir entre "la sesión terminó" y "la sesión está en pausa esperando que un trabajo en segundo plano la reactive". Ambos arrays están presentes cuando se puede acceder al registro de tareas y están vacíos cuando no hay nada en curso ni programado.

Cada entrada de background_tasks describe una tarea en curso y usa estos campos:

Campo Descripción
id Identificador de la tarea
type Etiqueta descriptiva del tipo de tarea, como shell, subagent, monitor, workflow, teammate, cloud session o MCP task. Cada etiqueta identifica qué función de Claude Code creó la tarea. Recurre al discriminante sin procesar para los tipos no reconocidos
status Estado actual de la tarea
description Descripción en texto libre, limitada a 1000 caracteres con un marcador … [+N chars] dentro de la cadena cuando se recorta
command Línea de comandos del shell, limitada a 1000 caracteres. Presente solo para tareas shell
agent_type Nombre del tipo de subagente. Presente solo para tareas subagent
server Nombre del servidor MCP. Presente solo para tareas monitor y MCP task
tool Nombre de la herramienta MCP. Presente solo para tareas monitor y MCP task
name Nombre del workflow. Presente solo para tareas workflow

Cada entrada de session_crons describe una reactivación programada con alcance de sesión, proveniente de CronCreate, ScheduleWakeup y /loop:

Campo Descripción
id Identificador de la tarea cron
schedule Expresión cron, por ejemplo 0 9 * * 1-5
recurring false para reactivaciones únicas cuyo programa codifica un único momento de disparo, true para tareas que se vuelven a disparar en cada coincidencia
prompt Prompt que se envía cuando se dispara el cron, limitado a 1000 caracteres con el mismo marcador … [+N chars]

Este ejemplo muestra una entrada de Stop con una tarea de shell en curso y un cron recurrente:

{
  "session_id": "abc123",
  "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "Stop",
  "stop_hook_active": true,
  "last_assistant_message": "I've completed the refactoring. Here's a summary...",
  "background_tasks": [
    {
      "id": "task-001",
      "type": "shell",
      "status": "running",
      "description": "tail logs",
      "command": "tail -f /var/log/syslog"
    }
  ],
  "session_crons": [
    {
      "id": "cron-001",
      "schedule": "0 9 * * 1-5",
      "recurring": true,
      "prompt": "check the build"
    }
  ]
}

Control de decisiones de Stop

Los hooks Stop y SubagentStop pueden controlar si Claude continúa. Además de los campos de salida JSON disponibles para todos los hooks, tu script de hook puede devolver estos campos específicos del evento:

Campo Descripción
decision "block" impide que Claude se detenga. Omítelo para permitir que Claude se detenga
reason Obligatorio cuando decision es "block". Le indica a Claude por qué debe continuar
hookSpecificOutput.additionalContext Retroalimentación para Claude que no es de error. La conversación continúa para que Claude pueda actuar en consecuencia, pero, a diferencia de decision: "block", se muestra en la transcripción como retroalimentación del hook en lugar de como un error del hook

Un hook que bloquea al salir con 2 se enruta igual que reason: Claude recibe el mensaje de stderr como la explicación de por qué debe continuar.

{
  "decision": "block",
  "reason": "Must be provided when Claude is blocked from stopping"
}

Usa additionalContext cuando el hook funciona según lo previsto y le da orientación a Claude, como "ejecuta el conjunto de pruebas antes de terminar". Mantiene la conversación en marcha con las mismas protecciones contra bucles que decision: "block", es decir, la entrada stop_hook_active y el límite de 8 continuaciones consecutivas, pero la transcripción lo etiqueta como Stop hook feedback y no se muestra ninguna notificación de error del hook:

{
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "additionalContext": "Please run the test suite before finishing"
  }
}

StopFailure

Se ejecuta en lugar de Stop cuando el turno termina debido a un error de la API. Claude Code ignora la salida y el código de salida del hook, excepto terminalSequence. Úsalo para registrar fallos, enviar alertas o tomar medidas de recuperación cuando Claude no puede completar una respuesta debido a rate limits, problemas de autenticación u otros errores de la API.

Entrada de StopFailure

Además de los campos de entrada comunes, los hooks StopFailure reciben error, error_details opcional y last_assistant_message opcional. El campo error identifica el tipo de error y se usa para el filtrado del matcher.

Campo Descripción
error Tipo de error: rate_limit, overloaded, authentication_failed, oauth_org_not_allowed, account_on_hold, billing_error, invalid_request, model_not_found, server_error, max_output_tokens, cloud_credential_error o unknown
error_details Detalles adicionales sobre el error, cuando están disponibles
last_assistant_message El texto de error renderizado que se muestra en la conversación. A diferencia de Stop y SubagentStop, donde este campo contiene la salida conversacional de Claude, en StopFailure contiene la propia cadena de error de la API, como "API Error: Rate limit reached"
{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "StopFailure",
  "error": "rate_limit",
  "error_details": "429 Too Many Requests",
  "last_assistant_message": "API Error: Rate limit reached"
}

Los hooks StopFailure no tienen control de decisiones. Se ejecutan solo con fines de notificación y registro.

TeammateIdle

Se ejecuta cuando un compañero de un equipo de agentes está a punto de quedar inactivo después de terminar su turno. Úsalo para aplicar controles de calidad antes de que un compañero deje de trabajar, como exigir que pasen las comprobaciones de lint o verificar que existan los archivos de salida.

Los hooks TeammateIdle no admiten matchers y se disparan en cada ocurrencia.

Entrada de TeammateIdle

Además de los campos de entrada comunes, los hooks TeammateIdle reciben teammate_name y team_name.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "TeammateIdle",
  "teammate_name": "researcher",
  "team_name": "session-a1b2c3d4"
}
Campo Descripción
teammate_name Nombre del compañero que está a punto de quedar inactivo
team_name Obsoleto. Nombre del equipo derivado de la sesión; se eliminará en una versión futura
agent_id En este evento, el campo de entrada común identifica al compañero en proceso que está a punto de quedar inactivo. Puede estar ausente. Requiere Claude Code v2.1.290 o posterior

Control de decisiones de TeammateIdle

Los hooks TeammateIdle admiten dos formas de controlar el comportamiento de los compañeros:

  • Código de salida 2: el compañero recibe el mensaje de stderr como retroalimentación y sigue trabajando en lugar de quedar inactivo.
  • JSON {"continue": false, "stopReason": "..."}: detiene al compañero por completo, igual que el comportamiento del hook Stop. El stopReason se muestra al usuario.

Este ejemplo comprueba que exista un artefacto de compilación antes de permitir que un compañero quede inactivo:

#!/bin/bash

if [ ! -f "./dist/output.js" ]; then
  echo "Build artifact missing. Run the build before stopping." >&2
  exit 2
fi

exit 0

ConfigChange

Se ejecuta cuando un archivo de configuración cambia durante una sesión. Úsalo para auditar cambios de configuración, aplicar políticas de seguridad o bloquear modificaciones no autorizadas en los archivos de configuración.

Claude Code ejecuta los hooks ConfigChange cuando cambia un archivo de configuración, un archivo de política administrada o un archivo de skill. Para la política administrada, los ejecuta solo cuando cambia managed-settings.json o un archivo en managed-settings.d/. Aplica la configuración administrada por el servidor y los cambios en las preferencias administradas de macOS o en la política del registro de Windows sin ejecutarlos. En WSL con wslInheritsWindowsSettings, también aplica un archivo de configuración administrada del lado de Windows que haya cambiado, en su sondeo de políticas, sin ejecutarlos.

El matcher filtra según el origen de la configuración:

Matcher Cuándo se dispara
user_settings Cambia ~/.claude/settings.json
project_settings Cambia .claude/settings.json
local_settings Cambia .claude/settings.local.json
policy_settings Cambia managed-settings.json o un archivo en managed-settings.d/
skills Cambia un archivo de skill en .claude/skills/

Este ejemplo registra todos los cambios de configuración para auditorías de seguridad:

{
  "hooks": {
    "ConfigChange": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/audit-config-change.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

Entrada de ConfigChange

Además de los campos de entrada comunes, los hooks ConfigChange reciben source y, opcionalmente, file_path. El campo source indica qué tipo de configuración cambió, y file_path proporciona la ruta al archivo específico que se modificó.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "ConfigChange",
  "source": "project_settings",
  "file_path": "/Users/.../my-project/.claude/settings.json"
}

Control de decisiones de ConfigChange

Los hooks ConfigChange pueden impedir que los cambios de configuración surtan efecto. Usa el código de salida 2 o una decision JSON para impedir el cambio. Cuando se bloquea, la nueva configuración no se aplica a la sesión en ejecución.

Campo Descripción
decision "block" impide que se aplique el cambio de configuración. Omítelo para permitir el cambio
reason Se acepta, pero nunca se muestra
{
  "decision": "block",
  "reason": "Configuration changes to project settings require admin approval"
}

Los cambios de policy_settings no se pueden bloquear. Los hooks siguen disparándose para los orígenes policy_settings cuando cambia un archivo de configuración administrada en la máquina, por lo que puedes usarlos para registrar esas ediciones, pero cualquier decisión de bloqueo se ignora. Esto garantiza que la configuración administrada por la empresa siempre surta efecto. Claude Code no ejecuta hooks ConfigChange cuando llega o se actualiza la configuración administrada por el servidor.

Claude Code actúa según la decisión de bloqueo de la salida JSON de un hook ConfigChange y descarta systemMessage y continue. Un cambio bloqueado no muestra ningún mensaje ni a ti ni a Claude, tanto si bloqueas con reason como con stderr al salir con 2. Claude Code solo escribe una línea en el registro de depuración.

CwdChanged

Se ejecuta cuando un comando de shell en la conversación principal cambia el directorio de trabajo, por ejemplo cuando Claude ejecuta un comando cd. Úsalo para reaccionar a los cambios de directorio: recargar variables de entorno, activar toolchains específicas del proyecto o ejecutar scripts de configuración automáticamente. Se combina con FileChanged para herramientas como direnv que administran el entorno por directorio.

Los hooks CwdChanged tienen acceso a CLAUDE_ENV_FILE. Si tu hook escribe variables en ese archivo, persisten en los comandos Bash posteriores hasta el siguiente evento CwdChanged, cuando Claude Code las borra.

CwdChanged no admite matchers y se dispara en cada ocurrencia.

Entrada de CwdChanged

Además de los campos de entrada comunes, los hooks CwdChanged reciben old_cwd y new_cwd.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
  "cwd": "/Users/my-project/src",
  "hook_event_name": "CwdChanged",
  "old_cwd": "/Users/my-project",
  "new_cwd": "/Users/my-project/src"
}

Salida de CwdChanged

Además de los campos de salida JSON disponibles para todos los hooks, los hooks CwdChanged pueden devolver watchPaths para establecer dinámicamente qué rutas de archivo vigila FileChanged:

Campo Descripción
watchPaths Array de rutas absolutas. Reemplaza la lista dinámica de vigilancia actual. Las rutas de tu configuración de matcher siempre se vigilan. Devolver un array vacío borra la lista dinámica, lo cual es habitual al entrar en un directorio nuevo

Los hooks CwdChanged no tienen control de decisiones. No pueden bloquear el cambio de directorio.

Claude Code lee watchPaths y systemMessage de su salida JSON y descarta continue. En las sesiones interactivas, muestra el systemMessage como una breve notificación en la terminal. El mensaje no llega al flujo de mensajes del SDK.

DirectoryAdded

Se ejecuta después de que agregas un directorio de trabajo a mitad de la sesión con el comando /add-dir, o después de que un cliente del SDK agrega uno con la solicitud de control register_repo_root. Úsalo para preparar un repositorio recién agregado, por ejemplo instalando sus dependencias.

Claude Code no dispara este evento cuando:

  • Pasas un directorio con el flag de inicio --add-dir; SessionStart cubre esos directorios
  • Agregas un directorio en la pestaña Workspace de /permissions
  • Agregas un directorio que ya es un directorio de trabajo o que está dentro de uno

Claude Code dispara DirectoryAdded después de actualizar el estado del sandbox y de los permisos, por lo que las herramientas en el sandbox ya ven el nuevo directorio cuando se ejecuta tu hook. Los comandos de los hooks en sí se ejecutan fuera del sandbox.

Claude Code no espera al hook: la adición se completa inmediatamente, y el hook se ejecuta en segundo plano con el tiempo de espera predeterminado de 600 segundos.

El matcher filtra según cómo se agregó el directorio:

Matcher Cuándo se dispara
slash_command Agregas un directorio con /add-dir
register_repo_root Un cliente del SDK agrega un directorio con la solicitud de control register_repo_root

Entrada de DirectoryAdded

Además de los campos de entrada comunes, los hooks DirectoryAdded reciben directory y source.

Campo Descripción
directory Ruta absoluta del directorio que se agregó
source Cómo se agregó el directorio: "slash_command" para /add-dir o "register_repo_root" para la solicitud de control del SDK
{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
  "cwd": "/Users/my-project",
  "hook_event_name": "DirectoryAdded",
  "directory": "/Users/my-other-repo",
  "source": "slash_command"
}

Los hooks DirectoryAdded no tienen control de decisiones. No pueden bloquear la adición, que ya se completó cuando se ejecuta el hook. Claude Code descarta el campo continue de su salida JSON y muestra el resto de forma diferente según el origen:

  • slash_command: Claude Code entrega el systemMessage del hook a Claude como contexto en el siguiente turno de la conversación, en lugar de mostrártelo a ti. En la transcripción aparece un recuento de los hooks fallidos. La salida completa de los fallos va al registro de depuración
  • register_repo_root: Claude Code escribe la salida de systemMessage y la salida de los fallos solo en el registro de depuración

FileChanged

Se ejecuta cuando un archivo vigilado cambia en el disco. Claude Code detecta los cambios con un observador del sistema de archivos, no inspeccionando las llamadas a herramientas, por lo que ejecuta el hook sin importar qué cambió el archivo: una llamada a la herramienta Edit o Write, un script que Claude ejecuta con Bash, o un proceso completamente ajeno a Claude Code. Un uso común es recargar variables de entorno cuando cambian los archivos de configuración del proyecto.

El matcher de este evento cumple dos funciones:

  • Construir la lista de vigilancia: el valor se divide por | y cada segmento se registra como un nombre de archivo literal en el directorio de trabajo, así que ".envrc|.env" vigila exactamente esos dos archivos. Los patrones regex no sirven aquí: un valor como ^\.env vigilaría un archivo llamado literalmente ^\.env.
  • Filtrar qué hooks se ejecutan: cuando cambia un archivo vigilado, el mismo valor filtra qué grupos de hooks se ejecutan usando las reglas de matcher estándar contra el nombre base del archivo modificado.

Este ejemplo normaliza los finales de línea en data.csv después de cualquier cambio, incluido un comando Bash o un script externo que reescriba el archivo:

{
  "hooks": {
    "FileChanged": [
      {
        "matcher": "data.csv",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/normalize-line-endings.sh"
          }
        ]
      }
    ]
  }
}

El hook lee la ruta absoluta del archivo modificado desde el campo file_path de la entrada JSON en stdin. Su comprobación con grep busca lo mismo que elimina perl, un CR al final de una línea, así que la ejecución posterior a una normalización termina sin tocar el archivo. Una comprobación más laxa entra en un bucle infinito, porque perl -i reescribe el archivo aunque no sustituya nada y Claude Code vuelve a ejecutar el hook después de cada reescritura. Guarda este script en /path/to/normalize-line-endings.sh y hazlo ejecutable:

#!/bin/bash
FILE=$(jq -r .file_path)
if grep -q $'\r$' "$FILE"; then
  perl -pi -e 's/\r$//' "$FILE"
fi

Para confirmar que el hook funciona, pídele a Claude que agregue una línea CRLF a data.csv con un comando Bash. Claude Code ejecuta el hook y el archivo termina con finales de línea LF.

Para vigilar archivos que no puedes nombrar de antemano, devuelve watchPaths desde un hook para actualizar la lista de vigilancia dinámicamente. Claude Code inicia el observador solo cuando algo nombra un archivo que vigilar, así que inicializa la lista con un grupo FileChanged cuyo matcher nombre al menos un archivo, o con un hook SessionStart o CwdChanged que devuelva watchPaths. El matcher sigue filtrando qué grupos de hooks se ejecutan cuando cambia un archivo vigilado, así que omite el matcher en el grupo que maneja las rutas dinámicas: así coincide con todos los archivos vigilados y no agrega nada a la lista de vigilancia. Un matcher "*" también coincide con todos los archivos, pero Claude Code lo registra en la lista de vigilancia como cualquier otro valor, como un archivo literal llamado *.

Los hooks FileChanged tienen acceso a CLAUDE_ENV_FILE. Las variables escritas en ese archivo persisten en los comandos Bash posteriores hasta el siguiente evento CwdChanged, cuando Claude Code las borra.

Entrada de FileChanged

Además de los campos de entrada comunes, los hooks FileChanged reciben file_path y event.

Campo Descripción
file_path Ruta absoluta al archivo que cambió
event Qué ocurrió: "change" para un archivo modificado, "add" para un archivo creado o "unlink" para un archivo eliminado
{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
  "cwd": "/Users/my-project",
  "hook_event_name": "FileChanged",
  "file_path": "/Users/my-project/.envrc",
  "event": "change"
}

Salida de FileChanged

Además de los campos de salida JSON disponibles para todos los hooks, los hooks FileChanged pueden devolver watchPaths para actualizar dinámicamente qué rutas de archivo se vigilan:

Campo Descripción
watchPaths Array de rutas absolutas. Reemplaza la lista de vigilancia dinámica actual. Las rutas de tu configuración de matcher siempre se vigilan. Úsalo cuando tu script de hook descubra archivos adicionales que vigilar según el archivo modificado

Los hooks FileChanged no tienen control de decisión. No pueden impedir que ocurra el cambio del archivo.

Claude Code lee watchPaths y systemMessage de su salida JSON y descarta continue. En sesiones interactivas, muestra el systemMessage como una breve notificación en la terminal. El mensaje no llega al flujo de mensajes del SDK.

WorktreeCreate

Se ejecuta cuando Claude Code crea un worktree en casos como estos:

Un hook WorktreeCreate no se ejecuta para un worktree que Claude agrega mediante Bash, y el hook de un plugin no se ejecuta para el que se crea cuando inicias con --worktree y --tmux:

  • git worktree add mediante Bash: para inspeccionar o bloquear ese comando, agrega un hook PreToolUse con el matcher Bash.
  • --worktree con --tmux: Claude Code crea el worktree antes de cargar los plugins, por lo que un hook WorktreeCreate de un plugin no se ejecuta. Para que tu hook se ejecute con ambos flags, defínelo en un archivo de configuración. Antes de v2.1.300, el hook WorktreeCreate de un plugin tampoco se ejecutaba solo con --worktree.

De forma predeterminada, Claude Code crea la copia de trabajo aislada con git worktree. Configurar un hook WorktreeCreate reemplaza ese comportamiento predeterminado de git, lo que te permite usar un sistema de control de versiones diferente como SVN, Perforce o Mercurial.

Como el hook reemplaza por completo el comportamiento predeterminado, .worktreeinclude no se procesa. Si necesitas copiar archivos de configuración locales como .env al nuevo worktree, hazlo dentro de tu script de hook.

El hook debe devolver la ruta al directorio del worktree creado. Claude Code usa esta ruta como directorio de trabajo para la sesión aislada. Consulta Salida de WorktreeCreate para ver cómo devuelve la ruta cada tipo de hook.

Claude Code actúa según el éxito del hook y la ruta devuelta, y descarta systemMessage y continue.

Este ejemplo crea una copia de trabajo de SVN e imprime la ruta para que Claude Code la use. Reemplaza la URL del repositorio por la tuya:

{
  "hooks": {
    "WorktreeCreate": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'NAME=$(jq -r .name); DIR=\"$HOME/.claude/worktrees/$NAME\"; svn checkout https://svn.example.com/repo/trunk \"$DIR\" >&2 && echo \"$DIR\"'"
          }
        ]
      }
    ]
  }
}

El hook lee el name del worktree de la entrada JSON en stdin, extrae una copia nueva en un directorio nuevo e imprime la ruta del directorio. El echo de la última línea es lo que Claude Code lee como la ruta del worktree. Redirige cualquier otra salida a stderr para que no interfiera con la ruta.

Entrada de WorktreeCreate

Además de los campos de entrada comunes, los hooks WorktreeCreate reciben el campo name. Es un identificador tipo slug para el nuevo worktree, especificado por el usuario o generado automáticamente, por ejemplo bold-oak-a3f2.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "WorktreeCreate",
  "name": "feature-auth"
}

Salida de WorktreeCreate

Los hooks WorktreeCreate no usan el modelo estándar de decisión de permitir/bloquear. En su lugar, el éxito o el fallo del hook determina el resultado. El hook debe devolver la ruta al directorio del worktree creado:

  • Hooks de comando (type: "command"): imprime la ruta como la última línea no vacía de stdout. Claude Code elimina los códigos de escape ANSI antes de leer esa línea, así que los banners de inicio del shell impresos antes de tu echo se ignoran. Redirige cualquier otra salida del hook a stderr.
  • Hooks HTTP (type: "http"): devuelve { "hookSpecificOutput": { "hookEventName": "WorktreeCreate", "worktreePath": "/absolute/path" } } en el cuerpo de la respuesta.

Si el hook falla o no produce ninguna ruta, la creación del worktree falla con un error. Claude Code resuelve una ruta relativa respecto al directorio en el que se ejecutó el hook, colapsando cualquier segmento . o .. que contenga. Si la ruta resultante no es un directorio al que Claude Code pueda entrar, la sesión imprime un error que nombra la ruta y termina con el código 1.

Claude Code rechaza una ruta absoluta que contenga segmentos . o .., y cualquier ruta que pase por un enlace simbólico debajo de la raíz del repositorio. El error nombra el componente rechazado. Un enlace simbólico incluido en un commit del repositorio podría redirigir el worktree fuera de él. Devuelve una ruta normalizada que no pase por un enlace simbólico dentro del repositorio.

WorktreeRemove

Se ejecuta cuando Claude Code limpia un worktree que creó tu hook WorktreeCreate. El evento se dispara cuando:

  • Sales de una sesión de worktree interactiva y eliges eliminar el worktree cuando Claude Code te lo pregunta
  • Sales de una sesión de worktree interactiva a la que no le has puesto nombre, Claude Code no encuentra archivos modificados ni sin seguimiento, y elimina el worktree sin preguntarte
  • Eliminas una sesión en segundo plano que se ejecuta en el worktree
  • Le pides a Claude que salga y elimine el worktree durante una sesión, y Claude lo elimina con la herramienta ExitWorktree
  • Una sesión que claude remote-control inició en modo worktree termina sin fallar, y Claude Code no encuentra archivos modificados ni sin seguimiento ni commits nuevos en su worktree
  • Detienes claude remote-control mientras las sesiones que inició en modo worktree siguen ejecutándose, y Claude Code no encuentra archivos modificados ni sin seguimiento ni commits nuevos en sus worktrees

Cuando un subagente o un agente de un flujo de trabajo termina, Claude Code conserva el worktree que tu hook WorktreeCreate creó para él y no ejecuta tu hook WorktreeRemove. Elimina esos worktrees tú mismo cuando termine el trabajo.

Claude Code no te pregunta antes de que tu hook se ejecute para una llamada a ExitWorktree o una limpieza de claude remote-control. ExitWorktree se niega a eliminar un worktree creado por un hook a menos que la llamada pase discard_changes: true, lo que convierte a tu hook en la última comprobación en esa vía.

Claude Code usa git para buscar archivos modificados o sin seguimiento, así que no encuentra ninguno en un worktree que no sea un checkout de git ni esté dentro de uno, aunque el directorio contenga trabajo sin commit. Comprueba ese trabajo en tu hook WorktreeRemove antes de que elimine nada.

Para los worktrees basados en git, Claude Code gestiona la limpieza automáticamente con git worktree remove. Si configuraste un hook WorktreeCreate, combínalo con un hook WorktreeRemove para controlar la limpieza:

  • Sin hook WorktreeRemove: Claude Code recurre a git para una eliminación al salir o mediante ExitWorktree, y conserva el worktree cuando claude remote-control hace la limpieza:
    • Sales de una sesión de worktree, o Claude llama a ExitWorktree: Claude Code elimina la ruta que devolvió tu hook WorktreeCreate como lo haría git worktree remove --force, así que se elimina un worktree que git reconoce. Un worktree que git no reconoce, por ejemplo uno que tu hook creó con un sistema de control de versiones distinto de git, permanece en el disco.
    • claude remote-control limpia el worktree de una sesión: el worktree permanece en el disco, y la terminal que ejecuta claude remote-control muestra worktree removal failed, kept: <path>.
    • Eliminas una sesión en segundo plano: consulta Qué elimina borrar una sesión.
  • El hook termina con 0: el worktree cuenta como eliminado. Claude Code no lee nada más del hook, así que asegúrate de que tu hook haya eliminado el directorio.
  • El hook termina con un código distinto de cero: la eliminación falla si el directorio en worktree_path sigue existiendo después, y el worktree permanece en el disco sin recurrir a git. Un hook que eliminó el directorio antes de terminar con un código distinto de cero cuenta como eliminado. Para saber cómo se informa el fallo, consulta Entrada de WorktreeRemove.

Claude Code nunca elimina una rama que pertenezca a un worktree creado por un hook, porque solo conoce la ruta que devolvió tu hook WorktreeCreate. Si tu hook WorktreeCreate crea una rama, elimínala en tu hook WorktreeRemove. Claude Code descarta los campos de salida JSON de un hook WorktreeRemove, como systemMessage y continue.

Al eliminar una sesión en segundo plano, Claude Code verifica la ruta del worktree almacenada antes de ejecutar el hook, y rechaza una ruta que sea un enlace simbólico o pase por uno debajo de la raíz del repositorio. En esa vía, el hook se ejecuta para un worktree que todavía contiene archivos solo cuando confirmas la eliminación en la vista de agentes. Para un worktree así, claude rm conserva la sesión y el worktree.

Claude Code pasa la ruta devuelta por WorktreeCreate como worktree_path en la entrada del hook. Este ejemplo lee esa ruta y elimina el directorio:

{
  "hooks": {
    "WorktreeRemove": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'jq -r .worktree_path | xargs rm -rf'"
          }
        ]
      }
    ]
  }
}

Entrada de WorktreeRemove

Además de los campos de entrada comunes, los hooks WorktreeRemove reciben el campo worktree_path, que es la ruta absoluta al worktree que se está eliminando.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "WorktreeRemove",
  "worktree_path": "/Users/.../my-project/.claude/worktrees/feature-auth"
}

El código de salida de un hook WorktreeRemove decide el resultado. Cuando un hook termina con un código distinto de cero y el directorio en worktree_path sigue existiendo después, la eliminación falla:

  • El worktree permanece en el disco, y el comando del hook y su stderr van al registro de depuración.
  • Si estabas eliminando una sesión en segundo plano, la sesión también permanece. El mensaje de rechazo en la vista de agentes informa cómo terminó el hook, por ejemplo exited 1, cita el comienzo de su stderr e indica si volver a eliminar la sesión elimina el directorio de todos modos.
  • Si claude remote-control estaba limpiando el worktree de una sesión, la terminal que lo ejecuta muestra worktree removal failed, kept: <path>.

PreCompact

Se ejecuta antes de que Claude Code vaya a ejecutar una operación de compactación.

El valor del matcher indica si la compactación se activó manual o automáticamente:

Matcher Cuándo se dispara
manual /compact
auto Compactación automática cuando la conversación alcanza la ventana de compactación automática, y compactación por inactividad

Termina con el código 2 para bloquear la compactación. Para un /compact manual, el mensaje de stderr se muestra al usuario. También puedes bloquear devolviendo JSON con "decision": "block".

Bloquear la compactación automática tiene efectos distintos según cuándo se dispare. Si la compactación se activó de forma proactiva antes del límite de contexto, Claude Code la omite y la conversación continúa sin compactar. Si la compactación se activó para recuperarse de un error de límite de contexto ya devuelto por la API, aparece el error subyacente y la solicitud actual falla.

Claude Code descarta los campos systemMessage y continue de un hook PreCompact.

Entrada de PreCompact

Además de los campos de entrada comunes, los hooks PreCompact reciben trigger y custom_instructions. Para manual, custom_instructions contiene lo que el usuario pasa a /compact y es null cuando no pasa nada. Para auto, custom_instructions es null.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "PreCompact",
  "trigger": "manual",
  "custom_instructions": null
}

Salida de PreCompact

Cuando un hook PreCompact termina con 0 y no bloquea, Claude Code usa su stdout como instrucciones para el resumen de la compactación. Mantén ese stdout por debajo de 10.000 caracteres por hook. Claude Code corta la salida más larga en el límite y agrega un marcador.

PostCompact

Se ejecuta después de que Claude Code completa una operación de compactación. Usa este evento para reaccionar al nuevo estado compactado, por ejemplo para registrar el resumen generado o actualizar un estado externo. Claude Code descarta los campos systemMessage y continue de un hook PostCompact.

Se aplican los mismos valores de matcher que para PreCompact:

Matcher Cuándo se dispara
manual Después de /compact
auto Después de la compactación automática cuando la conversación alcanza la ventana de compactación automática, y después de la compactación por inactividad

Entrada de PostCompact

Además de los campos de entrada comunes, los hooks PostCompact reciben trigger y compact_summary. El campo compact_summary contiene el resumen de la conversación generado por la operación de compactación.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "PostCompact",
  "trigger": "manual",
  "compact_summary": "Summary of the compacted conversation..."
}

Los hooks PostCompact no tienen control de decisión. No pueden afectar el resultado de la compactación, pero pueden realizar tareas posteriores.

PreModelSwitch

Se ejecuta antes de que Claude Code aplique un cambio de modelo que tú o un cliente solicitaron. Úsalo para bloquear un cambio, exigir confirmación o mostrar lo que costará el cambio antes de que ocurra.

PreModelSwitch requiere Claude Code v2.1.251 o posterior. Claude Code lo ejecuta para estas solicitudes:

  • /model <name> y el selector de /model
  • El selector de modelo con Option+P o Alt+P
  • El ajuste Model en /config
  • Activar el modo rápido cuando eso cambia el modelo de la sesión
  • Una solicitud set_model, o un cambio de modelo en una solicitud apply_flag_settings, desde un host del Agent SDK o Remote Control

Claude Code no ejecuta hooks PreModelSwitch para los cambios que hace por su cuenta, como un respaldo automático de modelo o la restauración del modelo cuando reanudas una sesión. Esos cambios solo llegan a PostModelSwitch.

Claude Code compara el matcher con el nombre canónico del modelo al que cambia la sesión, ignorando cualquier sufijo [1m]. Un alias como opus, un ID de modelo con fecha y un ID específico del proveedor, como un ID de modelo de Amazon Bedrock, coinciden todos con el único nombre canónico al que se resuelven, así que claude-opus-5 cubre todas las formas de escribir Opus 5.

Cuando Claude Code no puede determinar un nombre canónico para el destino, por ejemplo un ID de modelo personalizado que solo conoce tu gateway de LLM, ejecuta todos los hooks PreModelSwitch independientemente del matcher. Por eso, un hook que bloquea debe comprobar to_model en su entrada en lugar de depender solo del matcher.

Escribe el matcher como un nombre exacto, una lista separada por | como claude-opus-4-6|claude-opus-5, o una expresión regular como .*opus.*. Este ejemplo usa un matcher de nombre exacto y además comprueba to_model en la entrada del hook, así que rechaza un cambio a Opus 4.6 terminando con el código 2 y deja pasar cualquier otro destino:

[tab] macOS/Linux

El comando comprueba to_model con jq:

{
  "hooks": {
    "PreModelSwitch": [
      {
        "matcher": "claude-opus-4-6",
        "hooks": [
          {
            "type": "command",
            "command": "jq -e '.to_model | test(\"opus-4-6\")' > /dev/null && { echo 'Opus 4.6 is retired for this project. Use a newer model.' >&2; exit 2; }; exit 0"
          }
        ]
      }
    ]
  }
}

[tab] Windows (PowerShell)

Registra un hook de comando que ejecute un script mediante PowerShell:

{
  "hooks": {
    "PreModelSwitch": [
      {
        "matcher": "claude-opus-4-6",
        "hooks": [
          {
            "type": "command",
            "command": "powershell.exe",
            "args": [
              "-NoProfile",
              "-ExecutionPolicy",
              "Bypass",
              "-File",
              "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-opus-46.ps1"
            ]
          }
        ]
      }
    ]
  }
}

Guarda este script en .claude/hooks/block-opus-46.ps1 en tu proyecto:

$hookInput = [Console]::In.ReadToEnd() | ConvertFrom-Json
if ($hookInput.to_model -match 'opus-4-6') {
  [Console]::Error.WriteLine('Opus 4.6 is retired for this project. Use a newer model.')
  exit 2
}
exit 0

Para confirmar que el hook funciona, ejecuta /model claude-opus-4-6 desde una sesión que use un modelo diferente. Claude Code mantiene el modelo actual e informa que un hook PreModelSwitch bloqueó el cambio, con tu mensaje como motivo.

Entrada de PreModelSwitch

Además de los campos de entrada comunes, los hooks PreModelSwitch reciben los campos de esta tabla. Los últimos cinco describen lo que cuesta reenviar la conversación al nuevo modelo, para que un hook pueda mostrar esa cifra antes de que ocurra el cambio.

Campo Tipo Descripción
from_model string ID del modelo desde el que se cambia
to_model string ID del modelo al que se cambia. El matcher se compara con el nombre canónico de este modelo
requested_model string o null El modelo que nombró la solicitud: un alias como opus, un ID de modelo completo, o null cuando la solicitud era para el modelo predeterminado
source string De dónde vino la solicitud: "command" para /model <name>, el ajuste Model en /config o la activación del modo rápido; "picker" para un selector de modelo; "sdk" para una solicitud set_model, o un cambio de modelo en una solicitud apply_flag_settings, desde un host del Agent SDK o Remote Control
context_tokens number Tokens que la siguiente solicitud reenvía como su prompt: los tokens de entrada, de lectura de caché, de creación de caché y de salida de la última respuesta en la conversación principal, combinados. 0 antes de la primera respuesta
prompt_cache_warm boolean Si es probable que la caché de prompts del modelo actual siga caliente, lo que significa que el cambio la pierde
cache_ttl string Duración de la caché de prompts de la conversación principal en el momento de este cambio: "5m" o "1h"
estimated_cache_write_usd number Costo estimado en dólares estadounidenses de escribir context_tokens en la caché de prompts de to_model a la tarifa de cache_ttl, sin incluir la siguiente respuesta. Es posible que el servidor no necesite volver a almacenar en caché todo el contexto, así que trátalo como una estimación
pricing string Cómo calculó Claude Code el precio de estimated_cache_write_usd: "configured" con las tarifas propias de tu organización cuando las ha configurado, "catalog" a precio de lista, o "default" cuando to_model no tiene un precio conocido y Claude Code asumió una tarifa predeterminada

Este ejemplo muestra la entrada para /model opus en una sesión que usa Sonnet 5:

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "PreModelSwitch",
  "from_model": "claude-sonnet-5",
  "to_model": "claude-opus-5",
  "requested_model": "opus",
  "source": "command",
  "context_tokens": 182340,
  "prompt_cache_warm": true,
  "cache_ttl": "5m",
  "estimated_cache_write_usd": 1.1396,
  "pricing": "catalog"
}

Control de decisión de PreModelSwitch

Los hooks PreModelSwitch pueden cancelar el cambio, pedirle al usuario que lo confirme o dejar que continúe. El código de salida 2 o un decision: "block" de nivel superior cancela el cambio.

Para un control más fino, devuelve permissionDecision y permissionDecisionReason en un objeto hookSpecificOutput, como en PreToolUse. PreModelSwitch acepta "allow", "deny" y "ask". No acepta "defer", updatedInput ni additionalContext. La siguiente tabla describe ambos campos:

Campo Descripción
permissionDecision "allow" continúa y omite la confirmación que Claude Code muestra mientras la caché de prompts está caliente. "deny" cancela el cambio. "ask" le pide al usuario que lo confirme
permissionDecisionReason Para "deny", se muestra al usuario como el motivo por el que se bloqueó el cambio, o se devuelve como el error de una solicitud set_model. Para "ask", se muestra en la solicitud de confirmación. Se ignora para "allow"

Solo /model en una sesión interactiva puede mostrar la solicitud de "ask". En todas las demás superficies, incluido el modo no interactivo con el flag -p, /config y las solicitudes set_model, Claude Code trata "ask" como un rechazo.

Este ejemplo le pide al usuario que confirme y cita el recuento de tokens de context_tokens:

{
  "hookSpecificOutput": {
    "hookEventName": "PreModelSwitch",
    "permissionDecision": "ask",
    "permissionDecisionReason": "Switching now re-sends about 180k tokens to the new model. Continue?"
  }
}

Cuando varios hooks PreModelSwitch devuelven decisiones diferentes, la precedencia es deny > ask > allow.

Claude Code le muestra al usuario cualquier systemMessage que devuelva tu hook independientemente de la decisión, así que un hook de informe de costos puede devolver {"systemMessage": "..."} y terminar con 0.

Tiempos de espera y otros códigos de salida de PreModelSwitch

Un hook PreModelSwitch que no responde antes de que se agote su tiempo de espera bloquea el cambio. El tiempo de espera predeterminado para este evento es de 30 segundos.

Un código de salida distinto de 0 o 2 sin una salida JSON válida es un error no bloqueante, por lo que el cambio continúa, excepto para los hooks cuyo fallo lo bloquea.

PostModelSwitch

Se ejecuta después de que cambia el modelo de la sesión. Úsalo para darle a Claude orientación específica del modelo sin editar cada CLAUDE.md, por ejemplo una instrucción para toda la organización que se aplica en ciertos modelos.

PostModelSwitch requiere Claude Code v2.1.251 o posterior. No puede bloquear, porque el modelo ya cambió. Claude Code ejecuta hooks PostModelSwitch después de cualquiera de estos cambios:

  • Un cambio que tú o un cliente solicitaron
  • Un respaldo automático de modelo, que cambia el modelo de la sesión
  • Un ajuste como opusplan al entrar o salir del modo plan
  • Claude Code restaurando el modelo cuando reanudas una sesión

Claude Code no ejecuta hooks PostModelSwitch cuando un modelo de una cadena de modelos de respaldo atiende un turno, porque esa sustitución dura un turno y deja sin cambios el modelo de la sesión.

El matcher sigue las mismas reglas que PreModelSwitch: Claude Code lo compara con el nombre canónico del modelo al que cambió la sesión.

Este ejemplo agrega orientación cada vez que el modelo de la sesión cambia a cualquier modelo Opus:

{
  "hooks": {
    "PostModelSwitch": [
      {
        "matcher": ".*opus.*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'On Opus, delegate implementation work to subagents and keep this conversation for planning and review.'"
          }
        ]
      }
    ]
  }
}

Para confirmar que el hook funciona, cambia a un modelo Opus desde una sesión que use un modelo diferente, por ejemplo ejecuta /model opus desde una sesión de Sonnet, y luego pregúntale a Claude qué orientación tiene sobre el modelo actual.

Entrada de PostModelSwitch

Los hooks PostModelSwitch reciben los mismos campos que PreModelSwitch, con hook_event_name establecido en "PostModelSwitch" y dos valores más de source: "auto" para un respaldo automático u otro cambio que Claude Code hizo por su cuenta, y "resume" para el modelo restaurado cuando reanudas una sesión.

requested_model es null cuando source es "auto". Cuando source es "resume", es el ajuste de modelo guardado que Claude Code restauró.

Control de decisión de PostModelSwitch

Claude Code toma la stdout de texto plano de tu hook al terminar con 0, o additionalContext de la salida JSON, y se la entrega a Claude con la siguiente solicitud después del cambio. Además de los campos de salida JSON disponibles para todos los hooks, puedes devolver:

Campo Descripción
additionalContext Cadena que se agrega al contexto de Claude con la siguiente solicitud. Consulta Agregar contexto para Claude

Si el hook no ha terminado dentro de los cinco segundos posteriores a que envíes el siguiente prompt, Claude Code envía esa solicitud sin la salida y la adjunta a la solicitud siguiente. Si el modelo cambia varias veces antes de la siguiente solicitud, Claude Code entrega solo la salida correspondiente al modelo de destino del último cambio.

SessionEnd

Se ejecuta cuando termina una sesión de Claude Code. Útil para tareas de limpieza, registrar estadísticas de la sesión o guardar el estado de la sesión. Admite matchers para filtrar por motivo de salida.

El campo reason en la entrada del hook indica por qué terminó la sesión:

Motivo Descripción
clear Sesión borrada con el comando /clear
resume Sesión cambiada mediante /resume interactivo
logout El usuario cerró sesión
prompt_input_exit El usuario salió mientras la entrada del prompt estaba visible
other Otros motivos de salida
bypass_permissions_disabled Eliminado en v2.1.234; Claude Code no lo envía. Quítalo de tus matchers de SessionEnd

Entrada de SessionEnd

Además de los campos de entrada comunes, los hooks SessionEnd reciben un campo reason que indica por qué terminó la sesión. Consulta la tabla de motivos anterior para ver todos los valores.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "SessionEnd",
  "reason": "other"
}

Los hooks SessionEnd no tienen control de decisión. No pueden bloquear la finalización de la sesión, pero pueden realizar tareas de limpieza. Claude Code descarta sus campos de salida JSON, como systemMessage.

Los hooks SessionEnd tienen un tiempo de espera predeterminado de 1.5 segundos. Se aplica cuando sales, ejecutas /clear o cambias de sesión con /resume interactivo. Puedes darle más tiempo a un hook de dos maneras:

  • timeout por hook: establece timeout en la configuración de ese hook. El presupuesto total aumenta automáticamente para igualar el timeout por hook más alto de tus archivos de configuración, hasta 60 segundos. Si aumentas el presupuesto de esta manera, un hook sin su propio timeout sigue conservando el valor predeterminado. Los tiempos de espera establecidos en hooks proporcionados por plugins no aumentan el presupuesto.
  • CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS: establece esta variable de entorno en milisegundos para sobrescribir el presupuesto explícitamente. El valor que establezcas también se convierte en el tiempo de espera de cada hook sin su propio timeout.

Este ejemplo establece el presupuesto en 5 segundos:

CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS=5000 claude

Antes de v2.1.268, CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS solo aumentaba el presupuesto total, y un hook sin su propio timeout se seguía cancelando después de 1.5 segundos.

Elicitation

Se ejecuta cuando un servidor MCP solicita entrada del usuario a mitad de una tarea. De forma predeterminada, Claude Code muestra un diálogo interactivo para que el usuario responda. Los hooks pueden interceptar esta solicitud y responder mediante programación, omitiendo el diálogo por completo.

Para ver un hook completo con su entrada de configuración y su script, consulta Responder una solicitud de formulario desde un script.

El campo matcher se compara con el nombre del servidor MCP.

Entrada de Elicitation

Además de los campos de entrada comunes, los hooks Elicitation reciben mcp_server_name, message y los campos opcionales mode, url, elicitation_id y requested_schema.

Para la elicitación en modo formulario, el caso más común:

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "Elicitation",
  "mcp_server_name": "my-mcp-server",
  "message": "Please provide your credentials",
  "mode": "form",
  "requested_schema": {
    "type": "object",
    "properties": {
      "username": { "type": "string", "title": "Username" }
    }
  }
}

Para la elicitación en modo URL, usada para la autenticación basada en navegador:

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "Elicitation",
  "mcp_server_name": "my-mcp-server",
  "message": "Please authenticate",
  "mode": "url",
  "url": "https://auth.example.com/login"
}

Salida de Elicitation

Un hook Elicitation puede responder la solicitud en nombre del usuario, rechazarla o cancelarla, o dejarla al diálogo. Para responder, rechazar o cancelar, termina con 0 e imprime un objeto hookSpecificOutput con un action. El servidor recibe tu respuesta y no aparece ningún diálogo. Cada fila de esta tabla muestra qué devolver para un resultado y qué recibe el servidor MCP:

Para Devuelve El servidor recibe
Responder en nombre del usuario "action": "accept", con los valores de los campos del formulario en content accept con tu content
Rechazar la solicitud "action": "decline" decline
Cancelar la solicitud "action": "cancel" cancel
Dejar la solicitud al usuario Sin salida, con código de salida 0 La respuesta del usuario desde el diálogo

Esta salida responde la solicitud en modo formulario que se muestra en Entrada de Elicitation. Las claves de content son los nombres de propiedad del requested_schema de esa solicitud:

{
  "hookSpecificOutput": {
    "hookEventName": "Elicitation",
    "action": "accept",
    "content": {
      "username": "alice"
    }
  }
}

Esta salida rechaza una solicitud:

{
  "hookSpecificOutput": {
    "hookEventName": "Elicitation",
    "action": "decline"
  }
}

En el diálogo, seleccionar Decline envía decline y presionar Esc envía cancel, así que devuelve el que quieras que vea el servidor.

Para una solicitud en modo URL, un hook que devuelve accept omite el diálogo, así que la URL nunca se abre.

Claude Code descarta reason, systemMessage y continue de la salida JSON de un hook Elicitation, sea cual sea el action que devuelvas.

Otras formas de rechazar una elicitación

Tu hook también puede rechazar de estas maneras. El servidor recibe el mismo decline que con "action": "decline":

  • Termina con el código 2: Claude Code ignora un hookSpecificOutput impreso por el mismo hook
  • Imprime un "decision": "block" de nivel superior: el bloqueo sobrescribe un action en la misma salida

Cuando varios hooks coinciden con la misma solicitud, un rechazo de uno de ellos sobrescribe un accept o cancel de otro.

Este script rechaza las solicitudes en modo URL y deja las solicitudes de formulario al diálogo:

#!/bin/bash
if [ "$(jq -r '.mode')" = "url" ]; then
  exit 2
fi

Ni el usuario ni el servidor ven por qué tu hook rechazó, porque Claude Code no muestra tu stderr ni tu reason.

Claude Code ignoró un decision de nivel superior de los hooks Elicitation y ElicitationResult desde v2.1.105 hasta la corrección en v2.1.284.

Responder una solicitud de formulario desde un script

Este ejemplo responde una pregunta recurrente en nombre del usuario. Un servidor MCP llamado issue-tracker pide una clave de proyecto en un formulario, y el hook completa DOCS. El script acepta cuando project_key es el único campo del formulario. Para cualquier otra solicitud no imprime nada, así que aparece el diálogo.

[tab] macOS/Linux

Registra un hook de comando para el evento en tu archivo de configuración, con el nombre del servidor como matcher:

{
  "hooks": {
    "Elicitation": [
      {
        "matcher": "issue-tracker",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.sh",
            "args": []
          }
        ]
      }
    ]
  }
}

Guarda este script en .claude/hooks/answer-project-key.sh en tu proyecto y hazlo ejecutable con chmod +x:

#!/bin/bash
input=$(cat)
fields=$(jq -c '.requested_schema.properties // {} | keys' <<<"$input")

if [ "$fields" = '["project_key"]' ]; then
  jq -n '{hookSpecificOutput: {hookEventName: "Elicitation", action: "accept", content: {project_key: "DOCS"}}}'
fi

[tab] Windows (PowerShell)

Registra un hook de comando que ejecute el script mediante PowerShell, con el nombre del servidor como matcher:

{
  "hooks": {
    "Elicitation": [
      {
        "matcher": "issue-tracker",
        "hooks": [
          {
            "type": "command",
            "command": "powershell.exe",
            "args": [
              "-NoProfile",
              "-ExecutionPolicy",
              "Bypass",
              "-File",
              "${CLAUDE_PROJECT_DIR}/.claude/hooks/answer-project-key.ps1"
            ]
          }
        ]
      }
    ]
  }
}

Guarda este script en .claude/hooks/answer-project-key.ps1 en tu proyecto:

$request = [Console]::In.ReadToEnd() | ConvertFrom-Json
$fields = @($request.requested_schema.properties.PSObject.Properties.Name)

if ($fields.Count -eq 1 -and $fields[0] -eq 'project_key') {
  @{
    hookSpecificOutput = @{
      hookEventName = "Elicitation"
      action = "accept"
      content = @{ project_key = "DOCS" }
    }
  } | ConvertTo-Json -Depth 3
}

Para confirmar que el hook funciona, inicia Claude Code con claude --debug y dale a Claude una tarea que haga que el servidor pida la clave del proyecto. No aparece ningún diálogo, y el registro de depuración tiene una línea que termina con Elicitation resolved by hook: {"action":"accept","content":{"project_key":"DOCS"}}.

ElicitationResult

Se ejecuta después de que un usuario responde a una elicitación de MCP. Los hooks pueden observar, modificar o bloquear la respuesta antes de que se envíe de vuelta al servidor MCP.

Cuando un hook Elicitation responde una solicitud, Claude Code envía esa respuesta al servidor sin ejecutar hooks ElicitationResult.

El campo matcher se compara con el nombre del servidor MCP.

Entrada de ElicitationResult

Además de los campos de entrada comunes, los hooks ElicitationResult reciben mcp_server_name, action y los campos opcionales mode, elicitation_id y content.

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "hook_event_name": "ElicitationResult",
  "mcp_server_name": "my-mcp-server",
  "action": "accept",
  "content": { "username": "alice" },
  "mode": "form"
}

Salida de ElicitationResult

Un hook ElicitationResult puede dejar pasar la respuesta del usuario, cambiar sus valores o bloquearla. Para cambiar o bloquear la respuesta, termina con 0 e imprime un objeto hookSpecificOutput con un action. Cada fila de esta tabla muestra qué devolver para un resultado y qué recibe el servidor MCP:

Para Devuelve El servidor recibe
Dejar pasar la respuesta Sin salida, con código de salida 0 La respuesta del usuario, sin cambios
Cambiar los valores enviados "action": "accept", con los nuevos valores en content accept con tu content en lugar de los valores del usuario
Bloquear la respuesta "action": "decline" decline, sin los valores del usuario
Cancelar la solicitud "action": "cancel" cancel, junto con los valores que envió el usuario. Para retenerlos, devuelve "decline"

Esta salida cambia la respuesta que se muestra en Entrada de ElicitationResult, así que el servidor recibe alice@example.com donde el usuario envió alice:

{
  "hookSpecificOutput": {
    "hookEventName": "ElicitationResult",
    "action": "accept",
    "content": {
      "username": "alice@example.com"
    }
  }
}

Tu content reemplaza todo el objeto content del usuario, así que incluye los campos que no vas a cambiar. Devuelve action junto con él, porque Claude Code ignora un hookSpecificOutput que no tenga action.

Los hooks ElicitationResult también se ejecutan cuando el usuario rechaza o cancela, y tu action reemplaza el suyo. Comprueba que el action de la entrada sea accept antes de devolver accept, o tu hook convertirá una solicitud rechazada en una aceptada. Este script hace el mismo cambio cuando el usuario aceptó, conserva los demás campos y no imprime nada en caso contrario:

#!/bin/bash
input=$(cat)

if [ "$(jq -r '.action' <<<"$input")" = "accept" ]; then
  jq '{hookSpecificOutput: {hookEventName: "ElicitationResult", action: "accept", content: (.content + {username: (.content.username + "@example.com")})}}' <<<"$input"
fi

Esta salida bloquea la respuesta:

{
  "hookSpecificOutput": {
    "hookEventName": "ElicitationResult",
    "action": "decline"
  }
}

El código de salida 2 y un "decision": "block" de nivel superior también bloquean la respuesta. Otras formas de rechazar una elicitación explica cuál tiene efecto cuando un hook los combina, qué ve el usuario y qué versiones ignoraron decision.

Claude Code descarta reason, systemMessage y continue de la salida JSON de un hook ElicitationResult, sea cual sea el action que devuelvas.

Hooks basados en prompts

Además de hooks de comando, HTTP y herramientas MCP, Claude Code admite hooks basados en prompts (type: "prompt") que usan un LLM para evaluar si permitir o bloquear una acción, y hooks de agente (type: "agent") que generan un verificador agentico con acceso a herramientas. No todos los eventos admiten todos los tipos de hooks.

Eventos que admiten los cinco tipos de hooks (command, http, mcp_tool, prompt y agent):

  • PermissionDenied
  • PostToolBatch
  • PostToolUse
  • PostToolUseFailure
  • PreToolUse
  • Stop
  • SubagentStop
  • TaskCompleted
  • TaskCreated
  • TeammateIdle
  • UserPromptExpansion
  • UserPromptSubmit

PermissionRequest admite hooks command, http, mcp_tool y prompt pero no hooks agent. Si configura un hook de agente en este evento, Claude Code lo omite y el flujo de permisos continúa sin cambios. Para permitir o denegar desde un hook, devuelva el objeto de decisión desde un hook de comando o HTTP.

Eventos que admiten hooks command, http y mcp_tool pero no prompt o agent:

  • ConfigChange
  • CwdChanged
  • DirectoryAdded
  • Elicitation
  • ElicitationResult
  • FileChanged
  • InstructionsLoaded
  • MessageDisplay
  • Notification
  • PostCompact
  • PostModelSwitch
  • PreCompact
  • PreModelSwitch
  • SessionEnd
  • StopFailure
  • SubagentStart
  • WorktreeCreate
  • WorktreeRemove

SessionStart y Setup admiten hooks command y mcp_tool, y MCP tool hook fields describe cuándo se ejecutan sus hooks mcp_tool. No admiten hooks http, prompt o agent.

Cómo funcionan los hooks basados en prompts

En lugar de ejecutar un comando Bash, los hooks basados en prompts:

  1. Envían la entrada del hook y tu prompt a un modelo Claude, por defecto el que Claude Code usa para funcionalidad en segundo plano
  2. El LLM responde con JSON estructurado que contiene una decisión
  3. Claude Code procesa la decisión automáticamente

Configuración de hook de prompt

Establezca type en "prompt" y proporcione una cadena prompt en lugar de un command. Use el marcador de posición $ARGUMENTS para inyectar datos de entrada JSON del hook en su texto de prompt.

En un hook de prompt o de agente, puedes escribir el prompt como una regla sobre qué bloquear o permitir, como "Bloquea cualquier comando Bash que lea archivos .env", o como una condición que debe cumplirse, como "Todas las pruebas unitarias pasan".

Este hook Stop le pide al LLM que evalúe si todas las tareas están completas antes de permitir que Claude finalice:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Evaluate if Claude should stop: $ARGUMENTS. Check if all tasks are complete."
          }
        ]
      }
    ]
  }
}
Campo Requerido Descripción
type sí Debe ser "prompt"
prompt sí El texto del prompt a enviar al LLM. Use $ARGUMENTS como marcador de posición para la entrada JSON del hook. Si $ARGUMENTS no está presente, la entrada JSON se agrega al prompt
model no Modelo a usar para evaluación. Por defecto es el modelo que Claude Code usa para funcionalidad en segundo plano
timeout no Tiempo de espera en segundos. Predeterminado: 30
continueOnBlock no En los eventos a los que se aplica, true retroalimenta una razón ok: false a Claude y continúa en lugar de terminar el turno. Predeterminado: false. Consulte Esquema de respuesta para el comportamiento por evento

Esquema de respuesta

El LLM debe responder con JSON que contenga:

{
  "ok": true | false,
  "reason": "Explanation for the decision",
  "impossible": true | false
}
Campo Descripción
ok true para permitir. Para false, consulte el comportamiento por evento a continuación
reason Requerido cuando ok es false
impossible Opcional. El modelo lo devuelve con ok: false cuando juzga que la condición nunca puede satisfacerse. En Stop y SubagentStop, Claude Code permite que el turno termine en lugar de retroalimentar la razón. Los hooks de agente y otros eventos lo ignoran

Lo que sucede en ok: false depende del evento:

  • Stop y SubagentStop: la razón se retroalimenta a Claude como su siguiente instrucción y el turno continúa, a menos que la respuesta también establezca impossible: true, en cuyo caso Claude Code permite la detención y el turno termina
  • PreToolUse: la llamada a herramienta se deniega; por defecto el turno termina y la razón de denegación aparece en el chat como una línea de advertencia. Establece continueOnBlock: true para devolver en cambio la razón a Claude como el error de la herramienta para que pueda ajustarse y continuar, equivalente a un hook de comando con permissionDecision: "deny"
  • PostToolUse: por defecto el turno termina y la razón aparece en el chat como una línea de advertencia. Establezca continueOnBlock: true para retroalimentar la razón a Claude y continuar el turno en lugar de detener
  • PostToolBatch, UserPromptSubmit y UserPromptExpansion: el turno termina y la razón aparece como una línea de advertencia. Estos eventos terminan el turno en decision: "block" independientemente de continue
  • PostToolUseFailure y TaskCreated: la razón se devuelve a Claude como un error de herramienta y el turno continúa, independientemente de continueOnBlock
  • TaskCompleted: cuando se activa porque una tarea se marca como completada durante un turno, la razón se devuelve a Claude como un error de herramienta y el turno continúa, independientemente de continueOnBlock. Cuando se activa porque un compañero se detiene, se comporta como TeammateIdle y detiene al compañero por defecto
  • TeammateIdle: por defecto el compañero se detiene y la razón aparece como una línea de advertencia. Establezca continueOnBlock: true para retroalimentar la razón al compañero y mantenerlo trabajando en su lugar
  • PermissionRequest: ok: false no tiene efecto. Para denegar una aprobación desde un hook, use un hook de comando que devuelva hookSpecificOutput.decision.behavior: "deny"
  • PermissionDenied: ok: false no tiene efecto porque la denegación ya sucedió. La única salida que este evento lee es hookSpecificOutput.retry, que los hooks de prompt y agente no pueden establecer. Se ejecutan en este evento, pero su salida se descarta. Use un hook de comando para devolver retry

Si necesita un control más fino en cualquier evento, use un hook de comando con los campos por evento descritos en Control de decisión.

Verificar múltiples condiciones antes de detener

Este hook Stop usa un prompt detallado para verificar tres condiciones antes de permitir que Claude se detenga. Los hooks SubagentStop usan el mismo formato para evaluar si un subagente debe detenerse. Si el modelo devuelve "ok": false porque la condición aún no se cumple, Claude continúa trabajando con la razón proporcionada como su siguiente instrucción:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "You are evaluating whether Claude should stop working. Context: $ARGUMENTS\n\nAnalyze the conversation and determine if:\n1. All user-requested tasks are complete\n2. Any errors need to be addressed\n3. Follow-up work is needed\n\nRespond with JSON: {\"ok\": true} to allow stopping, or {\"ok\": false, \"reason\": \"your explanation\"} to continue working.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Hooks basados en agentes

[!WARNING]

Los hooks de agente son experimentales. El comportamiento y la configuración pueden cambiar en futuras versiones. Para flujos de trabajo de producción, prefiera command hooks.

Los hooks basados en agentes (type: "agent") son como hooks basados en prompts pero con acceso a herramientas de múltiples turnos. En lugar de una única llamada LLM, un hook de agente genera un subagente que puede leer archivos, buscar código e inspeccionar la base de código para verificar condiciones. Los hooks de agente admiten los mismos eventos que los hooks basados en prompts, excepto PermissionRequest.

Cómo funcionan los hooks de agente

Cuando se activa un hook de agente:

  1. Claude Code genera un subagente con su prompt y la entrada JSON del hook
  2. El subagente puede usar herramientas como Read, Grep y Glob para investigar
  3. Después de hasta 50 turnos, el subagente devuelve una decisión estructurada { "ok": true/false }
  4. Claude Code permite la acción si ok es true. Si ok es false, Claude Code maneja el bloqueo de la misma manera que un hook de prompt con continueOnBlock: true en ese evento, como se indica en Esquema de respuesta

Los hooks de agente son útiles cuando la verificación requiere inspeccionar archivos reales o salida de prueba, no solo evaluar los datos de entrada del hook solos.

Configuración de hook de agente

Establezca type en "agent" y proporcione una cadena prompt, usando $ARGUMENTS como marcador de posición para la entrada JSON del hook. Los campos de configuración son los mismos que los hooks de prompt, excepto que los hooks de agente tienen un tiempo de espera predeterminado más largo de 60 segundos y no tienen campo continueOnBlock.

El esquema de respuesta es { "ok": true } para permitir o { "ok": false, "reason": "..." } para bloquear. En ok: false, Claude Code maneja un hook de agente de la manera que maneja un hook de prompt con continueOnBlock: true en el mismo evento; los hooks de agente no tienen campo continueOnBlock y no admiten el campo impossible del hook de prompt.

Este hook Stop verifica que todas las pruebas unitarias pasen antes de permitir que Claude finalice:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

Ejecutar hooks en segundo plano

Por defecto, los hooks bloquean la ejecución de Claude hasta que se completen. Para tareas de larga duración como implementaciones, conjuntos de pruebas o llamadas a API externas, establezca "async": true para ejecutar el hook en segundo plano mientras Claude continúa trabajando. Los hooks asincronos no pueden bloquear o controlar el comportamiento de Claude: campos de respuesta como decision, permissionDecision y continue no tienen efecto, porque la acción que habrían controlado ya se ha completado.

Configurar un hook asincrónico

Agregue "async": true a la configuración de un hook de comando para ejecutarlo en segundo plano sin bloquear a Claude. Este campo solo está disponible en hooks type: "command".

Este hook ejecuta un script de prueba después de cada llamada a herramienta Write. Claude continúa trabajando inmediatamente mientras run-tests.sh se ejecuta. Cuando el script finaliza, su salida se entrega en el siguiente turno de conversación:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/run-tests.sh",
            "async": true
          }
        ]
      }
    ]
  }
}

Una vez que un hook asincrónico se está ejecutando en segundo plano, Claude Code no aplica timeout en él. Claude Code aún aplica timeout en un hook que ejecuta con asyncRewake.

Claude Code entrega los resultados de un hook asincrónico solo mientras se ejecuta la sesión:

  • En modo no interactivo con la bandera -p, Claude Code mata cualquier hook asincrónico que aún se esté ejecutando al finalizar y lo finaliza con resultado cancelled
  • Si el trabajo de su hook debe sobrevivir a una sesión claude -p, inicie un proceso completamente desacoplado desde él

Cómo se ejecutan los hooks asincronos

Cuando se activa un hook asincrónico, Claude Code inicia el proceso del hook e inmediatamente continúa sin esperar a que finalice. El hook recibe la misma entrada JSON a través de stdin que un hook sincrónico.

Después de que el proceso en segundo plano sale, Claude Code entrega los campos additionalContext y systemMessage de la respuesta JSON del hook a Claude en el siguiente turno de conversación, excepto en el caso de la salida repetida de SessionStart. A diferencia del systemMessage de un hook sincrónico, ninguno de estos campos se te muestra a ti.

Imprime la respuesta JSON sola en stdout o en una línea propia:

  • Sola en stdout: cuando la respuesta es el único texto en stdout, puede abarcar varias líneas, como la salida de jq con formato legible. Abarcar varias líneas requiere Claude Code v2.1.295 o posterior.
  • En una línea propia: un hook asincrónico puede imprimir otro texto en stdout cuando la respuesta cabe por sí sola en una línea, por ejemplo con jq -c.

Claude Code valida esa respuesta JSON contra el mismo esquema de salida que los hooks síncronos, y descarta cualquier campo cuyo valor tenga el tipo incorrecto, como un systemMessage que no sea una cadena, en lugar de entregarlo. Ejecuta con --debug para ver una advertencia que nombre cada campo descartado.

Las notificaciones de finalización de hooks asincronos se suprimen por defecto. Para verlas, habilite el modo detallado con Ctrl+O o inicie Claude Code con --verbose.

Ejecutar pruebas después de cambios de archivo

Este hook inicia un conjunto de pruebas en segundo plano cada vez que Claude escribe un archivo, luego reporta los resultados a Claude cuando las pruebas finalizan. Guarde este script en .claude/hooks/run-tests-async.sh en su proyecto y hágalo ejecutable con chmod +x:

#!/bin/bash
# run-tests-async.sh

# Lee entrada de hook desde stdin
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Solo ejecute pruebas para archivos de origen
if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.js ]]; then
  exit 0
fi

# Ejecute pruebas e informe resultados a Claude a través de additionalContext
RESULT=$(npm test 2>&1)
EXIT_CODE=$?

if [ $EXIT_CODE -eq 0 ]; then
  MSG="Tests passed after editing $FILE_PATH"
else
  MSG="Tests failed after editing $FILE_PATH: $RESULT"
fi
jq -nc --arg msg "$MSG" '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $msg}}'

Luego agregue esta configuración a .claude/settings.json en la raíz de su proyecto. La bandera async: true permite que Claude continúe trabajando mientras se ejecutan las pruebas:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/run-tests-async.sh",
            "args": [],
            "async": true
          }
        ]
      }
    ]
  }
}

Limitaciones

Los hooks asincronos tienen restricciones adicionales en comparación con los hooks sincronos:

  • La salida del hook se entrega en el siguiente turno de conversación. Si la sesión está inactiva, la respuesta espera hasta la siguiente interacción del usuario. Excepción: un hook asyncRewake que sale con código 2 despierta a Claude inmediatamente incluso cuando la sesión está inactiva.
  • Cada ejecución crea un proceso en segundo plano separado.
  • Un hook SessionStart asincrónico se ejecuta cada vez que se activa el evento. Claude Code omite la salida del hook cuando el texto de additionalContext y systemMessage coincide con lo último que Claude recibió de ese hook, a menos que un hook SessionStart sincrónico haya agregado otro texto desde entonces. Claude Code entrega el texto cuando cambia, y de nuevo la próxima vez que el hook se ejecuta después de una compactación o de /clear. Antes de v2.1.295, Claude Code volvía a agregar el mismo texto a la conversación en cada reanudación.

Consideraciones de seguridad

Descargo de responsabilidad

[!WARNING]

Los hooks de comando ejecutan comandos de shell con sus permisos de usuario completos. Pueden modificar, eliminar o acceder a cualquier archivo al que su cuenta de usuario pueda acceder. Revise y pruebe todos los comandos de hook antes de agregarlos a su configuración.

Confianza del espacio de trabajo

Claude Code verifica la confianza del espacio de trabajo antes de ejecutar cualquier hook desde un archivo de configuración. Lo que cuenta como confiable depende del tipo de sesión:

  • Sesión interactiva: Claude Code retiene los hooks de todos los archivos de configuración, incluido su propio ~/.claude/settings.json, hasta que acepte el diálogo de confianza del espacio de trabajo para la carpeta, o para un directorio principal cuya confianza se extienda a ella
  • Sesión -p o SDK: Claude Code nunca muestra el diálogo y trata la carpeta como confiable, por lo que los hooks confirmados en el .claude/settings.json de un repositorio se ejecutan en una carpeta que nunca ha confiado

Antes de ejecutar claude -p en un repositorio que no escribió, revise sus archivos de configuración .claude/, comience con --bare, o desactive los hooks para esa ejecución con --settings '{"disableAllHooks": true}'. Los hooks de frontmatter en un subagente de proyecto siguen una regla más estricta que los hooks de archivo de configuración. Lo que se ejecuta antes de confiar en una carpeta enumera cada tipo de contenido de repositorio por tipo de sesión.

Mejores prácticas de seguridad

Tenga en cuenta estas prácticas al escribir hooks:

  • Validar y sanitizar entradas: nunca confíe en datos de entrada ciegamente
  • Siempre entrecomillar variables de shell: use "$VAR" no $VAR
  • Bloquear traversal de ruta: verifique .. en rutas de archivo
  • Usar rutas absolutas: especifique rutas completas para scripts. En forma exec, use ${CLAUDE_PROJECT_DIR} y la ruta no necesita entrecomillarse. En forma shell, envuélvala en comillas dobles
  • Omitir archivos sensibles: evite .env, .git/, claves, etc.

Herramienta PowerShell en Windows

En Windows, puede ejecutar hooks individuales en PowerShell estableciendo "shell": "powershell" en un hook de comando. Claude Code detecta automáticamente pwsh.exe, el ejecutable de PowerShell 7 y posterior, y recurre a powershell.exe para Windows PowerShell 5.1.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "shell": "powershell",
            "command": "Write-Host 'File written'"
          }
        ]
      }
    ]
  }
}

Hacer referencia a scripts por ruta en hooks de PowerShell

Para hacer referencia al directorio raíz del proyecto desde un comando de forma shell de PowerShell, escribe ${CLAUDE_PROJECT_DIR} o $env:CLAUDE_PROJECT_DIR. Claude Code reescribe los marcadores de posición ${CLAUDE_PROJECT_DIR}, ${CLAUDE_PLUGIN_ROOT} y ${CLAUDE_PLUGIN_DATA} en un comando de forma shell de PowerShell a la forma ${env:NAME} de PowerShell, ya sea que el hook esté definido en settings.json, un plugin o un skill. PowerShell luego resuelve el valor del entorno exportado después del análisis, por lo que el marcador de posición funciona dentro de cadenas entre comillas dobles pero no dentro de cadenas entre comillas simples, donde PowerShell nunca expande variables.

No escriba la ortografía desnuda $CLAUDE_PROJECT_DIR en un hook de PowerShell. PowerShell la analiza como una variable local indefinida y la resuelve a $null, lo que deja la ruta del script sin su prefijo de directorio raíz del proyecto. Claude Code no reescribe esa forma; en su lugar, registra una advertencia en el registro de depuración.

El ejemplo a continuación muestra un hook de settings.json que ejecuta un script del proyecto con la forma $env::

{
  "type": "command",
  "shell": "powershell",
  "command": "& \"$env:CLAUDE_PROJECT_DIR\\.claude\\hooks\\check.ps1\""
}

Depurar hooks

Los detalles de ejecución de hooks se escriben en el archivo de registro de depuración. Inicie Claude Code con claude --debug-file <path> para escribir el registro en una ubicación conocida, o ejecute claude --debug y lea el registro en ~/.claude/debug/<session-id>.txt. La bandera --debug no imprime en la terminal.

Por ejemplo, un hook PostToolUse en Write cuyo comando imprime hook-ran produce entradas como:

2026-07-19T02:03:24.382Z [DEBUG] Hook output does not start with {, treating as plain text
2026-07-19T02:03:24.382Z [DEBUG] "Hook PostToolUse:Write (PostToolUse) success:\nhook-ran"

Para encontrar un hook lento, busca en el registro las líneas Hooks: que terminan en una duración. En Claude Code v2.1.296 o posterior, cada hook de comando en un evento de herramienta, UserPromptSubmit, SessionStart, Stop y varios otros eventos deja una al terminar, sin importar lo que haya impreso. La línea indica el nombre del evento, unido mediante dos puntos al nombre de la herramienta u otro valor con el que coincidió el hook, luego el comando del hook entre corchetes, el plugin del que proviene, si lo hay, cómo terminó la ejecución y cuánto tardó, como en Hooks: PostToolUse:Write [.claude/hooks/log-write.sh] finished with status 0 (31ms). Una ejecución también puede terminar como timed out after <N>ms, cancelled, moved to the background o failed to start. En algunos eventos, como Notification, SessionEnd y PreCompact, un hook de comando deja en su lugar una línea completed with status sin duración.

Para detalles de coincidencia de hooks más granulares, establezca CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose para ver líneas de registro adicionales como recuentos de matchers de hooks y coincidencia de consultas.

Para solucionar problemas comunes como hooks que no se activan, hooks Stop que siguen bloqueando, o errores de configuración, consulte Limitaciones y solución de problemas en la guía. Para un recorrido de diagnóstico más amplio que cubra /context, /doctor y precedencia de configuración, consulte Depure su configuración.