Módulo 6: Hooks Avanzados y SDK Headless

3. PostToolUse, Subagent Events y Stop — Reaccionar, Rastrear y Cerrar

3. PostToolUse, Subagent Events y Stop — Reaccionar, Rastrear y Cerrar

Descripción

En la cápsula anterior aprendiste los hooks de prevención: SessionStart configura el entorno antes de que trabajes, PreToolUse valida antes de que una herramienta se ejecute. Ahora viene la otra mitad: los hooks de reacción. PostToolUse se dispara después de que una herramienta termina — es donde pones auto-lint, auto-test, logging de cambios. SubagentStart y SubagentStop rastrean el ciclo de vida de los subagents: cuándo arrancan, cuándo terminan, qué produjeron. Stop se dispara cuando Claude termina de responder — el lugar para cleanup final y generación de reportes. PermissionRequest intercepta solicitudes de permisos para crear flujos de aprobación custom.

La diferencia conceptual es clara: PreToolUse pregunta "¿debería ejecutarse esto?" PostToolUse pregunta "¿qué hago ahora que se ejecutó?" SubagentStop pregunta "¿qué produjo este agente?" Stop pregunta "¿qué hago cuando Claude termina?" Juntos forman el ciclo de vida completo de eventos dentro de Claude Code.

Al terminar esta cápsula tendrás hooks que auto-formatean tu código después de cada edición, generan logs del ciclo de vida de subagents, producen reportes al final de sesiones, y gestionan permisos de forma automática. Tu Claude Code no solo previene problemas — reacciona inteligentemente a todo lo que ocurre.


PostToolUse: Reacción Después de la Ejecución

Cuándo se dispara

PostToolUse se ejecuta inmediatamente después de que una herramienta completa su ejecución. El matcher funciona igual que en PreToolUse — filtra por nombre de herramienta.

Flujo:
Claude decide usar Edit → PreToolUse hook → Edit se ejecuta → PostToolUse hook
                           (valida)          (modifica archivo)   (reacciona)

El input JSON de PostToolUse

PostToolUse recibe el mismo formato JSON que PreToolUse, pero con información adicional sobre el resultado:

{
  "hook_event_name": "PostToolUse",
  "tool_name": "Edit",
  "tool_input": {
    "file_path": "src/api/routes.py",
    "old_string": "def get_users():",
    "new_string": "def get_users(skip: int = 0, limit: int = 100):"
  },
  "session_id": "abc123",
  "transcript_path": "/tmp/claude/transcript-abc123.json"
}

Exit codes en PostToolUse

Exit CodeEfecto
0OK — continúa normalmente
1Error reportado a Claude — Claude ve el output del script y puede decidir corregir
2No aplica (la herramienta ya se ejecutó) — se trata como error

La diferencia clave con PreToolUse: exit code 2 en PostToolUse no puede revertir lo que la herramienta ya hizo. La herramienta ya se ejecutó. Exit 1 es el código útil aquí — reporta un problema a Claude para que decida cómo reaccionar.

Pattern 1: Auto-lint después de ediciones

El caso de uso más valioso de PostToolUse. Cada vez que Claude edita un archivo, el linter corre automáticamente:

./scripts/auto-lint.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 [ ! -f "$FILE" ]; then
    exit 0
fi

EXTENSION="${FILE##*.}"

case "$EXTENSION" in
    py)
        RESULT=$(ruff check "$FILE" 2>&1)
        if [ $? -ne 0 ]; then
            echo "LINT ERROR in $FILE:"
            echo "$RESULT"
            exit 1
        fi
        ;;
    ts|tsx|js|jsx)
        RESULT=$(npx eslint "$FILE" --no-warn-ignored 2>&1)
        if [ $? -ne 0 ]; then
            echo "LINT ERROR in $FILE:"
            echo "$RESULT"
            exit 1
        fi
        ;;
    *)
        exit 0
        ;;
esac

exit 0

Configuración:

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

Cuando el lint falla (exit 1), Claude recibe el output del linter y puede decidir corregir automáticamente. Esto crea un ciclo de auto-corrección:

Claude edita archivo
  → PostToolUse: lint falla → output va a Claude
    → Claude edita para corregir
      → PostToolUse: lint pasa → exit 0
        → Claude continúa

Pattern 2: Auto-format después de ediciones

Similar al auto-lint, pero formatea el código automáticamente en lugar de solo reportar:

./scripts/auto-format.sh:

