Módulo 6: Hooks Avanzados y SDK Headless

2. SessionStart y PreToolUse Avanzado — Setup Automático y Validación

2. SessionStart y PreToolUse Avanzado — Setup Automático y Validación

Descripción

Cada vez que abres Claude Code, el primer minuto es siempre igual: verificas que las dependencias están instaladas, que el servidor de desarrollo no está corriendo en un puerto ocupado, que la base de datos tiene las migraciones al día. Es tedioso y repetitivo. SessionStart elimina ese ritual — un hook que se dispara automáticamente al arrancar cada sesión y ejecuta tu script de setup.

PreToolUse, por otro lado, es el guardián. Ya lo conoces de guías anteriores en su forma básica — un hook que se dispara antes de ejecutar una herramienta. Pero PreToolUse avanzado va más allá: validación condicional basada en el contenido del comando, bloqueo de operaciones peligrosas por exit code 2, restricción de rutas de archivos, y matchers con regex para capturar grupos de herramientas.

Al terminar esta cápsula sabrás configurar hooks en settings.json y en frontmatter de subagents, entenderás el formato JSON que los hooks reciben via stdin, y dominarás los tres exit codes que controlan el flujo de ejecución. Tu Claude Code arrancará configurado automáticamente y bloqueará operaciones peligrosas antes de que se ejecuten.


SessionStart: Configuración Automática al Iniciar

El problema

Sin SessionStart, tu inicio de sesión se ve así:

Tú: "Verifica que las deps estén instaladas"
Claude: [corre npm install, pip install, etc.]
Tú: "Verifica que la DB esté corriendo"
Claude: [chequea PostgreSQL, Redis]
Tú: "Corre las migraciones pendientes"
Claude: [ejecuta alembic upgrade head]
Tú: "Ahora sí, implementa la feature X"

Cuatro interacciones desperdiciadas. Con SessionStart:

[Sesión arranca → SessionStart hook ejecuta setup automático]
Tú: "Implementa la feature X"

Configuración en settings.json

Los hooks se configuran en el archivo de settings de Claude Code. Hay tres niveles:

NivelArchivoScope
Proyecto.claude/settings.jsonSolo este proyecto
Usuario~/.claude/settings.jsonTodos tus proyectos
EmpresaConfiguración administradaToda la organización

Para hooks específicos del proyecto, usa .claude/settings.json:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/session-setup.sh"
          }
        ]
      }
    ]
  }
}

El script de setup

./scripts/session-setup.sh:

#!/bin/bash

echo "🔧 Configurando entorno de desarrollo..."

# Verificar Node.js
if ! command -v node &> /dev/null; then
    echo "ERROR: Node.js no encontrado"
    exit 1
fi

# Instalar dependencias si package-lock.json cambió
if [ package-lock.json -nt node_modules/.package-lock.json ] 2>/dev/null; then
    echo "📦 Instalando dependencias..."
    npm ci --silent
fi

# Verificar Python virtual env
if [ -f "requirements.txt" ]; then
    if [ ! -d ".venv" ]; then
        echo "🐍 Creando virtual environment..."
        python3 -m venv .venv
    fi
    source .venv/bin/activate
    pip install -r requirements.txt -q
fi

# Verificar base de datos
if command -v pg_isready &> /dev/null; then
    if ! pg_isready -q 2>/dev/null; then
        echo "⚠️ PostgreSQL no está corriendo"
        exit 1
    fi
fi

# Migraciones pendientes
if [ -d "alembic" ]; then
    CURRENT=$(alembic current 2>/dev/null | tail -1)
    HEAD=$(alembic heads 2>/dev/null | tail -1)
    if [ "$CURRENT" != "$HEAD" ]; then
        echo "📊 Ejecutando migraciones pendientes..."
        alembic upgrade head
    fi
fi

echo "✅ Entorno listo"
exit 0

Recuerda hacer el script ejecutable:

chmod +x ./scripts/session-setup.sh

SessionStart no tiene matcher

A diferencia de PreToolUse o PostToolUse, SessionStart no tiene campo matcher. Se dispara una vez al iniciar cualquier sesión, sin condiciones. Si necesitas lógica condicional, ponla dentro del script:

#!/bin/bash

