Módulo 6: Hooks Avanzados y SDK Headless
6. Proyecto — Workflow Automatizado con Hooks + SDK End-to-End
6. Proyecto — Workflow Automatizado con Hooks + SDK End-to-End
Descripción del Proyecto
Has aprendido cada pieza del puzzle por separado: SessionStart para setup automático, PreToolUse para validación, PostToolUse para reacción post-ejecución, SubagentStop para tracking de subagents, y el SDK headless para ejecución programática desde Python y TypeScript. Ahora integras todo en un pipeline funcional.
En este proyecto construyes un workflow automatizado completo: un script Python que arranca Claude Code en modo headless, le pide implementar una feature, y hooks reaccionan a cada evento durante la ejecución. SessionStart verifica el entorno, PreToolUse bloquea operaciones peligrosas, PostToolUse auto-lintea cada edición, SubagentStop genera logs por subagent, y Stop produce un reporte final de sesión. El script Python procesa el resultado y genera un resumen ejecutivo.
El flujo es: tú ejecutas python scripts/automated-workflow.py "Implementa feature X" y el sistema se encarga del resto. El script invoca Claude Code, los hooks reaccionan a cada evento, y al final recibes un reporte con los archivos modificados, el costo, y el resultado.
Este proyecto cierra el Módulo 6 y la Phase 2 de la guía. Si el pipeline ejecuta de principio a fin con hooks reaccionando en cada punto — has dominado la automatización de Claude Code. Si además puedes explicar cómo cada hook contribuye al pipeline y cómo el SDK orquesta todo — has internalizado el modelo mental de hooks + SDK como sistema integrado.
Objetivo del Proyecto
Construir un workflow automatizado que integre 4 hooks (SessionStart, PreToolUse, PostToolUse, SubagentStop) con un script SDK Python, ejecutarlo sobre un proyecto real, y analizar el pipeline completo.
Al completar este proyecto:
- ✅ Tendrás 4 scripts de hooks en
scripts/hooks/que reaccionan a eventos del ciclo de vida - ✅ Un
settings.jsoncon toda la configuración de hooks - ✅ Un script Python SDK que orquesta la ejecución completa
- ✅ Cada edición de archivo será auto-linteada por PostToolUse
- ✅ Cada comando Bash será validado por PreToolUse
- ✅ Reportes de sesión generados automáticamente por Stop
- ✅ Un pipeline end-to-end que funciona sin intervención humana
Duración estimada: 1-1.5 horas (setup: 15 min + hooks: 20 min + SDK script: 20 min + ejecución: 15 min + iteración: 15 min).
Especificaciones Técnicas
Stack Tecnológico
- Herramienta: Claude Code (versión reciente con soporte de hooks)
- Scripts de hooks: Bash
- Script SDK: Python 3.8+
- Dependencias:
jq(para parsing JSON en bash) - Proyecto base: Cualquier proyecto con código fuente en
src/
Requisitos del Proyecto Base
| Requisito | Mínimo | Ideal |
|---|---|---|
Directorio src/ | Con 3+ archivos | Con módulos separados |
| Archivos Python o TypeScript | Al menos 5 | 10+ |
| Linter instalado | ruff o eslint | Ambos |
| Git inicializado | Sí | Con 3+ commits |
| Python 3.8+ | Instalado | Con venv |
jq | Instalado | — |
Verificación de requisitos
python3 --version
jq --version
claude --version
git status
ls src/
Estructura Final del Proyecto
Al terminar, tu proyecto tendrá estos archivos adicionales:
your-project/
├── .claude/
│ ├── settings.json ← Configuración de hooks
│ ├── logs/ ← Logs generados por hooks
│ │ ├── changes.log
│ │ └── subagents.log
│ └── reports/ ← Reportes de sesión
│ └── session-*.md
├── scripts/
│ ├── hooks/
│ │ ├── session-setup.sh ← SessionStart hook
│ │ ├── validate-commands.sh ← PreToolUse hook
│ │ ├── auto-lint.sh ← PostToolUse hook
│ │ ├── subagent-report.sh ← SubagentStop hook
│ │ └── session-summary.sh ← Stop hook
│ └── automated-workflow.py ← Script SDK orquestador
└── src/ ← Tu código fuente
Paso 1: Crear la Estructura de Directorios
mkdir -p scripts/hooks
mkdir -p .claude/logs
mkdir -p .claude/reports
Paso 2: Hook SessionStart — Setup del Entorno
Este hook se ejecuta una vez al inicio de cada sesión. Verifica que el entorno esté listo.
Crea scripts/hooks/session-setup.sh:
#!/bin/bash
echo "=== SESSION SETUP ==="
LOG_DIR=".claude/logs"
REPORT_DIR=".claude/reports"
mkdir -p "$LOG_DIR" "$REPORT_DIR"
if [ -f "$LOG_DIR/changes.log" ]; then
BACKUP="$LOG_DIR/changes-$(date +%Y%m%d-%H%M%S).log.bak"
mv "$LOG_DIR/changes.log" "$BACKUP"
fi
touch "$LOG_DIR/changes.log"
touch "$LOG_DIR/subagents.log"
if ! git rev-parse --git-dir > /dev/null 2>&1; then
echo "ERROR: No es un repositorio git"
exit 1
fi
BRANCH=$(git branch --show-current 2>/dev/null)
echo "Branch: $BRANCH"
DIRTY=$(git status --porcelain | wc -l | tr -d ' ')
if [ "$DIRTY" -gt 20 ]; then
echo "WARNING: $DIRTY archivos sin commitear"
echo "Considera hacer commit antes de continuar"
exit 1
fi
if [ -f "requirements.txt" ] && [ -d ".venv" ]; then
source .venv/bin/activate 2>/dev/null
fi
if [ -f "package.json" ]; then
if [ ! -d "node_modules" ]; then
echo "Instalando dependencias npm..."
npm ci --silent 2>/dev/null
fi
fi
if command -v ruff &> /dev/null; then
echo "Linter: ruff $(ruff --version 2>/dev/null)"
elif command -v npx &> /dev/null; then
echo "Linter: eslint (via npx)"
else
echo "WARNING: No se encontró linter (ruff o eslint)"
fi
echo "Session ID: $(date +%Y%m%d-%H%M%S)"
echo "=== SETUP COMPLETE ==="
exit 0
Hazlo ejecutable:
chmod +x scripts/hooks/session-setup.sh
Paso 3: Hook PreToolUse — Validación de Comandos
Este hook se ejecuta antes de cada invocación de la herramienta Bash. Bloquea comandos peligrosos y advierte sobre operaciones sensibles.
Crea scripts/hooks/validate-commands.sh:
#!/bin/bash
INPUT=$(cat -)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if [ "$TOOL_NAME" != "Bash" ] || [ -z "$COMMAND" ]; then
exit 0
fi
BLOCKED_PATTERNS=(
"rm -rf /"
"rm -rf ~"
"rm -rf \."
"DROP DATABASE"
"DROP TABLE"
"TRUNCATE TABLE"
"mkfs"
"dd if="
":(){:|:&};:"
"chmod -R 777 /"
"npm publish"
"git push --force"
"git reset --hard"
)
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "BLOCKED: Comando peligroso detectado"
echo "Pattern: $pattern"
echo "Comando: $COMMAND"
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
echo "$TIMESTAMP | BLOCKED | $pattern | $COMMAND" >> .claude/logs/changes.log
exit 2
fi
done
WARNING_PATTERNS=(
"sudo"
"chmod 777"
"curl.*|.*sh"
"wget.*|.*bash"
"pip install"
"npm install.*-g"
)
for pattern in "${WARNING_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "WARNING: Comando potencialmente riesgoso"
echo "Pattern: $pattern"
echo "Comando: $COMMAND"
exit 1
fi
done
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
echo "$TIMESTAMP | ALLOWED | Bash | $COMMAND" >> .claude/logs/changes.log
exit 0
Hazlo ejecutable:
chmod +x scripts/hooks/validate-commands.sh
Paso 4: Hook PostToolUse — Auto-Lint Después de Ediciones
Este hook se ejecuta después de cada Edit o Write. Lintea el archivo modificado y reporta errores a Claude.
Crea scripts/hooks/auto-lint.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
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
echo "$TIMESTAMP | EDITED | $TOOL_NAME | $FILE" >> .claude/logs/changes.log
EXTENSION="${FILE##*.}"
case "$EXTENSION" in
py)
if command -v ruff &> /dev/null; then
LINT_RESULT=$(ruff check "$FILE" 2>&1)
LINT_EXIT=$?
if [ $LINT_EXIT -ne 0 ]; then
echo "LINT ERROR in $FILE:"
echo "$LINT_RESULT"
echo "$TIMESTAMP | LINT_FAIL | $FILE" >> .claude/logs/changes.log
exit 1
fi
echo "$TIMESTAMP | LINT_PASS | $FILE" >> .claude/logs/changes.log
fi
;;
ts|tsx)
if command -v npx &> /dev/null; then
LINT_RESULT=$(npx eslint "$FILE" --no-warn-ignored 2>&1)
LINT_EXIT=$?
if [ $LINT_EXIT -ne 0 ]; then
echo "LINT ERROR in $FILE:"
echo "$LINT_RESULT"
echo "$TIMESTAMP | LINT_FAIL | $FILE" >> .claude/logs/changes.log
exit 1
fi
echo "$TIMESTAMP | LINT_PASS | $FILE" >> .claude/logs/changes.log
fi
;;
js|jsx)
if command -v npx &> /dev/null; then
LINT_RESULT=$(npx eslint "$FILE" --no-warn-ignored 2>&1)
LINT_EXIT=$?
if [ $LINT_EXIT -ne 0 ]; then
echo "LINT ERROR in $FILE:"
echo "$LINT_RESULT"
echo "$TIMESTAMP | LINT_FAIL | $FILE" >> .claude/logs/changes.log
exit 1
fi
echo "$TIMESTAMP | LINT_PASS | $FILE" >> .claude/logs/changes.log
fi
;;
*)
;;
esac
exit 0
Hazlo ejecutable:
chmod +x scripts/hooks/auto-lint.sh
Paso 5: Hook SubagentStop — Reporte por Subagent
Este hook se ejecuta cuando un subagent termina su ejecución. Genera un log con los archivos cambiados.
Crea scripts/hooks/subagent-report.sh:
#!/bin/bash
INPUT=$(cat -)
AGENT_NAME=$(echo "$INPUT" | jq -r '.agent_name // "unknown-agent"')
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
LOG_FILE=".claude/logs/subagents.log"
CHANGED_FILES=$(git diff --name-only 2>/dev/null | head -20)
CHANGED_COUNT=$(echo "$CHANGED_FILES" | grep -c . 2>/dev/null || echo "0")
cat >> "$LOG_FILE" << EOF
--- Subagent Report ---
Agent: $AGENT_NAME
Timestamp: $TIMESTAMP
Files changed: $CHANGED_COUNT
$CHANGED_FILES
-----------------------
EOF
echo "Subagent $AGENT_NAME completed. $CHANGED_COUNT files changed."
exit 0
Hazlo ejecutable:
chmod +x scripts/hooks/subagent-report.sh
Paso 6: Hook Stop — Resumen de Sesión
Este hook se ejecuta cuando Claude termina de responder. Genera un reporte Markdown de la sesión.
Crea scripts/hooks/session-summary.sh:
#!/bin/bash
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
DATE_STR=$(date +"%Y%m%d-%H%M%S")
REPORT_DIR=".claude/reports"
LOG_FILE=".claude/logs/changes.log"
AGENT_LOG=".claude/logs/subagents.log"
CHANGED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
if [ "$CHANGED" -eq 0 ] && [ ! -s "$LOG_FILE" ]; then
exit 0
fi
REPORT_FILE="$REPORT_DIR/session-$DATE_STR.md"
TOOL_COUNT=0
LINT_FAILS=0
BLOCKED_COUNT=0
if [ -f "$LOG_FILE" ]; then
TOOL_COUNT=$(wc -l < "$LOG_FILE" | tr -d ' ')
LINT_FAILS=$(grep -c "LINT_FAIL" "$LOG_FILE" 2>/dev/null || echo "0")
BLOCKED_COUNT=$(grep -c "BLOCKED" "$LOG_FILE" 2>/dev/null || echo "0")
fi
cat > "$REPORT_FILE" << EOF
# Session Report
**Date:** $TIMESTAMP
## Summary
- Tool invocations logged: $TOOL_COUNT
- Files with git changes: $CHANGED
- Lint failures caught: $LINT_FAILS
- Commands blocked: $BLOCKED_COUNT
## Files Changed
$(git diff --name-only 2>/dev/null || echo "None")
## Git Diff Stats
$(git diff --stat 2>/dev/null || echo "No changes")
## Tool Usage Log
$(if [ -f "$LOG_FILE" ]; then cat "$LOG_FILE"; else echo "No log"; fi)
## Subagent Activity
$(if [ -f "$AGENT_LOG" ] && [ -s "$AGENT_LOG" ]; then cat "$AGENT_LOG"; else echo "No subagent activity"; fi)
EOF
echo "Session report: $REPORT_FILE"
exit 0
Hazlo ejecutable:
chmod +x scripts/hooks/session-summary.sh
Paso 7: Configurar settings.json
Crea .claude/settings.json con todos los hooks conectados:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "./scripts/hooks/session-setup.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./scripts/hooks/validate-commands.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "./scripts/hooks/auto-lint.sh"
}
]
}
],
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "./scripts/hooks/subagent-report.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "./scripts/hooks/session-summary.sh"
}
]
}
]
}
}
Paso 8: Script SDK Python — El Orquestador
Este es el script que conecta todo. Ejecuta Claude Code en modo headless, deja que los hooks reaccionen a cada evento, y procesa el resultado final.
Crea scripts/automated-workflow.py:
#!/usr/bin/env python3
"""
Automated Workflow: Hooks + SDK Pipeline
Ejecuta Claude Code en modo headless con hooks activos.
Los hooks reaccionan a cada evento del ciclo de vida.
Uso:
python scripts/automated-workflow.py "Descripción de la tarea"
python scripts/automated-workflow.py --file tasks/feature-request.md
"""
import subprocess
import json
import sys
import os
from datetime import datetime
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
LOGS_DIR = PROJECT_ROOT / ".claude" / "logs"
REPORTS_DIR = PROJECT_ROOT / ".claude" / "reports"
def ensure_dirs():
LOGS_DIR.mkdir(parents=True, exist_ok=True)
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
def run_claude(prompt, tools, timeout=600):
cmd = [
"claude", "-p", prompt,
"--output-format", "json",
"--allowedTools", ",".join(tools),
]
print(f"\n{'='*60}")
print(f"EXECUTING: Claude Code (headless)")
print(f"Tools: {', '.join(tools)}")
print(f"Timeout: {timeout}s")
print(f"{'='*60}\n")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
cwd=str(PROJECT_ROOT),
)
except subprocess.TimeoutExpired:
return {
"is_error": True,
"error_type": "timeout",
"result": f"Execution timed out after {timeout}s",
}
except FileNotFoundError:
return {
"is_error": True,
"error_type": "not_found",
"result": "claude CLI not found. Install: npm install -g @anthropic-ai/claude-code",
}
if result.returncode != 0:
return {
"is_error": True,
"error_type": "exit_code",
"result": result.stderr or "Unknown error",
"exit_code": result.returncode,
}
try:
parsed = json.loads(result.stdout)
except json.JSONDecodeError:
return {
"is_error": True,
"error_type": "json_parse",
"result": f"Invalid JSON output: {result.stdout[:200]}",
}
return parsed
def read_task_file(filepath):
with open(filepath, "r") as f:
return f.read().strip()
def collect_hook_logs():
logs = {}
changes_log = LOGS_DIR / "changes.log"
if changes_log.exists():
content = changes_log.read_text().strip()
if content:
lines = content.split("\n")
logs["tool_invocations"] = len(lines)
logs["lint_failures"] = sum(1 for l in lines if "LINT_FAIL" in l)
logs["blocked_commands"] = sum(1 for l in lines if "BLOCKED" in l)
logs["edited_files"] = sum(1 for l in lines if "EDITED" in l)
logs["changes_detail"] = lines[-20:]
agents_log = LOGS_DIR / "subagents.log"
if agents_log.exists():
content = agents_log.read_text().strip()
if content:
logs["subagent_activity"] = content
return logs
def find_latest_report():
if not REPORTS_DIR.exists():
return None
reports = sorted(REPORTS_DIR.glob("session-*.md"), reverse=True)
if reports:
return reports[0]
return None
def generate_summary(task, claude_result, hook_logs, report_path):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
summary = []
summary.append(f"{'='*60}")
summary.append(f"WORKFLOW EXECUTION SUMMARY")
summary.append(f"{'='*60}")
summary.append(f"")
summary.append(f"Timestamp: {timestamp}")
summary.append(f"Task: {task[:100]}...")
summary.append(f"")
if claude_result.get("is_error"):
summary.append(f"Status: FAILED")
summary.append(f"Error: {claude_result.get('result', 'Unknown')}")
else:
summary.append(f"Status: SUCCESS")
summary.append(f"Cost: ${claude_result.get('cost_usd', 0):.4f}")
summary.append(f"Duration: {claude_result.get('duration_ms', 0)}ms")
summary.append(f"Turns: {claude_result.get('num_turns', 0)}")
summary.append(f"")
summary.append(f"--- Hook Activity ---")
summary.append(f"Tool invocations: {hook_logs.get('tool_invocations', 0)}")
summary.append(f"Files edited: {hook_logs.get('edited_files', 0)}")
summary.append(f"Lint failures caught: {hook_logs.get('lint_failures', 0)}")
summary.append(f"Commands blocked: {hook_logs.get('blocked_commands', 0)}")
if hook_logs.get("subagent_activity"):
summary.append(f"")
summary.append(f"--- Subagent Activity ---")
summary.append(hook_logs["subagent_activity"])
if report_path:
summary.append(f"")
summary.append(f"Session report: {report_path}")
if not claude_result.get("is_error"):
summary.append(f"")
summary.append(f"--- Claude Output (first 500 chars) ---")
summary.append(claude_result.get("result", "")[:500])
summary.append(f"")
summary.append(f"{'='*60}")
return "\n".join(summary)
def main():
if len(sys.argv) < 2:
print("Uso:")
print(' python scripts/automated-workflow.py "Descripción de la tarea"')
print(" python scripts/automated-workflow.py --file tasks/feature.md")
sys.exit(1)
if sys.argv[1] == "--file":
if len(sys.argv) < 3:
print("Error: --file requiere una ruta al archivo")
sys.exit(1)
task = read_task_file(sys.argv[2])
else:
task = " ".join(sys.argv[1:])
ensure_dirs()
print(f"\n{'#'*60}")
print(f"# AUTOMATED WORKFLOW — Hooks + SDK Pipeline")
print(f"{'#'*60}")
print(f"\nTask: {task[:100]}...")
print(f"Project: {PROJECT_ROOT}")
print(f"Hooks: SessionStart, PreToolUse, PostToolUse, SubagentStop, Stop")
tools = ["Read", "Write", "Edit", "Grep", "Glob", "Bash"]
result = run_claude(task, tools, timeout=600)
hook_logs = collect_hook_logs()
report_path = find_latest_report()
summary = generate_summary(task, result, hook_logs, report_path)
print(summary)
summary_path = REPORTS_DIR / f"workflow-{datetime.now().strftime('%Y%m%d-%H%M%S')}.txt"
with open(summary_path, "w") as f:
f.write(summary)
if result.get("is_error"):
print(f"\nFull error: {result.get('result', '')}")
sys.exit(1)
print(f"\nWorkflow summary saved: {summary_path}")
sys.exit(0)
if __name__ == "__main__":
main()
Hazlo ejecutable:
chmod +x scripts/automated-workflow.py
Paso 9: Verificar la Configuración
Antes de ejecutar el pipeline completo, verifica cada pieza:
Verificar scripts de hooks
echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' | ./scripts/hooks/validate-commands.sh
echo "Exit code: $?"
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | ./scripts/hooks/validate-commands.sh
echo "Exit code: $?"
echo '{"tool_name":"Edit","tool_input":{"file_path":"src/test.py"}}' | ./scripts/hooks/auto-lint.sh
echo "Exit code: $?"
Resultado esperado:
ls -la→ exit 0 (permitido)rm -rf /→ exit 2 (bloqueado)- Archivo inexistente → exit 0 (no hay archivo que lintear)
Verificar la estructura
ls -la scripts/hooks/
ls -la .claude/settings.json
cat .claude/settings.json | jq .
Verificar que Claude CLI funciona en headless
claude -p "Di 'hola'" --output-format json --allowedTools "Read" | jq .result
Paso 10: Ejecutar el Pipeline Completo
Ejecución básica
python scripts/automated-workflow.py "Agrega type hints a todas las funciones en src/api/ que no los tengan"
Ejecución con archivo de tarea
Crea tasks/add-validation.md:
Agrega validación de input a todos los endpoints en src/api/:
1. Lee cada archivo de routes/endpoints
2. Identifica funciones que reciben parámetros sin validación
3. Agrega Pydantic models para request validation
4. Asegúrate de que cada endpoint retorna errores 422 para input inválido
Solo modifica archivos en src/api/. No toques tests.
python scripts/automated-workflow.py --file tasks/add-validation.md
Lo que deberías observar
1. SessionStart se ejecuta:
=== SESSION SETUP ===
Branch: feature/hooks-demo
Linter: ruff 0.8.x
Session ID: 20260313-143022
=== SETUP COMPLETE ===
2. PreToolUse valida cada comando Bash:
- Comandos seguros (
ls,cat,python -m pytest) → exit 0, permitidos - Comandos peligrosos (si Claude intentara alguno) → exit 2, bloqueados
3. PostToolUse lintea cada edición:
- Si Claude edita un archivo
.py→ ruff check se ejecuta - Si el lint falla → Claude recibe el error y corrige
- Si el lint pasa → exit 0, continúa
4. SubagentStop registra actividad (si Claude usa subagents):
- Cada subagent que termina genera una entrada en el log
5. Stop genera el reporte de sesión:
- Un archivo
session-*.mdaparece en.claude/reports/
6. El script Python genera el resumen final:
============================================================
WORKFLOW EXECUTION SUMMARY
============================================================
Timestamp: 2026-03-13 14:35:42
Task: Agrega type hints a todas las funciones en src/api/...
Status: SUCCESS
Cost: $0.0156
Duration: 45200ms
Turns: 12
--- Hook Activity ---
Tool invocations: 28
Files edited: 6
Lint failures caught: 2
Commands blocked: 0
Paso 11: Analizar la Ejecución
Revisar los logs
cat .claude/logs/changes.log
Deberías ver un historial cronológico de todas las acciones:
2026-03-13 14:30:22 | ALLOWED | Bash | ls src/api/
2026-03-13 14:30:45 | EDITED | Edit | src/api/routes.py
2026-03-13 14:30:46 | LINT_PASS | src/api/routes.py
2026-03-13 14:31:02 | EDITED | Edit | src/api/schemas.py
2026-03-13 14:31:03 | LINT_FAIL | src/api/schemas.py
2026-03-13 14:31:15 | EDITED | Edit | src/api/schemas.py
2026-03-13 14:31:16 | LINT_PASS | src/api/schemas.py
En este ejemplo, Claude editó schemas.py, el lint falló, Claude corrigió, y el lint pasó en el segundo intento.
Revisar el reporte de sesión
ls .claude/reports/session-*.md
cat .claude/reports/session-*.md
Revisar el resumen del workflow
cat .claude/reports/workflow-*.txt
Checklist de éxito
✅ SessionStart se ejecutó al inicio
✅ PreToolUse validó al menos 1 comando Bash
✅ PostToolUse linteó al menos 1 archivo editado
✅ Al menos 1 lint failure fue corregido por Claude
✅ Stop generó un reporte de sesión
✅ El script Python generó un resumen con costo y métricas
✅ changes.log tiene el historial completo
✅ El pipeline funcionó de principio a fin sin intervención manual
Paso 12: Iteración — Extender el Pipeline
Extensión 1: Agregar auto-test después de lint
Modifica scripts/hooks/auto-lint.sh para que, después de pasar el lint, también corra tests relacionados:
# Agregar al final de auto-lint.sh, después del lint exitoso para Python:
if [ "$LINT_EXIT" -eq 0 ]; then
BASENAME=$(basename "$FILE" .py)
TEST_FILE="tests/test_${BASENAME}.py"
if [ -f "$TEST_FILE" ]; then
TEST_RESULT=$(python -m pytest "$TEST_FILE" -x --tb=line -q 2>&1)
if [ $? -ne 0 ]; then
echo "TEST FAILURE after editing $FILE:"
echo "$TEST_RESULT" | tail -10
echo "$TIMESTAMP | TEST_FAIL | $FILE" >> .claude/logs/changes.log
exit 1
fi
echo "$TIMESTAMP | TEST_PASS | $FILE" >> .claude/logs/changes.log
fi
fi
Extensión 2: Notificación al terminar
Agrega al final de scripts/hooks/session-summary.sh:
if command -v osascript &> /dev/null; then
osascript -e "display notification \"$CHANGED files changed, $LINT_FAILS lint issues caught\" with title \"Claude Workflow Complete\""
fi
Extensión 3: Múltiples tareas en secuencia
Modifica el script Python para ejecutar múltiples tareas:
tasks = [
("Agrega type hints a src/api/", ["Read", "Write", "Edit", "Grep", "Glob"]),
("Corre los tests y reporta", ["Read", "Grep", "Glob", "Bash"]),
("Genera documentación para src/api/", ["Read", "Grep", "Glob"]),
]
total_cost = 0
for task, tools in tasks:
result = run_claude(task, tools)
if not result.get("is_error"):
total_cost += result.get("cost_usd", 0)
print(f"Task done: ${result.get('cost_usd', 0):.4f}")
print(f"\nTotal pipeline cost: ${total_cost:.4f}")
Errores Comunes y Soluciones
Error 1: "Permission denied: ./scripts/hooks/session-setup.sh"
Síntoma: El hook falla inmediatamente con un error de permisos.
Causa: Los scripts no tienen permisos de ejecución.
Solución:
chmod +x scripts/hooks/*.sh
Error 2: "jq: command not found"
Síntoma: Los hooks que parsean JSON fallan silenciosamente. PreToolUse y PostToolUse no funcionan correctamente.
Causa: jq no está instalado.
Solución:
# macOS
brew install jq
# Ubuntu/Debian
sudo apt-get install jq
# Verificar
jq --version
Error 3: "El auto-lint crea un loop infinito"
Síntoma: Claude edita un archivo, el lint falla, Claude corrige, el lint falla de nuevo, en un ciclo sin fin.
Causa: El linter detecta un error que Claude no sabe corregir, o el auto-format cambia algo que el linter rechaza.
Solución: Agrega un contador de intentos en auto-lint.sh:
ATTEMPT_FILE="/tmp/lint-attempt-$(echo "$FILE" | md5sum | cut -c1-8)"
ATTEMPTS=0
if [ -f "$ATTEMPT_FILE" ]; then
ATTEMPTS=$(cat "$ATTEMPT_FILE")
fi
if [ "$ATTEMPTS" -ge 3 ]; then
echo "WARNING: Skipping lint after 3 failures for $FILE"
rm -f "$ATTEMPT_FILE"
exit 0
fi
echo $((ATTEMPTS + 1)) > "$ATTEMPT_FILE"
Error 4: "El reporte de sesión está vacío"
Síntoma: Stop hook genera un archivo .md pero sin contenido útil.
Causa: Los logs de cambios no se crearon porque SessionStart no ejecutó o porque los hooks PostToolUse no loguearon.
Solución: Verifica que:
- SessionStart creó
changes.log:ls -la .claude/logs/changes.log - PostToolUse escribe en el log: ejecuta manualmente un test del hook
- El path en
session-summary.shcoincide con el directorio actual
Error 5: "Claude CLI not found en el script Python"
Síntoma: El script Python reporta FileNotFoundError o claude: command not found.
Causa: claude no está en el PATH cuando Python ejecuta subprocess.
Solución: Usa la ruta completa:
import shutil
claude_path = shutil.which("claude")
if not claude_path:
print("Error: claude not found in PATH")
sys.exit(1)
cmd = [claude_path, "-p", prompt, ...]
Error 6: "Hooks no se ejecutan en modo headless"
Síntoma: El script SDK funciona pero ningún hook se dispara.
Causa: Los hooks necesitan que settings.json esté en el directorio correcto y que Claude Code los reconozca.
Solución: Verifica que .claude/settings.json existe en el root del proyecto y que estás ejecutando Claude desde ese directorio. El cwd en subprocess debe apuntar al proyecto:
result = subprocess.run(cmd, cwd=str(PROJECT_ROOT), ...)
Error 7: "El script tarda demasiado"
Síntoma: El pipeline tarda más de 10 minutos para tareas simples.
Causa: Los hooks PostToolUse (lint, test) agregan overhead en cada herramienta. Si Claude hace 50 ediciones, son 50 invocaciones del linter.
Solución: Limita el lint a archivos que realmente cambiaron:
LAST_LINT_HASH=$(md5sum "$FILE" 2>/dev/null | cut -d' ' -f1)
HASH_FILE="/tmp/lint-hash-$(echo "$FILE" | md5sum | cut -c1-8)"
if [ -f "$HASH_FILE" ] && [ "$(cat "$HASH_FILE")" = "$LAST_LINT_HASH" ]; then
exit 0
fi
echo "$LAST_LINT_HASH" > "$HASH_FILE"
Error 8: "Los logs de subagents están vacíos"
Síntoma: No hay actividad en subagents.log aunque Claude usó subagents.
Causa: SubagentStop puede no estar soportado en tu versión de Claude Code, o Claude no usó subagents para la tarea (las usó directamente).
Solución: Verifica si Claude realmente delegó a subagents. Para tareas simples, Claude trabaja directamente sin crear subagents. El log de subagents solo se llena si Claude explícitamente delega a un subagent definido en .claude/agents/.
Conexión con el Siguiente Módulo
Has construido un pipeline automatizado completo. Los hooks detectan cada evento del ciclo de vida de Claude Code, y el SDK permite arrancarlo todo desde un script. Pero hay un detalle: el script Python tiene que correr en tu máquina. Estás ahí, ejecutando python scripts/automated-workflow.py. Ya no escribes los prompts manualmente, pero sigues estando presente.
El Módulo 7: Remote Control y CLAUDE.md para Equipos elimina esa última dependencia. Remote control permite ejecutar y monitorear Claude Code desde cualquier dispositivo — tu teléfono, otro computador, un servidor. Y CLAUDE.md para equipos establece las reglas compartidas que hacen que la automatización sea consistente entre todos los miembros del equipo.
El SDK que aprendiste aquí es la base del remote control: si puedes invocar Claude Code desde un script Python local, puedes invocar Claude Code desde un servidor remoto. Los hooks que configuraste son las reglas que se comparten via CLAUDE.md. La automatización local que construiste se convierte en automatización distribuida.
Resumen
- Construiste un pipeline automatizado end-to-end con 5 hooks + 1 script SDK Python
- SessionStart configura el entorno automáticamente — dependencias, logs, verificaciones de git
- PreToolUse valida cada comando Bash contra patrones peligrosos — exit 2 bloquea, exit 1 advierte
- PostToolUse auto-lintea cada archivo editado — los errores de lint van a Claude para auto-corrección
- SubagentStop genera logs del ciclo de vida de cada subagent
- Stop produce un reporte de sesión con métricas: archivos editados, lint failures, comandos bloqueados
- El script Python SDK orquesta todo: invoca Claude en modo headless, recolecta logs de hooks, genera un resumen ejecutivo
- El pipeline funciona sin intervención humana — ejecutas un comando y recibes un reporte al final
- Los hooks y el SDK se complementan: hooks controlan desde dentro, SDK controla desde fuera
- Este pipeline es la base del remote control (Módulo 7) y del proyecto integrador (Módulo 8)
Recursos del Proyecto
- Claude Code Hooks (Anthropic Docs) — Documentación oficial de todos los eventos de hooks
- Claude Code CLI Reference — Flags
-p,--output-format,--allowedTools - Claude Code Settings — Configuración de hooks en settings.json
- jq Manual — Parsing de JSON en scripts bash
- Python subprocess — Referencia de subprocess para invocar Claude
- Claude Code Best Practices — Buenas prácticas de automatización y hooks
Siguiente módulo: El Módulo 7 (Remote Control y CLAUDE.md para Equipos) extiende lo que construiste aquí. El SDK headless que usas localmente se convierte en un servicio que puedes controlar desde cualquier dispositivo. Los hooks que configuraste se empaquetan en CLAUDE.md como estándares de equipo. La automatización pasa de ser personal a ser organizacional — tu pipeline funciona igual para todo el equipo, desde cualquier lugar.