#!/bin/bash

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

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

EXTENSION="${FILE##*.}"

case "$EXTENSION" in
    py)
        ruff format "$FILE" --quiet 2>/dev/null
        ;;
    ts|tsx|js|jsx)
        npx prettier --write "$FILE" --log-level error 2>/dev/null
        ;;
    json)
        npx prettier --write "$FILE" --log-level error 2>/dev/null
        ;;
esac

exit 0

Con auto-format, el hook modifica el archivo directamente. No necesita reportar a Claude porque la corrección es automática. Exit 0 siempre.

Pattern 3: Auto-test después de ediciones en archivos específicos

./scripts/auto-test.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 -q "^src/api/"; then
    TEST_FILE="tests/test_$(basename "$FILE")"
    if [ -f "$TEST_FILE" ]; then
        RESULT=$(python -m pytest "$TEST_FILE" -x --tb=short 2>&1)
        if [ $? -ne 0 ]; then
            echo "TEST FAILURE after editing $FILE:"
            echo "$RESULT" | tail -20
            exit 1
        fi
    fi
fi

exit 0

Si editas un archivo en src/api/, el hook busca su archivo de test correspondiente y lo ejecuta. Si falla, Claude recibe los errores del test.

Pattern 4: Logging de cambios

./scripts/log-changes.sh:

#!/bin/bash

INPUT=$(cat -)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")

LOG_DIR=".claude/logs"
mkdir -p "$LOG_DIR"

if [ -n "$FILE" ]; then
    echo "$TIMESTAMP | $TOOL | $FILE" >> "$LOG_DIR/changes.log"
fi

exit 0

Este hook crea un log de todos los archivos que Claude toca durante la sesión. Útil para auditoría y debugging.


SubagentStart y SubagentStop: Ciclo de Vida de Subagents

SubagentStart: Cuando un subagent arranca

Se dispara cuando Claude lanza un subagent. El matcher filtra por el nombre del tipo de agente.

{
  "hooks": {
    "SubagentStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/subagent-start.sh"
          }
        ]
      }
    ]
  }
}

./scripts/subagent-start.sh:

#!/bin/bash

INPUT=$(cat -)
AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_name // .tool_input.agent_name // "unknown"')
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")

LOG_DIR=".claude/logs"
mkdir -p "$LOG_DIR"

echo "$TIMESTAMP | START | $AGENT_TYPE" >> "$LOG_DIR/subagents.log"

exit 0

SubagentStop: Cuando un subagent termina

El hook más valioso para tracking. Se dispara cuando un subagent completa su ejecución. Puedes generar reportes, métricas, o triggear acciones basadas en lo que el subagent produjo.

./scripts/subagent-stop.sh:

#!/bin/bash

INPUT=$(cat -)
AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_name // .tool_input.agent_name // "unknown"')
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")

LOG_DIR=".claude/logs"
mkdir -p "$LOG_DIR"

echo "$TIMESTAMP | STOP  | $AGENT_TYPE" >> "$LOG_DIR/subagents.log"

exit 0

Matcher en SubagentStart/SubagentStop

El matcher filtra por nombre del tipo de agente:

{
  "hooks": {
    "SubagentStop": [
      {
        "matcher": "backend-agent",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/backend-report.sh"
          }
        ]
      },
      {
        "matcher": "frontend-agent",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/frontend-report.sh"
          }
        ]
      }
    ]
  }
}

Pattern: Reporte automático por subagent

./scripts/subagent-report.sh:

#!/bin/bash

INPUT=$(cat -)
AGENT=$(echo "$INPUT" | jq -r '.agent_name // "unknown"')
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")

REPORT_DIR=".claude/reports"
mkdir -p "$REPORT_DIR"

REPORT_FILE="$REPORT_DIR/subagent-${AGENT}-$(date +%Y%m%d-%H%M%S).md"

CHANGED_FILES=$(git diff --name-only 2>/dev/null)

cat > "$REPORT_FILE" << EOF
# Subagent Report: $AGENT
**Timestamp:** $TIMESTAMP

## Files Changed
$CHANGED_FILES

## Git Diff Summary
$(git diff --stat 2>/dev/null)
EOF

echo "Report generated: $REPORT_FILE"
exit 0

Stop: Cleanup al Final de la Respuesta

Cuándo se dispara

Stop se ejecuta cuando Claude termina de responder en su turno actual. No cuando la sesión cierra — cuando Claude completa una respuesta.