# Solo ejecutar setup completo en días laborales
DAY=$(date +%u)
if [ "$DAY" -gt 5 ]; then
    echo "Fin de semana — skip setup completo"
    exit 0
fi

# Setup completo aquí...

Exit codes en SessionStart

Exit CodeEfecto
0Sesión arranca normalmente
1Error reportado a Claude — la sesión continúa pero Claude sabe que algo falló
2Sesión bloqueada — Claude Code no inicia

Exit code 2 en SessionStart es drástico: impide que la sesión arranque. Úsalo solo para condiciones críticas que harían la sesión inútil (ej: la base de datos de producción no es accesible para un proyecto que la necesita).


PreToolUse Avanzado: Validación y Bloqueo

Más allá del básico

En guías anteriores, usaste PreToolUse para validaciones simples. Ahora vas a crear hooks que:

  1. Leen el JSON de input para inspeccionar qué va a hacer la herramienta
  2. Toman decisiones condicionales basadas en el contenido
  3. Bloquean operaciones peligrosas con exit code 2
  4. Usan matchers con regex para capturar grupos de herramientas

El input JSON

Todo hook recibe información via stdin en formato JSON. Para PreToolUse, el JSON incluye:

{
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /tmp/test",
    "description": "Clean temp files"
  },
  "session_id": "abc123",
  "transcript_path": "/tmp/claude/transcript-abc123.json"
}

Tu script puede leer este JSON y tomar decisiones:

#!/bin/bash

INPUT=$(cat -)

TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Lógica de validación basada en el contenido

Pattern 1: Bloquear comandos peligrosos

./scripts/validate-bash.sh:

#!/bin/bash

INPUT=$(cat -)

COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

if [ -z "$COMMAND" ]; then
    exit 0
fi

BLOCKED_PATTERNS=(
    "rm -rf /"
    "rm -rf ~"
    "rm -rf \."
    "DROP DATABASE"
    "DROP TABLE"
    "truncate"
    "mkfs"
    "dd if="
    ":(){:|:&};:"
)

for pattern in "${BLOCKED_PATTERNS[@]}"; do
    if echo "$COMMAND" | grep -qi "$pattern"; then
        echo "BLOCKED: Comando peligroso detectado: $pattern"
        echo "Comando intentado: $COMMAND"
        exit 2
    fi
done

DANGEROUS_PATTERNS=(
    "sudo"
    "chmod 777"
    "curl.*|.*sh"
    "wget.*|.*bash"
)

for pattern in "${DANGEROUS_PATTERNS[@]}"; do
    if echo "$COMMAND" | grep -qi "$pattern"; then
        echo "WARNING: Comando potencialmente peligroso: $pattern"
        echo "Comando: $COMMAND"
        exit 1
    fi
done

exit 0

Configuración en settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/validate-bash.sh"
          }
        ]
      }
    ]
  }
}

Pattern 2: Restricción de rutas de archivos

Impedir que Claude modifique archivos fuera de ciertos directorios:

./scripts/validate-file-paths.sh:

#!/bin/bash

INPUT=$(cat -)

TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')

if [ -z "$FILE_PATH" ]; then
    exit 0
fi

PROTECTED_PATHS=(
    ".env"
    ".env.local"
    ".env.production"
    "credentials"
    "secrets"
    ".ssh"
    ".aws"
)

for protected in "${PROTECTED_PATHS[@]}"; do
    if echo "$FILE_PATH" | grep -qi "$protected"; then
        echo "BLOCKED: Acceso a archivo protegido: $FILE_PATH"
        exit 2
    fi
done

ALLOWED_DIRS=(
    "src/"
    "tests/"
    "docs/"
    "scripts/"
    ".claude/"
)

ALLOWED=false
for dir in "${ALLOWED_DIRS[@]}"; do
    if echo "$FILE_PATH" | grep -q "^$dir"; then
        ALLOWED=true
        break
    fi
done

if [ "$ALLOWED" = false ]; then
    echo "WARNING: Archivo fuera de directorios permitidos: $FILE_PATH"
    echo "Directorios permitidos: ${ALLOWED_DIRS[*]}"
    exit 1
fi

exit 0

Configuración con matcher para múltiples herramientas:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/validate-file-paths.sh"
          }
        ]
      }
    ]
  }
}

