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 Code | Efecto |
|---|---|
0 | OK — continúa normalmente |
1 | Error reportado a Claude — Claude ve el output del script y puede decidir corregir |
2 | No 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
| Aspecto | PreToolUse | PostToolUse |
|---|---|---|
| Timing | Antes de la ejecución | Después de la ejecución |
| Puede bloquear | Sí (exit 2) | No (ya se ejecutó) |
| Exit 1 | Claude decide si continuar | Claude recibe el error y puede corregir |
| Caso de uso | Validar, prevenir | Reaccionar, corregir, loguear |
| Performance | Debe ser rápido | Puede ser más pesado |
| Matcher | Nombre de herramienta | Nombre de herramienta |
| Ciclo | No crea ciclos | Puede crear ciclos de corrección |
| Revertir | Previene la acción | No 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:
- Solo se active para archivos en
src/ - Busque un test file correspondiente en
tests/ - Si existe, lo ejecute con pytest
- Si el test falla, reporte a Claude (exit 1)
- 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:
- PostToolUse para Edit/Write: auto-format + lint (dos comandos encadenados)
- SubagentStop: genera reporte por subagent
- Stop: genera resumen de sesión solo si hubo más de 5 herramientas usadas
- 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
- Claude Code Hooks (Anthropic Docs) — Documentación oficial de PostToolUse, SubagentStart/Stop, Stop
- Claude Code Settings — Configuración de hooks en settings.json
- Ruff — Python Linter — Linter rápido para Python, ideal para hooks PostToolUse
- ESLint — Linter de JavaScript/TypeScript para hooks
- Prettier — Formatter para hooks de auto-format
- Claude Code Sub-agents — Referencia de subagents para entender eventos SubagentStart/Stop
- jq Manual — Parseo de JSON en scripts de hooks
- 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.