Caso de uso: Reporte de sesión

./scripts/session-summary.sh:

#!/bin/bash

TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
REPORT_DIR=".claude/reports"
mkdir -p "$REPORT_DIR"

CHANGED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
ADDED=$(git diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ')
LOG_FILE=".claude/logs/changes.log"

REPORT_FILE="$REPORT_DIR/session-$(date +%Y%m%d-%H%M%S).md"

cat > "$REPORT_FILE" << EOF
# Session Summary
**Timestamp:** $TIMESTAMP

## Changes
- Files modified: $CHANGED
- Files staged: $ADDED

## Tool Usage
$(if [ -f "$LOG_FILE" ]; then cat "$LOG_FILE"; else echo "No log available"; fi)

## Git Status
$(git status --short 2>/dev/null)
EOF

exit 0

Configuración:

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

Stop no tiene matcher

Como SessionStart, Stop no filtra por herramienta ni por agente. Se dispara cada vez que Claude termina de responder. Si la lógica del Stop hook es pesada, considera agregar condiciones internas:

#!/bin/bash

CHANGED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')

if [ "$CHANGED" -eq 0 ]; then
    exit 0
fi

# Solo genera reporte si hubo cambios
./scripts/generate-report.sh

PermissionRequest: Aprobación Automática

Cuándo se dispara

PermissionRequest se dispara cuando Claude necesita un permiso que no está pre-aprobado. Normalmente, te aparecería una pregunta en la terminal: "¿Puedo ejecutar este comando?" Con un hook, puedes auto-aprobar o auto-rechazar basándote en reglas.

Caso de uso: Auto-aprobar operaciones seguras

./scripts/auto-approve.sh:

#!/bin/bash

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

case "$TOOL" in
    "Read"|"Glob"|"Grep")
        exit 0
        ;;
    "Bash")
        if echo "$COMMAND" | grep -qE "^(ls|cat|head|tail|wc|grep|find|echo|pwd|date)"; then
            exit 0
        fi
        echo "Requiere aprobación manual: $COMMAND"
        exit 1
        ;;
    *)
        exit 1
        ;;
esac

Configuración:

{
  "hooks": {
    "PermissionRequest": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/auto-approve.sh"
          }
        ]
      }
    ]
  }
}

Exit 0 en PermissionRequest auto-aprueba el permiso. Exit 1 deja que el flujo normal continue (te pregunta a ti). Exit 2 rechaza el permiso automáticamente.


Configuración Completa: Todos los Hooks Reactivos

Un settings.json con todos los hooks de reacción configurados:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/auto-format.sh"
          },
          {
            "type": "command",
            "command": "./scripts/auto-lint.sh"
          }
        ]
      },
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/log-changes.sh"
          }
        ]
      }
    ],
    "SubagentStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/subagent-start.sh"
          }
        ]
      }
    ],
    "SubagentStop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/subagent-report.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/session-summary.sh"
          }
        ]
      }
    ]
  }
}

Con esta configuración, cada acción de Claude genera reacciones automáticas:

  • Edita un archivo → se formatea y lintea automáticamente
  • Cualquier herramienta → se loguea el uso
  • Subagent arranca → se registra el inicio
  • Subagent termina → se genera un reporte
  • Claude termina su respuesta → se genera un resumen de sesión

El Ciclo de Vida Completo

Todos los hooks en orden

SessionStart ──→ Setup (una vez al inicio)
     │
     ▼
PreToolUse ───→ Validar (antes de cada herramienta)
     │
     ▼
[Herramienta se ejecuta]
     │
     ▼
PostToolUse ──→ Reaccionar (después de cada herramienta)
     │
     ▼
SubagentStart → Log inicio (cuando un subagent arranca)
     │
     ▼
[Subagent trabaja]
     │
     ▼
SubagentStop ─→ Reportar (cuando un subagent termina)
     │
     ▼
Stop ─────────→ Cleanup (cuando Claude termina de responder)
     │
     ▼
PermissionRequest → Aprobar/Rechazar (cuando se necesita permiso)

Interacción entre hooks

Los hooks no se pisan entre sí, pero pueden crear ciclos:

Claude edita archivo
  → PostToolUse: auto-lint falla (exit 1)
    → Claude recibe error, edita para corregir
      → PostToolUse: auto-lint se ejecuta de nuevo
        → Si pasa: exit 0, continúa
        → Si falla de nuevo: exit 1, Claude intenta otra corrección