Pattern 3: Validación condicional por contexto

Un hook que valida de forma diferente según el entorno:

./scripts/context-validator.sh:

#!/bin/bash

INPUT=$(cat -)

TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

BRANCH=$(git branch --show-current 2>/dev/null)

if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
    if [ "$TOOL_NAME" = "Bash" ]; then
        if echo "$COMMAND" | grep -qiE "(git push|npm publish|deploy)"; then
            echo "BLOCKED: Operación de deployment en branch principal"
            echo "Crea un branch de feature primero"
            exit 2
        fi
    fi

    if [ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ]; then
        echo "WARNING: Editando en branch principal ($BRANCH)"
        echo "Considera crear un branch de feature"
        exit 1
    fi
fi

exit 0

Matchers: Regex y pipe-separated

El campo matcher acepta dos formatos:

Nombre exacto:

{ "matcher": "Bash" }

Pipe-separated (OR):

{ "matcher": "Edit|Write" }

Regex:

{ "matcher": ".*" }

El matcher .* captura todas las herramientas — útil para logging universal.

Sin matcher: captura todo

Si omites el campo matcher, el hook se dispara para todas las herramientas:

{
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/log-all-tools.sh"
          }
        ]
      }
    ]
  }
}

Hooks en Frontmatter de Subagents

Cuándo usar hooks en el frontmatter

Los hooks en settings.json aplican a toda la sesión. Los hooks en el frontmatter de un subagent aplican solo a ese subagent. Esto permite reglas específicas por agente:

---
name: safe-implementer
description: Implements code with extra safety checks
tools: Read, Write, Edit, Grep, Glob
hooks:
  PreToolUse:
    - matcher: "Write|Edit"
      hooks:
        - type: command
          command: "./scripts/validate-file-paths.sh"
  PostToolUse:
    - matcher: "Write|Edit"
      hooks:
        - type: command
          command: "./scripts/auto-lint.sh"
---

You are a careful implementer. Write clean, tested code.

Precedencia: frontmatter vs settings.json

Cuando un subagent tiene hooks en su frontmatter Y hay hooks en settings.json, ambos se ejecutan. El orden es:

  1. Settings.json hooks se ejecutan primero
  2. Frontmatter hooks se ejecutan después

Si cualquiera de los dos retorna exit code 2, la operación se bloquea.

Ejemplo: Subagent con restricciones estrictas

Un subagent que solo puede editar archivos en su directorio asignado:

---
name: auth-specialist
description: Only modifies files in src/auth/
tools: Read, Write, Edit, Grep, Glob
hooks:
  PreToolUse:
    - matcher: "Write|Edit"
      hooks:
        - type: command
          command: |
            #!/bin/bash
            INPUT=$(cat -)
            FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
            if [ -n "$FILE" ] && ! echo "$FILE" | grep -q "^src/auth/"; then
              echo "BLOCKED: auth-specialist solo puede editar src/auth/"
              echo "Intentó editar: $FILE"
              exit 2
            fi
            exit 0
---

You are an auth specialist. You ONLY modify files in src/auth/.

Con este hook, el sistema enforce técnicamente la restricción. Aunque el system prompt diga "solo edita src/auth/", los LLMs pueden cometer errores. El hook garantiza que ningún archivo fuera de src/auth/ sea modificado.


Múltiples Hooks por Evento

Encadenamiento

Puedes tener múltiples hooks para el mismo evento. Se ejecutan en orden:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/validate-bash.sh"
          }
        ]
      },
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/validate-file-paths.sh"
          }
        ]
      },
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/log-all-tools.sh"
          }
        ]
      }
    ]
  }
}

Cuando Claude ejecuta Edit:

  1. validate-bash.sh → NO se ejecuta (matcher no coincide)
  2. validate-file-paths.sh → SÍ se ejecuta (matcher coincide)
  3. log-all-tools.sh → SÍ se ejecuta (sin matcher = captura todo)

Si validate-file-paths.sh retorna exit 2, la operación se bloquea y log-all-tools.sh no se ejecuta.

Múltiples comandos dentro de un hook

Un hook entry puede tener múltiples comandos:

