Qué son los Hooks
Los hooks son comandos shell, HTTP endpoints, prompts LLM o tools MCP definidos por vos que se ejecutan automáticamente en puntos específicos del lifecycle de Claude Code. Permiten automatización, validación y comportamiento custom.
Cadencias del lifecycle
Los hooks disparan en tres ritmos:
- Una vez por sesión:
SessionStart,SessionEnd - Una vez por turno:
UserPromptSubmit,Stop,StopFailure - Por cada tool call:
PreToolUse,PostToolUse,PostToolUseFailure,PermissionRequest,PermissionDenied
Más eventos async: FileChanged, CwdChanged, ConfigChange, InstructionsLoaded, Notification, WorktreeCreate, WorktreeRemove, PreCompact, PostCompact, SubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle, Elicitation, ElicitationResult, UserPromptExpansion, Setup, PostToolBatch.
Eventos principales
SessionStart
Cuando la sesión inicia o se reanuda. Matchers: startup, resume, clear, compact.
Acceso a CLAUDE_ENV_FILE para persistir variables:
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
fi
Output:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Contexto para Claude"
}
}
UserPromptSubmit
Antes de que Claude procese el prompt. Timeout default 30s (más corto que otros). Sin matchers (siempre dispara).
{
"decision": "block",
"reason": "Por qué se bloqueó",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Contexto adicional",
"sessionTitle": "Nombre auto-generado"
}
}
Salida en stdout plana se agrega como contexto (sin JSON necesario).
UserPromptExpansion
Cuando un slash command tipeado se expande antes de llegar a Claude. Matcher: command_name o vacío para todos.
{
"expansion_type": "slash_command|mcp_prompt",
"command_name": "example-skill",
"command_args": "arg1 arg2",
"command_source": "plugin|project|user|mcp",
"prompt": "/example-skill arg1 arg2"
}
PreToolUse
Antes de que un tool se ejecute (puede bloquearlo). Matcher: nombre del tool (Bash, Edit, Write, Read, Glob, Grep, Agent, WebFetch, WebSearch, AskUserQuestion, ExitPlanMode, tools MCP).
Decision control:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask|defer",
"permissionDecisionReason": "Por qué",
"updatedInput": { "command": "comando modificado" },
"additionalContext": "Contexto para Claude"
}
}
allow: permitedeny: bloqueaask: escala al usuario con dialogdefer: deja decidir al modelo
PostToolUse / PostToolUseFailure
Después de tool exitoso/fallido. Matcher: nombre del tool.
{
"tool_name": "Write",
"tool_input": { "file_path": "...", "content": "..." },
"tool_result": "File written successfully"
}
Puede bloquear el loop agentic con decision: "block".
PostToolBatch
Después del batch completo de tool calls paralelos, antes del próximo model call. Sin matchers.
PermissionRequest
Cuando aparece el permission dialog. Matcher: tool name.
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow|deny",
"updatedInput": { "command": "npm run lint" },
"rule": "Bash(npm *)"
}
}
}
PermissionDenied
Cuando el classifier de auto-mode deniega. Sin blocking.
{
"hookSpecificOutput": {
"hookEventName": "PermissionDenied",
"retry": true
}
}
retry: true le dice al modelo que puede reintentar.
SubagentStart / SubagentStop
Cuando un subagent se spawnea o termina. Matcher: agent type.
Stop / StopFailure
Stop: Claude termina de responder. Sin matchers. Puede bloquear.
StopFailure: el turno termina por error de API. Matchers por tipo: rate_limit, authentication_failed, oauth_org_not_allowed, billing_error, invalid_request, server_error, max_output_tokens, unknown. Sin control.
TaskCreated / TaskCompleted
Cuando una tarea se crea o completa. Pueden bloquear.
TeammateIdle
Agent team teammate por ir idle.
ConfigChange
Cuando cambia configuración durante la sesión. Matchers: user_settings, project_settings, local_settings, policy_settings, skills.
CwdChanged
Cuando cambia el working directory (ej con cd). Sin matchers. Acceso a CLAUDE_ENV_FILE. Útil para direnv y similares.
{ "old_cwd": "/home/user/project", "new_cwd": "/home/user/project/src" }
FileChanged
Cuando un archivo observado cambia en disco. Matcher: nombres literales con |.
{
"hooks": {
"FileChanged": [
{
"matcher": ".envrc|.env",
"hooks": [{ "type": "command", "command": "direnv allow" }]
}
]
}
}
WorktreeCreate / WorktreeRemove
Cuando se crea/elimina un worktree (con --worktree o isolation: "worktree").
WorktreeCreate debe devolver el path:
#!/bin/bash
WORKTREE_PATH="/tmp/git-worktree-$$"
git worktree add "$WORKTREE_PATH" feature-branch
echo "$WORKTREE_PATH"
PreCompact / PostCompact
Antes/después de compactación. Matcher: manual o auto. PreCompact puede bloquear.
InstructionsLoaded
Cuando se carga CLAUDE.md o .claude/rules/*.md. Solo observabilidad.
Matchers: session_start, nested_traversal, path_glob_match, include, compact.
Setup
Para flags CLI --init-only, --init, o --maintenance. Matchers: init, maintenance. Acceso a CLAUDE_ENV_FILE.
Notification
Cuando Claude Code envía notificación. Matcher: tipo (permission_prompt, idle_prompt, auth_success, elicitation_*).
Emitir notificaciones del terminal (v2.1.141+):
#!/bin/bash
input=$(cat)
body=$(jq -r '.message // "Needs attention"' <<<"$input")
seq=$(printf '\033]777;notify;Claude Code;%s\007' "$body")
jq -nc --arg seq "$seq" '{terminalSequence: $seq}'
Secuencias permitidas: OSC 0/1/2 (window titles), OSC 9 (iTerm2/Windows Terminal/WezTerm + 9;4 taskbar progress), OSC 99 (Kitty), OSC 777 (Ghostty/Warp/urxvt), BEL.
Elicitation / ElicitationResult
Cuando un MCP server pide input al usuario / cuando el usuario responde. Matcher: server name.
SessionEnd
Cuando termina la sesión. Matchers: clear, resume, logout, prompt_input_exit, bypass_permissions_disabled, other.
Tipos de hooks
Command
{
"type": "command",
"command": "/path/to/script.sh",
"args": [],
"async": false,
"asyncRewake": false,
"shell": "bash",
"timeout": 600,
"statusMessage": "Validando...",
"once": false,
"if": "Bash(git *)"
}
Exec form (con args): cada element es un argumento sin tokenización shell.
Shell form (sin args): interpreta pipes, &&, globs y variables. Hay que quotear placeholders.
Async: corre en background. Con asyncRewake: true, despierta a Claude con exit 2 mostrando stderr como system reminder.
PowerShell en Windows:
{ "type": "command", "command": "Get-ChildItem -Recurse", "shell": "powershell" }
HTTP
{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"headers": { "Authorization": "Bearer $MY_TOKEN" },
"allowedEnvVars": ["MY_TOKEN"],
"timeout": 600
}
POST con JSON del hook input. Respuesta:
- 2xx empty: success, sin output
- 2xx text: success, texto como contexto
- 2xx JSON: parseado como decision
- Non-2xx / connection failure / timeout: non-blocking error
MCP Tool
{
"type": "mcp_tool",
"server": "my_server",
"tool": "security_scan",
"input": { "file_path": "${tool_input.file_path}" },
"timeout": 600
}
La salida text del tool se trata como stdout. Si es JSON válido, se procesa como decision.
Prompt
{
"type": "prompt",
"prompt": "Is this code safe?\n\n$ARGUMENTS",
"model": "claude-opus",
"timeout": 30
}
$ARGUMENTS se reemplaza con el hook input JSON. El modelo devuelve yes/no como JSON.
Agent (experimental)
{
"type": "agent",
"prompt": "Verify deployment.\n\n$ARGUMENTS",
"timeout": 60
}
Spawnea un subagente que puede usar tools antes de devolver decisión.
Campos comunes
| Campo | Descripción |
|---|---|
type | command, http, mcp_tool, prompt, agent |
if | Permission rule para filtrar ("Bash(git *)"). Solo en tool events |
timeout | Segundos antes de cancelar (defaults: 600 / 30 prompt / 60 agent / 30 UserPromptSubmit) |
statusMessage | Mensaje custom mientras corre |
once | Si true, corre una vez por sesión. Solo en skill/agent frontmatter |
Exit codes
| Exit | Comportamiento |
|---|---|
0 | Success. Stdout parseado para JSON output |
2 | Blocking error. Stderr enviado a Claude como mensaje |
| Otros | Non-blocking error. Stderr al debug log; ejecución continúa |
Exit 2 bloqueante por evento:
| Puede bloquear | Eventos |
|---|---|
| Sí | PreToolUse, PermissionRequest, UserPromptSubmit, UserPromptExpansion, Stop, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, ConfigChange, PostToolBatch, PreCompact, Elicitation, ElicitationResult, WorktreeCreate |
| No | StopFailure, PostToolUse, PostToolUseFailure, PermissionDenied, Notification, SubagentStart, SessionStart, Setup, SessionEnd, CwdChanged, FileChanged, PostCompact, WorktreeRemove, InstructionsLoaded |
Matchers
| Matcher | Evaluado como | Ejemplo |
|---|---|---|
"*", "", omitido | Todos | Cualquier ocurrencia |
Solo letras/dígitos/_/| | String exacto o lista |-separated | Bash, Edit|Write |
| Otros caracteres | Regex JS | ^Notebook, mcp__memory__.* |
FileChanged no usa reglas estándar — son filenames literales: .envrc|.env.
MCP tools: mcp__<server>__<tool>. Todos los de un server: mcp__memory__.*.
JSON output universal
| Campo | Default | Descripción |
|---|---|---|
continue | true | Si false, Claude para. Precede a campos event-specific |
stopReason | — | Mensaje al usuario cuando continue: false |
suppressOutput | false | Omite stdout del debug log |
systemMessage | — | Warning al usuario |
terminalSequence | — | Escape sequence para emitir (desktop notif, window title, bell) |
Configuración
| Ubicación | Scope | Shareable |
|---|---|---|
~/.claude/settings.json | Todos los proyectos | No |
.claude/settings.json | Proyecto | Sí |
.claude/settings.local.json | Proyecto | No (gitignored) |
| Managed policy | Organización | Sí (admin) |
Plugin hooks/hooks.json | Donde el plugin esté enabled | Sí |
| Skill/agent frontmatter | Mientras esté activo | Sí (en el componente) |
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
}
]
}
]
}
}
Deshabilitar todos los hooks
{ "disableAllHooks": true }
Path placeholders
${CLAUDE_PROJECT_DIR}: raíz del proyecto${CLAUDE_PLUGIN_ROOT}: directorio de instalación del plugin (cambia con updates)${CLAUDE_PLUGIN_DATA}: directorio persistente del plugin (sobrevive updates)
Preferí exec form para hooks que usan placeholders:
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/scripts/format.js", "--fix"]
}
Shell form requiere quoting: node "${CLAUDE_PLUGIN_ROOT}"/scripts/format.js.
Hooks en Skills y Agents
---
name: secure-ops
description: Operaciones con security checks
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/security-check.sh"
---
Todos los eventos soportados. Para subagents, Stop hooks se convierten automáticamente a SubagentStop. Scope al lifetime del componente.
Campos comunes de input
Todos los hooks reciben:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/working/dir",
"permission_mode": "default|plan|acceptEdits|auto|dontAsk|bypassPermissions",
"hook_event_name": "EventName",
"effort": { "level": "low|medium|high|xhigh|max" }
}
Cuando corre con --agent o dentro de subagent:
{ "agent_id": "id", "agent_type": "name" }
Env var disponible: $CLAUDE_EFFORT.
El menú /hooks
Tipeá /hooks para abrir un browser read-only de hooks configurados. Muestra eventos, conteos, matchers, handlers, source (User/Project/Local/Plugin/Session/Built-in). Read-only — editá settings JSON directamente para cambios.
Ejemplo: bloquear rm -rf
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"
}
]
}
]
}
}
Script:
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Comando destructivo bloqueado"
}
}'
else
exit 0
fi
Ejemplo: lint después de Edit/Write
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "/path/to/lint-check.sh" }]
}
]
}
}
Siguiente: MCP Servers