Este ciclo es deseable (auto-corrección), pero puede quedarse en loop si el problema no tiene solución obvia. Claude generalmente desiste después de 2-3 intentos fallidos. Si necesitas un límite explícito, agrégalo en el script:

#!/bin/bash

ATTEMPT_FILE="/tmp/lint-attempts-$$"

ATTEMPTS=0
if [ -f "$ATTEMPT_FILE" ]; then
    ATTEMPTS=$(cat "$ATTEMPT_FILE")
fi

if [ "$ATTEMPTS" -ge 3 ]; then
    echo "WARNING: 3+ lint failures. Continuing without fixing."
    rm -f "$ATTEMPT_FILE"
    exit 0
fi

echo $((ATTEMPTS + 1)) > "$ATTEMPT_FILE"

# ... lint logic ...

Comparación: PreToolUse vs PostToolUse

AspectoPreToolUsePostToolUse
TimingAntes de la ejecuciónDespués de la ejecución
Puede bloquearSí (exit 2)No (ya se ejecutó)
Exit 1Claude decide si continuarClaude recibe el error y puede corregir
Caso de usoValidar, prevenirReaccionar, corregir, loguear
PerformanceDebe ser rápidoPuede ser más pesado
MatcherNombre de herramientaNombre de herramienta
CicloNo crea ciclosPuede crear ciclos de corrección
RevertirPreviene la acciónNo puede revertir

Regla práctica: Si puedes detectar el problema antes → PreToolUse. Si necesitas ver el resultado para reaccionar → PostToolUse.


Troubleshooting

"El auto-lint PostToolUse no reporta errores a Claude"

Causa: El script retorna exit 0 siempre, incluso cuando el lint falla.

Solución: Asegúrate de que el script retorna exit 1 cuando el lint detecta errores. El output del script (stdout) se envía a Claude como contexto:

RESULT=$(ruff check "$FILE" 2>&1)
if [ $? -ne 0 ]; then
    echo "$RESULT"
    exit 1
fi

"SubagentStop no se dispara"

Causa: El matcher no coincide con el nombre del agente, o los hooks de subagent no están soportados en tu versión.

Solución: Verifica el nombre exacto del agente. Usa un hook sin matcher para capturar todos los subagents y confirmar que el evento se dispara:

{
  "hooks": {
    "SubagentStop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'SubagentStop fired' >> /tmp/hook-debug.log"
          }
        ]
      }
    ]
  }
}

"Stop hook genera reportes en cada respuesta (demasiados)"

Causa: Stop se dispara en cada turno de Claude, no solo al final de la sesión.

Solución: Agrega lógica condicional para solo generar reportes significativos:

#!/bin/bash

CHANGED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')

if [ "$CHANGED" -eq 0 ]; then
    exit 0
fi

# Solo genera reporte si hubo cambios reales

"Auto-format PostToolUse modifica archivos y confunde a Claude"

Causa: El auto-format cambia el contenido del archivo después de que Claude lo editó. Claude puede notar la discrepancia.

Solución: Auto-format es generalmente transparente, pero si causa problemas, usa auto-lint (que reporta sin modificar) en lugar de auto-format. Alternativamente, configura el formatter para que sea consistente con las convenciones que Claude ya sigue.

"PermissionRequest hook no auto-aprueba"

Causa: El hook retorna exit 1 (flujo normal) en lugar de exit 0 (auto-aprobar).

Solución: Verifica que el script retorna exit 0 para los casos que quieres auto-aprobar. Testa manualmente:

echo '{"tool_name": "Read"}' | ./scripts/auto-approve.sh
echo $?

Ejercicios

Ejercicio 1: Auto-lint para Python (Fácil)

Crea un hook PostToolUse que ejecute ruff check en archivos Python después de cada Edit o Write. Si el lint falla, reporta el error a Claude (exit 1).

Ver solución

./scripts/lint-python.sh:

#!/bin/bash
INPUT=$(cat -)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')

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

if [[ "$FILE" != *.py ]]; then
    exit 0
fi

RESULT=$(ruff check "$FILE" 2>&1)
if [ $? -ne 0 ]; then
    echo "Lint errors in $FILE:"
    echo "$RESULT"
    exit 1
fi

exit 0
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "./scripts/lint-python.sh" }
        ]
      }
    ]
  }
}

Ejercicio 2: Log de subagents con duración (Fácil)