{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "command": "./scripts/validate-bash.sh"
    },
    {
      "type": "command",
      "command": "./scripts/log-bash.sh"
    }
  ]
}

Ambos se ejecutan en orden. Si el primero retorna exit 2, el segundo no se ejecuta.


Configuración Completa: settings.json con SessionStart + PreToolUse

Un ejemplo completo que combina ambos:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/session-setup.sh"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/validate-bash.sh"
          }
        ]
      },
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/validate-file-paths.sh"
          }
        ]
      }
    ]
  }
}

Con esta configuración:

  • Cada sesión arranca con setup automático
  • Cada comando Bash se valida contra patrones peligrosos
  • Cada escritura de archivo se valida contra rutas protegidas

Troubleshooting

"El hook SessionStart no se ejecuta"

Causa: El script no tiene permisos de ejecución, o la ruta es incorrecta.

Solución:

chmod +x ./scripts/session-setup.sh

# Verifica que la ruta sea relativa al root del proyecto
ls -la ./scripts/session-setup.sh

"El hook PreToolUse no bloquea nada"

Causa: El matcher no coincide con el nombre de la herramienta, o el script siempre retorna exit 0.

Solución:

# Testa el script manualmente
echo '{"tool_name": "Bash", "tool_input": {"command": "rm -rf /"}}' | ./scripts/validate-bash.sh
echo $?

"jq: command not found"

Causa: jq no está instalado. Los scripts de hooks que parsean JSON necesitan jq.

Solución:

# macOS
brew install jq

# Ubuntu/Debian
sudo apt-get install jq

# Si no puedes instalar jq, usa Python como alternativa:
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))")

"El hook bloquea todo — exit code 2 inesperado"

Causa: El script tiene un error que causa exit code != 0 por defecto.

Solución: Agrega exit 0 al final del script como default y asegúrate de que cada path de ejecución retorne un exit code explícito. Testa el script aislado antes de configurarlo como hook.

"El hook tarda demasiado y hace la sesión lenta"

Causa: El script de hook hace operaciones pesadas (npm install, compilación, requests de red).

Solución: Mantén los hooks ligeros (< 2 segundos). Para SessionStart, operaciones pesadas son aceptables. Para PreToolUse, que se dispara en cada herramienta, el script debe ser casi instantáneo:

# MAL: hook PreToolUse pesado
npm run build  # 30 segundos cada vez que Claude usa una herramienta

# BIEN: hook PreToolUse ligero
grep -q "rm -rf" <<< "$COMMAND" && exit 2  # < 1ms

Comparación: settings.json vs Frontmatter

Aspectosettings.jsonFrontmatter del subagent
ScopeToda la sesión / proyectoSolo ese subagent
Aplica aClaude principal + todos los subagentsSolo el subagent específico
Dónde vive.claude/settings.json.claude/agents/my-agent.md
Cuándo usarReglas globales del proyectoReglas específicas del agente
FormatoJSONYAML
PrecedenciaSe ejecuta primeroSe ejecuta después
DistribuciónManual o via settings syncCon el agent file

Regla práctica: Si la regla es "nadie debe hacer X en este proyecto" → settings.json. Si la regla es "este agente específico no debe hacer Y" → frontmatter.


Ejercicios

Ejercicio 1: SessionStart básico (Fácil)

Crea un hook SessionStart que verifique si git está inicializado y si hay cambios sin commitear. Si hay más de 10 archivos modificados sin commit, reporta un warning (exit 1).

Ver solución

./scripts/git-check.sh:

#!/bin/bash

if ! git rev-parse --git-dir > /dev/null 2>&1; then
    echo "WARNING: No es un repositorio git"
    exit 1
fi

MODIFIED=$(git status --porcelain | wc -l | tr -d ' ')

if [ "$MODIFIED" -gt 10 ]; then
    echo "WARNING: $MODIFIED archivos sin commitear"
    echo "Considera hacer commit antes de empezar"
    exit 1
fi

echo "Git OK: $MODIFIED archivos pendientes"
exit 0

En .claude/settings.json:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          { "type": "command", "command": "./scripts/git-check.sh" }
        ]
      }
    ]
  }
}

Ejercicio 2: PreToolUse — Bloquear rm recursivo (Fácil)

