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.json con 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

RequisitoMínimoIdeal
Directorio src/Con 3+ archivosCon módulos separados
Archivos Python o TypeScriptAl menos 510+
Linter instaladoruff o eslintAmbos
Git inicializadoSíCon 3+ commits
Python 3.8+InstaladoCon venv
jqInstalado—

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-*.md aparece 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:

  1. SessionStart creó changes.log: ls -la .claude/logs/changes.log
  2. PostToolUse escribe en el log: ejecuta manualmente un test del hook
  3. El path en session-summary.sh coincide 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

  1. Claude Code Hooks (Anthropic Docs) — Documentación oficial de todos los eventos de hooks
  2. Claude Code CLI Reference — Flags -p, --output-format, --allowedTools
  3. Claude Code Settings — Configuración de hooks en settings.json
  4. jq Manual — Parsing de JSON en scripts bash
  5. Python subprocess — Referencia de subprocess para invocar Claude
  6. 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.