Crea hooks SubagentStart y SubagentStop que logueen el inicio y fin de cada subagent en un archivo .claude/logs/agents.log, incluyendo timestamps que permitan calcular la duración.

Ver solución

./scripts/agent-log-start.sh:

#!/bin/bash
INPUT=$(cat -)
AGENT=$(echo "$INPUT" | jq -r '.agent_name // "unknown"')
echo "$(date +%s) | START | $AGENT | $(date +"%H:%M:%S")" >> .claude/logs/agents.log
exit 0

./scripts/agent-log-stop.sh:

#!/bin/bash
INPUT=$(cat -)
AGENT=$(echo "$INPUT" | jq -r '.agent_name // "unknown"')
echo "$(date +%s) | STOP  | $AGENT | $(date +"%H:%M:%S")" >> .claude/logs/agents.log
exit 0
{
  "hooks": {
    "SubagentStart": [
      { "hooks": [{ "type": "command", "command": "./scripts/agent-log-start.sh" }] }
    ],
    "SubagentStop": [
      { "hooks": [{ "type": "command", "command": "./scripts/agent-log-stop.sh" }] }
    ]
  }
}

El timestamp Unix al inicio permite calcular duración restando START de STOP para el mismo agente.

Ejercicio 3: Stop hook con reporte condicional (Medio)

Crea un hook Stop que genere un reporte Markdown solo si Claude modificó más de 3 archivos durante la sesión. El reporte debe incluir: lista de archivos, git diff stats, y hora.

Ver solución

./scripts/conditional-report.sh:

#!/bin/bash

CHANGED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')

if [ "$CHANGED" -le 3 ]; then
    exit 0
fi

REPORT_DIR=".claude/reports"
mkdir -p "$REPORT_DIR"
REPORT="$REPORT_DIR/session-$(date +%Y%m%d-%H%M%S).md"

cat > "$REPORT" << EOF
# Session Report — $(date +"%Y-%m-%d %H:%M:%S")

## Files Changed ($CHANGED)
$(git diff --name-only 2>/dev/null)

## Diff Stats
$(git diff --stat 2>/dev/null)
EOF

echo "Report: $REPORT"
exit 0

Ejercicio 4: PostToolUse con auto-test selectivo (Medio)

Crea un hook PostToolUse que:

  1. Solo se active para archivos en src/
  2. Busque un test file correspondiente en tests/
  3. Si existe, lo ejecute con pytest
  4. Si el test falla, reporte a Claude (exit 1)
  5. Si no hay test file, no haga nada (exit 0)
Ver solución

./scripts/auto-test-selective.sh:

#!/bin/bash
INPUT=$(cat -)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')

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

if ! echo "$FILE" | grep -q "^src/"; then
    exit 0
fi

if [[ "$FILE" != *.py ]]; then
    exit 0
fi

BASENAME=$(basename "$FILE")
MODULE=$(echo "$FILE" | sed 's|^src/||' | sed 's|/|_|g' | sed 's|\.py$||')

TEST_CANDIDATES=(
    "tests/test_${BASENAME}"
    "tests/test_${MODULE}.py"
)

for TEST_FILE in "${TEST_CANDIDATES[@]}"; do
    if [ -f "$TEST_FILE" ]; then
        RESULT=$(python -m pytest "$TEST_FILE" -x --tb=short 2>&1)
        if [ $? -ne 0 ]; then
            echo "Test failure after editing $FILE:"
            echo "$RESULT" | tail -15
            exit 1
        fi
        exit 0
    fi
done

exit 0

Ejercicio 5: Combinación completa de hooks reactivos (Difícil)

Diseña un settings.json con:

  1. PostToolUse para Edit/Write: auto-format + lint (dos comandos encadenados)
  2. SubagentStop: genera reporte por subagent
  3. Stop: genera resumen de sesión solo si hubo más de 5 herramientas usadas
  4. PermissionRequest: auto-aprueba Read/Glob/Grep, rechaza Drop/Delete

Escribe la configuración JSON y los scripts.

Ver solución

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "./scripts/hooks/format.sh" },
          { "type": "command", "command": "./scripts/hooks/lint.sh" }
        ]
      }
    ],
    "SubagentStop": [
      {
        "hooks": [
          { "type": "command", "command": "./scripts/hooks/agent-report.sh" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "./scripts/hooks/session-report.sh" }
        ]
      }
    ],
    "PermissionRequest": [
      {
        "hooks": [
          { "type": "command", "command": "./scripts/hooks/auto-perms.sh" }
        ]
      }
    ]
  }
}