Crea un hook PreToolUse que bloquee (exit 2) cualquier comando Bash que contenga rm -rf seguido de /, ~, o ..

Ver solución

./scripts/block-rm.sh:

#!/bin/bash

INPUT=$(cat -)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

if echo "$COMMAND" | grep -qE "rm\s+-rf\s+[/~.]"; then
    echo "BLOCKED: rm -rf con ruta peligrosa detectado"
    echo "Comando: $COMMAND"
    exit 2
fi

exit 0

En .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "./scripts/block-rm.sh" }
        ]
      }
    ]
  }
}

Ejercicio 3: PreToolUse — Restricción de rutas por subagent (Medio)

Crea un subagent api-specialist.md que solo pueda editar archivos dentro de src/api/. Usa un hook PreToolUse en el frontmatter que bloquee cualquier Write o Edit fuera de ese directorio.

Ver solución

.claude/agents/api-specialist.md:

---
name: api-specialist
description: API endpoint specialist. Only modifies src/api/.
tools: Read, Write, Edit, Grep, Glob
hooks:
  PreToolUse:
    - matcher: "Write|Edit"
      hooks:
        - type: command
          command: |
            #!/bin/bash
            INPUT=$(cat -)
            FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
            if [ -z "$FILE" ]; then
              exit 0
            fi
            if ! echo "$FILE" | grep -q "^src/api/"; then
              echo "BLOCKED: api-specialist solo puede editar src/api/"
              echo "Intentó: $FILE"
              exit 2
            fi
            exit 0
---

You are an API specialist. Implement and modify REST endpoints.
Only modify files in src/api/. Read any file for context.

La validación técnica garantiza que, incluso si el LLM intenta editar fuera de src/api/, el hook lo bloquea.

Ejercicio 4: PreToolUse — Validación condicional por branch (Medio)

Crea un hook PreToolUse que:

  • En main/master: bloquea (exit 2) cualquier Write/Edit
  • En branches de feature: permite todo pero loguea warnings para archivos de configuración
  • En cualquier branch: bloquea acceso a .env*
Ver solución

./scripts/branch-validator.sh:

#!/bin/bash

INPUT=$(cat -)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')

if [ -z "$FILE" ]; then
    exit 0
fi

if echo "$FILE" | grep -qE "^\.env"; then
    echo "BLOCKED: Archivos .env son protegidos en todos los branches"
    exit 2
fi

BRANCH=$(git branch --show-current 2>/dev/null)

if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
    echo "BLOCKED: No se permite editar archivos en branch $BRANCH"
    echo "Crea un feature branch: git checkout -b feature/my-feature"
    exit 2
fi

if echo "$FILE" | grep -qE "(config|settings|\.yaml|\.yml|\.toml)"; then
    echo "WARNING: Editando archivo de configuración: $FILE"
    echo "Branch: $BRANCH"
    exit 1
fi

exit 0

Ejercicio 5: Múltiples hooks encadenados (Difícil)

Diseña una configuración settings.json con:

  1. SessionStart: verifica git y dependencias
  2. PreToolUse para Bash: bloquea comandos peligrosos
  3. PreToolUse para Write/Edit: restringe rutas
  4. PreToolUse universal (sin matcher): logging de todas las herramientas

Escribe la configuración JSON completa y los 4 scripts.

Ver solución

.claude/settings.json:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          { "type": "command", "command": "./scripts/hooks/session-setup.sh" }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "./scripts/hooks/validate-bash.sh" }
        ]
      },
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "./scripts/hooks/validate-paths.sh" }
        ]
      },
      {
        "hooks": [
          { "type": "command", "command": "./scripts/hooks/log-tools.sh" }
        ]
      }
    ]
  }
}

./scripts/hooks/session-setup.sh:

#!/bin/bash
if ! git rev-parse --git-dir > /dev/null 2>&1; then
    echo "WARNING: No git repo"
    exit 1
fi
if [ -f "package.json" ] && [ ! -d "node_modules" ]; then
    npm ci --silent
fi
echo "Setup complete"
exit 0

./scripts/hooks/validate-bash.sh:

#!/bin/bash
INPUT=$(cat -)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -qiE "(rm -rf [/~.]|DROP DATABASE|DROP TABLE)"; then
    echo "BLOCKED: $CMD"
    exit 2
