Cap 5: Hooks

Por: Artiko
claude-codehooksautomationlifecycle

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:

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

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:

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

CampoDescripción
typecommand, http, mcp_tool, prompt, agent
ifPermission rule para filtrar ("Bash(git *)"). Solo en tool events
timeoutSegundos antes de cancelar (defaults: 600 / 30 prompt / 60 agent / 30 UserPromptSubmit)
statusMessageMensaje custom mientras corre
onceSi true, corre una vez por sesión. Solo en skill/agent frontmatter

Exit codes

ExitComportamiento
0Success. Stdout parseado para JSON output
2Blocking error. Stderr enviado a Claude como mensaje
OtrosNon-blocking error. Stderr al debug log; ejecución continúa

Exit 2 bloqueante por evento:

Puede bloquearEventos
PreToolUse, PermissionRequest, UserPromptSubmit, UserPromptExpansion, Stop, SubagentStop, TeammateIdle, TaskCreated, TaskCompleted, ConfigChange, PostToolBatch, PreCompact, Elicitation, ElicitationResult, WorktreeCreate
NoStopFailure, PostToolUse, PostToolUseFailure, PermissionDenied, Notification, SubagentStart, SessionStart, Setup, SessionEnd, CwdChanged, FileChanged, PostCompact, WorktreeRemove, InstructionsLoaded

Matchers

MatcherEvaluado comoEjemplo
"*", "", omitidoTodosCualquier ocurrencia
Solo letras/dígitos/_/|String exacto o lista |-separatedBash, Edit|Write
Otros caracteresRegex 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

CampoDefaultDescripción
continuetrueSi false, Claude para. Precede a campos event-specific
stopReasonMensaje al usuario cuando continue: false
suppressOutputfalseOmite stdout del debug log
systemMessageWarning al usuario
terminalSequenceEscape sequence para emitir (desktop notif, window title, bell)

Configuración

UbicaciónScopeShareable
~/.claude/settings.jsonTodos los proyectosNo
.claude/settings.jsonProyecto
.claude/settings.local.jsonProyectoNo (gitignored)
Managed policyOrganizaciónSí (admin)
Plugin hooks/hooks.jsonDonde el plugin esté enabled
Skill/agent frontmatterMientras esté activoSí (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

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