./scripts/hooks/format.sh:

#!/bin/bash
INPUT=$(cat -)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
case "${FILE##*.}" in
    py) ruff format "$FILE" --quiet 2>/dev/null ;;
    ts|tsx|js|jsx) npx prettier --write "$FILE" --log-level error 2>/dev/null ;;
esac
exit 0

./scripts/hooks/lint.sh:

#!/bin/bash
INPUT=$(cat -)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
case "${FILE##*.}" in
    py) RESULT=$(ruff check "$FILE" 2>&1); [ $? -ne 0 ] && echo "$RESULT" && exit 1 ;;
    ts|tsx|js|jsx) RESULT=$(npx eslint "$FILE" 2>&1); [ $? -ne 0 ] && echo "$RESULT" && exit 1 ;;
esac
exit 0

./scripts/hooks/agent-report.sh:

#!/bin/bash
INPUT=$(cat -)
AGENT=$(echo "$INPUT" | jq -r '.agent_name // "unknown"')
mkdir -p .claude/reports
echo "## $AGENT — $(date +%H:%M:%S)" >> .claude/reports/agents.md
echo "Files: $(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')" >> .claude/reports/agents.md
echo "" >> .claude/reports/agents.md
exit 0

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

#!/bin/bash
LOG=".claude/logs/changes.log"
[ ! -f "$LOG" ] && exit 0
COUNT=$(wc -l < "$LOG" | tr -d ' ')
[ "$COUNT" -le 5 ] && exit 0
mkdir -p .claude/reports
cat > ".claude/reports/session-$(date +%Y%m%d-%H%M%S).md" << EOF
# Session: $(date)
Tools used: $COUNT
$(cat "$LOG")
EOF
exit 0

./scripts/hooks/auto-perms.sh:

#!/bin/bash
INPUT=$(cat -)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
case "$TOOL" in
    Read|Glob|Grep) exit 0 ;;
esac
if echo "$CMD" | grep -qiE "(DROP|DELETE|TRUNCATE)"; then
    echo "BLOCKED: destructive operation"
    exit 2
fi
exit 1

Resumen

  • PostToolUse se dispara después de cada ejecución de herramienta — ideal para auto-lint, auto-format, auto-test, y logging
  • Exit 1 en PostToolUse reporta errores a Claude, que puede decidir corregir automáticamente — creando un ciclo de auto-corrección
  • SubagentStart y SubagentStop rastrean el ciclo de vida de subagents — útil para logging, métricas, y reportes por agente
  • Stop se dispara cuando Claude termina de responder — el lugar para reportes de sesión y cleanup
  • PermissionRequest permite auto-aprobar (exit 0) o auto-rechazar (exit 2) solicitudes de permisos
  • PostToolUse puede crear ciclos de corrección (edit → lint falla → Claude corrige → lint again) — generalmente deseable, pero agrega límites si es necesario
  • El ciclo de vida completo es: SessionStart → PreToolUse → [ejecución] → PostToolUse → SubagentStart → [subagent] → SubagentStop → Stop
  • Todos los hooks reciben JSON via stdin y comunican decisiones via exit codes (0, 1, 2)

Recursos Adicionales

  1. Claude Code Hooks (Anthropic Docs) — Documentación oficial de PostToolUse, SubagentStart/Stop, Stop
  2. Claude Code Settings — Configuración de hooks en settings.json
  3. Ruff — Python Linter — Linter rápido para Python, ideal para hooks PostToolUse
  4. ESLint — Linter de JavaScript/TypeScript para hooks
  5. Prettier — Formatter para hooks de auto-format
  6. Claude Code Sub-agents — Referencia de subagents para entender eventos SubagentStart/Stop
  7. jq Manual — Parseo de JSON en scripts de hooks
  8. Claude Code Best Practices — Buenas prácticas de automatización

Siguiente cápsula: En la cápsula 04 cruzas del mundo de los hooks al mundo del SDK. Aprenderás a ejecutar Claude Code desde scripts de Python — no como herramienta interactiva, sino como un servicio que recibe prompts y retorna resultados JSON. Scripts de changelog automático, code review programático, y corrección de tests sin tocar la terminal. Claude Code se convierte en una función Python que puedes llamar desde cualquier pipeline.