fi
exit 0

./scripts/hooks/validate-paths.sh:

#!/bin/bash
INPUT=$(cat -)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
if [ -z "$FILE" ]; then exit 0; fi
if echo "$FILE" | grep -qE "^\.env"; then
    echo "BLOCKED: $FILE"
    exit 2
fi
exit 0

./scripts/hooks/log-tools.sh:

#!/bin/bash
INPUT=$(cat -)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
echo "$TIMESTAMP | $TOOL" >> .claude/tool-log.txt
exit 0

Ejercicio 6: Debugging de hooks (Difícil)

Un desarrollador configuró este hook pero reporta que "no bloquea nada". Encuentra y corrige todos los bugs:

{
  "hooks": {
    "preToolUse": [
      {
        "match": "bash",
        "hooks": [
          {
            "type": "cmd",
            "command": "scripts/validate.sh"
          }
        ]
      }
    ]
  }
}
#!/bin/bash
# scripts/validate.sh
COMMAND=$1
if [ "$COMMAND" == "rm -rf" ]; then
    echo "Blocked"
fi
Ver solución

Hay 5 bugs:

  1. preToolUse → Debe ser PreToolUse (PascalCase)
  2. match → Debe ser matcher
  3. bash → Debe ser Bash (PascalCase, nombre de herramienta de Claude Code)
  4. type: "cmd" → Debe ser type: "command"
  5. COMMAND=$1 → Los hooks reciben input via stdin, no argumentos. Debe ser INPUT=$(cat -) y luego parsear con jq
  6. No hay exit code → El script no retorna exit 2 para bloquear; el echo solo imprime
  7. Ruta sin ./ → Debe ser ./scripts/validate.sh para ruta relativa

Configuración corregida:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/validate.sh"
          }
        ]
      }
    ]
  }
}

Script corregido:

#!/bin/bash
INPUT=$(cat -)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$COMMAND" | grep -q "rm -rf"; then
    echo "BLOCKED: $COMMAND"
    exit 2
fi
exit 0

Resumen

  • SessionStart se dispara al arrancar cada sesión — ideal para setup automático de dependencias, verificación de base de datos, y migraciones
  • PreToolUse se dispara antes de cada ejecución de herramienta — el guardián que valida, advierte, o bloquea operaciones
  • Los hooks reciben input JSON via stdin con tool_name, tool_input, y metadata de sesión
  • Exit codes: 0 = continuar, 1 = error (Claude decide), 2 = bloquear operación
  • El campo matcher filtra por herramienta: nombre exacto, pipe-separated (Edit|Write), regex (.*), o sin matcher (captura todo)
  • Los hooks se configuran en settings.json (scope proyecto/usuario) o en el frontmatter de subagents (scope agente)
  • Los hooks de settings.json se ejecutan primero, los de frontmatter después
  • Múltiples hooks por evento se ejecutan en orden; si uno retorna exit 2, los siguientes no se ejecutan
  • Mantén los hooks PreToolUse ligeros (< 2 segundos) — se ejecutan en cada invocación de herramienta
  • Instala jq para parsear JSON en scripts bash — es esencial para hooks que inspeccionan input

Recursos Adicionales

  1. Claude Code Hooks (Anthropic Docs) — Documentación oficial de todos los eventos, matchers, y tipos de hooks
  2. Claude Code Settings — Ubicación y formato de settings.json
  3. Create Custom Subagents — Hooks en frontmatter YAML
  4. jq Manual — Referencia de jq para parseo de JSON en bash
  5. Claude Code CLI Reference — Referencia de herramientas y nombres para matchers
  6. Claude Code Best Practices — Buenas prácticas de seguridad y validación
  7. Bash Exit Codes — Referencia de exit codes en bash
  8. Claude Code Overview — Contexto general para entender el ciclo de vida de herramientas

Siguiente cápsula: En la cápsula 03 verás los hooks de reacción: PostToolUse para auto-lint después de ediciones, SubagentStart y SubagentStop para tracking del ciclo de vida de subagents, Stop para cleanup al final de la sesión, y PermissionRequest para flujos de aprobación custom. Pasas de prevenir (PreToolUse) a reaccionar (PostToolUse) — la otra mitad del sistema nervioso.