Módulo 7: Remote Control y CLAUDE.md para Equipos

3. Approval Flows Remotos — Gates de Aprobación para Operaciones Sensibles

3. Approval Flows Remotos — Gates de Aprobación para Operaciones Sensibles

Descripción

Monitorear una sesión de Claude Code desde tu teléfono es útil, pero el verdadero poder del remote control está en los approval flows: definir qué operaciones necesitan tu aprobación explícita antes de ejecutarse. Un git push --force, un DROP TABLE, un npm publish — operaciones que si salen mal son difíciles o imposibles de revertir. Sin approval flows, Claude Code puede ejecutar estas operaciones si tiene los permisos. Con approval flows, la operación se pausa, te envía una solicitud de aprobación, y espera tu respuesta antes de continuar.

Esta cápsula cubre el diseño completo de approval flows: cómo definir qué operaciones necesitan aprobación, cómo configurar gates con hooks PermissionRequest, cómo aprobar desde tu teléfono o desde otro terminal, qué pasa cuando nadie aprueba a tiempo (timeout policies), y los tradeoffs entre auto-aprobar todo vs. requerir aprobación manual para cada operación.

Al terminar, tendrás un sistema de aprobación que protege tu proyecto de operaciones destructivas sin ralentizar el trabajo normal del agente.

Estado experimental (marzo 2026): Los approval flows remotos dependen de la feature de remote control, que está en desarrollo activo. Los patrones de hooks PermissionRequest descritos aquí son funcionales. La integración con aprobación desde dispositivos móviles está basada en la documentación disponible a marzo 2026. Verifica disponibilidad actual en docs.anthropic.com.


El Problema: Operaciones Sin Vuelta Atrás

Por qué necesitas approval gates

Claude Code con permisos amplios puede hacer casi cualquier cosa. Eso es una ventaja cuando quieres productividad — y un riesgo cuando la operación es destructiva:

Operaciones seguras (auto-aprobables):
  ✅ Leer archivos
  ✅ Escribir código nuevo
  ✅ Ejecutar tests
  ✅ Generar documentación
  ✅ git add, git commit

Operaciones riesgosas (necesitan aprobación):
  ⚠️ git push --force
  ⚠️ DROP TABLE / DROP DATABASE
  ⚠️ npm publish
  ⚠️ rm -rf en directorios importantes
  ⚠️ Modificar archivos de configuración de producción
  ⚠️ Ejecutar migraciones de base de datos
  ⚠️ Cambiar permisos de archivos del sistema

Sin approval flows, tienes dos opciones extremas:

  1. Dar todos los permisos → Productivo pero peligroso
  2. Restringir todo → Seguro pero ineficiente (Claude pregunta por cada operación)

Los approval flows son el punto medio: operaciones normales pasan automáticamente, operaciones sensibles se pausan hasta que tú las apruebas.


Hooks PermissionRequest: La Mecánica

Cómo funciona PermissionRequest

El hook PermissionRequest se dispara cuando Claude Code necesita un permiso que no tiene pre-aprobado. Tu hook decide:

Exit CodeSignificadoClaude Code hace...
0AprobadoEjecuta la operación
1Error (decidir)Reporta a Claude, que decide si insistir o buscar alternativa
2RechazadoLa operación se cancela

Input JSON del PermissionRequest

Cuando el hook se dispara, recibe información sobre qué operación necesita permiso:

{
  "hook_event_name": "PermissionRequest",
  "tool_name": "Bash",
  "tool_input": {
    "command": "git push --force origin main"
  },
  "permission_type": "tool_execution",
  "session_id": "abc123"
}

Tu script inspecciona este JSON y decide si aprobar, rechazar, o escalar a aprobación remota.


Diseñando Approval Gates

Nivel 1: Auto-aprobar operaciones seguras

El primer paso es clasificar operaciones por riesgo:

scripts/hooks/permission-gate.sh:

#!/bin/bash

INPUT=$(cat -)

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

AUTO_APPROVE_TOOLS=(
    "Read"
    "Grep"
    "Glob"
)

for tool in "${AUTO_APPROVE_TOOLS[@]}"; do
    if [ "$TOOL_NAME" = "$tool" ]; then
        exit 0
    fi
done

if [ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ]; then
    if echo "$FILE_PATH" | grep -qE "^(src/|tests/|docs/)"; then
        exit 0
    fi
fi

if [ "$TOOL_NAME" = "Bash" ]; then
    SAFE_PATTERNS=(
        "^ls "
        "^cat "
        "^echo "
        "^python -m pytest"
        "^npm test"
        "^npm run lint"
        "^git status"
        "^git log"
        "^git diff"
    )

    for pattern in "${SAFE_PATTERNS[@]}"; do
        if echo "$COMMAND" | grep -qE "$pattern"; then
            exit 0
        fi
    done
fi

echo "REQUIRES_APPROVAL: $TOOL_NAME"
echo "Details: ${COMMAND:-$FILE_PATH}"
exit 1

Nivel 2: Bloquear operaciones destructivas

Algunas operaciones nunca deberían ejecutarse sin revisión humana:

scripts/hooks/block-destructive.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=(
    "git push --force"
    "git push.*-f "
    "git reset --hard"
    "DROP DATABASE"
    "DROP TABLE"
    "TRUNCATE TABLE"
    "npm publish"
    "rm -rf /"
    "rm -rf ~"
    "rm -rf \."
    "chmod -R 777"
    "curl.*| sh"
    "wget.*| bash"
)

for pattern in "${BLOCKED_PATTERNS[@]}"; do
    if echo "$COMMAND" | grep -qiE "$pattern"; then
        echo "BLOCKED: Operación destructiva detectada"
        echo "Pattern: $pattern"
        echo "Comando: $COMMAND"
        echo ""
        echo "Esta operación requiere aprobación manual."
        echo "Usa remote control para aprobar si es intencional."
        exit 2
    fi
done

exit 0

Nivel 3: Escalamiento a aprobación remota

Para operaciones que no son destructivas pero sí sensibles, el hook puede escalar a aprobación remota escribiendo la solicitud a un archivo que el remote control monitorea:

scripts/hooks/remote-approval.sh:

#!/bin/bash

INPUT=$(cat -)

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

SENSITIVE_COMMANDS=(
    "git push"
    "npm install.*-g"
    "pip install"
    "alembic upgrade"
    "alembic downgrade"
    "docker"
    "kubectl"
)

NEEDS_APPROVAL=false
for pattern in "${SENSITIVE_COMMANDS[@]}"; do
    if echo "$COMMAND" | grep -qiE "$pattern"; then
        NEEDS_APPROVAL=true
        break
    fi
done

SENSITIVE_FILES=(
    "package.json"
    "requirements.txt"
    "Dockerfile"
    "docker-compose"
    ".github/workflows"
    "alembic/versions"
)

for pattern in "${SENSITIVE_FILES[@]}"; do
    if echo "$FILE_PATH" | grep -qi "$pattern"; then
        NEEDS_APPROVAL=true
        break
    fi
done

if [ "$NEEDS_APPROVAL" = true ]; then
    APPROVAL_DIR=".claude/approvals"
    mkdir -p "$APPROVAL_DIR"

    APPROVAL_ID="approval-$(date +%s)"
    APPROVAL_FILE="$APPROVAL_DIR/$APPROVAL_ID.json"

    cat > "$APPROVAL_FILE" << EOF
{
  "id": "$APPROVAL_ID",
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "tool": "$TOOL_NAME",
  "command": "$COMMAND",
  "file": "$FILE_PATH",
  "status": "pending",
  "timeout_seconds": 300
}
EOF

    echo "APPROVAL_REQUIRED: $APPROVAL_ID"
    echo "Operation: $TOOL_NAME ${COMMAND:-$FILE_PATH}"
    echo "Approve via remote control or create: $APPROVAL_DIR/$APPROVAL_ID.approved"

    TIMEOUT=300
    ELAPSED=0
    INTERVAL=5

    while [ $ELAPSED -lt $TIMEOUT ]; do
        if [ -f "$APPROVAL_DIR/$APPROVAL_ID.approved" ]; then
            echo "APPROVED by remote user"
            rm -f "$APPROVAL_FILE" "$APPROVAL_DIR/$APPROVAL_ID.approved"
            exit 0
        fi

        if [ -f "$APPROVAL_DIR/$APPROVAL_ID.rejected" ]; then
            echo "REJECTED by remote user"
            rm -f "$APPROVAL_FILE" "$APPROVAL_DIR/$APPROVAL_ID.rejected"
            exit 2
        fi

        sleep $INTERVAL
        ELAPSED=$((ELAPSED + INTERVAL))
    done

    echo "TIMEOUT: No approval received in ${TIMEOUT}s"
    rm -f "$APPROVAL_FILE"
    exit 2
fi

exit 0

Aprobación desde Dispositivos Móviles

El flujo completo

1. Claude Code intenta ejecutar: git push origin feature-branch
   ↓
2. Hook PermissionRequest detecta "git push" → necesita aprobación
   ↓
3. Hook escribe solicitud a .claude/approvals/approval-xxx.json
   ↓
4. Remote control detecta solicitud pendiente
   ↓
5. Tu teléfono recibe notificación: "Aprobar git push?"
   ↓
6a. Apruebas → se crea approval-xxx.approved → hook retorna exit 0
6b. Rechazas → se crea approval-xxx.rejected → hook retorna exit 2
6c. No respondes → timeout → hook retorna exit 2

Aprobación via CLI remoto

Desde otro terminal o dispositivo con acceso CLI:

claude remote approvals --session abc123 --token rc_tk_xxxxx
Pending approvals:
  1. [approval-1710345600] git push origin feature-branch (4m 32s remaining)

Approve: claude remote approve --id approval-1710345600
Reject:  claude remote reject --id approval-1710345600
claude remote approve --session abc123 --token rc_tk_xxxxx --id approval-1710345600

Aprobación via SDK

import subprocess
import json


def check_pending_approvals(session_id, token):
    result = subprocess.run(
        ["claude", "remote", "approvals",
         "--session", session_id,
         "--token", token,
         "--output-format", "json"],
        capture_output=True, text=True
    )
    if result.returncode != 0:
        return []
    data = json.loads(result.stdout)
    return data.get("pending", [])


def approve_operation(session_id, token, approval_id):
    result = subprocess.run(
        ["claude", "remote", "approve",
         "--session", session_id,
         "--token", token,
         "--id", approval_id],
        capture_output=True, text=True
    )
    return result.returncode == 0


def reject_operation(session_id, token, approval_id):
    result = subprocess.run(
        ["claude", "remote", "reject",
         "--session", session_id,
         "--token", token,
         "--id", approval_id],
        capture_output=True, text=True
    )
    return result.returncode == 0


session = "abc123"
token = "rc_tk_xxxxx"

pending = check_pending_approvals(session, token)
for approval in pending:
    print(f"Pendiente: {approval['operation']}")
    print(f"  Comando: {approval.get('command', 'N/A')}")
    print(f"  Tiempo restante: {approval.get('remaining_seconds', '?')}s")

    response = input("  ¿Aprobar? (s/n): ")
    if response.lower() == "s":
        approve_operation(session, token, approval["id"])
        print("  → Aprobado")
    else:
        reject_operation(session, token, approval["id"])
        print("  → Rechazado")

Timeout Policies

Qué pasa cuando nadie aprueba

Si lanzas un Agent Team, te vas, y una operación necesita aprobación que nunca llega, el sistema necesita una política clara:

PolíticaComportamientoCuándo usar
Timeout → RejectSi nadie aprueba en X minutos, rechaza la operaciónDefault seguro. Para operaciones destructivas
Timeout → ApproveSi nadie aprueba en X minutos, aprueba automáticamenteOperaciones de bajo riesgo que pueden esperar
Timeout → FallbackSi nadie aprueba, usa una alternativa seguraCuando hay opciones menos riesgosas
No timeoutEspera indefinidamenteOperaciones críticas que DEBEN ser revisadas

Configuración de timeouts

scripts/hooks/timed-approval.sh:

#!/bin/bash

INPUT=$(cat -)

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

get_timeout() {
    local cmd="$1"

    if echo "$cmd" | grep -qiE "(DROP|TRUNCATE|--force|publish)"; then
        echo 600  # 10 minutos para operaciones destructivas
    elif echo "$cmd" | grep -qiE "(git push|deploy|migrate)"; then
        echo 300  # 5 minutos para operaciones sensibles
    else
        echo 120  # 2 minutos para el resto
    fi
}

get_timeout_action() {
    local cmd="$1"

    if echo "$cmd" | grep -qiE "(DROP|TRUNCATE|--force)"; then
        echo "reject"
    elif echo "$cmd" | grep -qiE "(git push|migrate)"; then
        echo "reject"
    else
        echo "approve"
    fi
}

TIMEOUT=$(get_timeout "$COMMAND")
ACTION=$(get_timeout_action "$COMMAND")

APPROVAL_DIR=".claude/approvals"
mkdir -p "$APPROVAL_DIR"
APPROVAL_ID="approval-$(date +%s)-$$"
APPROVAL_FILE="$APPROVAL_DIR/$APPROVAL_ID.json"

cat > "$APPROVAL_FILE" << EOF
{
  "id": "$APPROVAL_ID",
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "tool": "$TOOL_NAME",
  "command": "$COMMAND",
  "status": "pending",
  "timeout_seconds": $TIMEOUT,
  "timeout_action": "$ACTION"
}
EOF

echo "APPROVAL NEEDED: $TOOL_NAME"
echo "Command: $COMMAND"
echo "Timeout: ${TIMEOUT}s (action: $ACTION)"

ELAPSED=0
while [ $ELAPSED -lt $TIMEOUT ]; do
    if [ -f "$APPROVAL_DIR/$APPROVAL_ID.approved" ]; then
        echo "APPROVED"
        rm -f "$APPROVAL_FILE" "$APPROVAL_DIR/$APPROVAL_ID.approved"
        exit 0
    fi

    if [ -f "$APPROVAL_DIR/$APPROVAL_ID.rejected" ]; then
        echo "REJECTED"
        rm -f "$APPROVAL_FILE" "$APPROVAL_DIR/$APPROVAL_ID.rejected"
        exit 2
    fi

    sleep 5
    ELAPSED=$((ELAPSED + 5))
done

rm -f "$APPROVAL_FILE"

if [ "$ACTION" = "approve" ]; then
    echo "TIMEOUT → Auto-approved (low risk)"
    exit 0
else
    echo "TIMEOUT → Rejected (high risk, no approval received)"
    exit 2
fi

Configuración Completa en settings.json

Un setup de approval flows production-ready:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/hooks/block-destructive.sh"
          }
        ]
      }
    ],
    "PermissionRequest": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/hooks/permission-gate.sh"
          }
        ]
      }
    ]
  },
  "remote": {
    "enabled": true,
    "require_auth": true,
    "allowed_operations": ["monitor", "approve", "reject"],
    "log_remote_actions": true
  }
}

Con esta configuración:

  • PreToolUse bloquea operaciones destructivas inmediatamente (exit 2)
  • PermissionRequest clasifica las operaciones restantes en auto-aprobables vs. requieren aprobación
  • Remote control está habilitado para aprobar/rechazar desde otro dispositivo

Production Safety: Patrones Avanzados

Patrón 1: Approval por entorno

Diferentes niveles de aprobación según el entorno:

#!/bin/bash

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

ENV=$(echo "$COMMAND" | grep -oE "(production|staging|development)" | head -1)
ENV=${ENV:-development}

case "$ENV" in
    production)
        echo "BLOCKED: Operaciones de producción requieren aprobación manual"
        echo "Usa el portal de deploys para operaciones en producción"
        exit 2
        ;;
    staging)
        echo "APPROVAL_REQUIRED: Operación en staging"
        # Esperar aprobación con timeout de 5 minutos
        exit 1
        ;;
    development)
        exit 0
        ;;
esac

Patrón 2: Doble aprobación para operaciones críticas

Algunas operaciones necesitan aprobación de dos personas:

#!/bin/bash

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

is_critical() {
    echo "$COMMAND" | grep -qiE "(DROP DATABASE|npm publish|deploy.*production)"
}

if is_critical; then
    APPROVAL_DIR=".claude/approvals"
    APPROVAL_ID="critical-$(date +%s)"

    mkdir -p "$APPROVAL_DIR"
    echo "0" > "$APPROVAL_DIR/$APPROVAL_ID.count"

    cat > "$APPROVAL_DIR/$APPROVAL_ID.json" << EOF
{
  "id": "$APPROVAL_ID",
  "type": "critical",
  "requires": 2,
  "current": 0,
  "command": "$COMMAND",
  "approvers": []
}
EOF

    echo "CRITICAL OPERATION: Requiere 2 aprobaciones"
    echo "Command: $COMMAND"

    TIMEOUT=900  # 15 minutos
    ELAPSED=0

    while [ $ELAPSED -lt $TIMEOUT ]; do
        if [ -f "$APPROVAL_DIR/$APPROVAL_ID.count" ]; then
            COUNT=$(cat "$APPROVAL_DIR/$APPROVAL_ID.count")
            if [ "$COUNT" -ge 2 ]; then
                echo "APPROVED: 2 aprobaciones recibidas"
                rm -f "$APPROVAL_DIR/$APPROVAL_ID.json" "$APPROVAL_DIR/$APPROVAL_ID.count"
                exit 0
            fi
        fi

        if [ -f "$APPROVAL_DIR/$APPROVAL_ID.rejected" ]; then
            echo "REJECTED"
            rm -f "$APPROVAL_DIR/$APPROVAL_ID.json" "$APPROVAL_DIR/$APPROVAL_ID.count" "$APPROVAL_DIR/$APPROVAL_ID.rejected"
            exit 2
        fi

        sleep 5
        ELAPSED=$((ELAPSED + 5))
    done

    echo "TIMEOUT: No se alcanzaron 2 aprobaciones"
    rm -f "$APPROVAL_DIR/$APPROVAL_ID.json" "$APPROVAL_DIR/$APPROVAL_ID.count"
    exit 2
fi

exit 0

Patrón 3: Audit log de aprobaciones

Registrar todas las decisiones de aprobación para auditoría:

#!/bin/bash

AUDIT_LOG=".claude/logs/approval-audit.log"
mkdir -p "$(dirname "$AUDIT_LOG")"

log_audit() {
    local action=$1
    local tool=$2
    local command=$3
    local approver=${4:-"system"}
    local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

    echo "$timestamp | $action | $tool | $approver | $command" >> "$AUDIT_LOG"
}

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

if echo "$COMMAND" | grep -qiE "(git push|npm publish|deploy|migrate)"; then
    log_audit "REQUESTED" "$TOOL_NAME" "$COMMAND"

    # ... lógica de aprobación ...

    # Si se aprueba:
    log_audit "APPROVED" "$TOOL_NAME" "$COMMAND" "remote-user"
    exit 0

    # Si se rechaza:
    # log_audit "REJECTED" "$TOOL_NAME" "$COMMAND" "remote-user"
    # exit 2
fi

log_audit "AUTO_APPROVED" "$TOOL_NAME" "$COMMAND"
exit 0

Troubleshooting

"La aprobación nunca llega al hook"

Causa: El hook está esperando un archivo .approved pero el remote control usa un mecanismo diferente de notificación.

Solución: Verifica cómo tu versión de remote control comunica aprobaciones. Si usa archivos, confirma la ruta. Si usa API, ajusta el hook para consultar via curl:

APPROVED=$(curl -s "http://localhost:3500/approvals/$APPROVAL_ID/status" 2>/dev/null)
if [ "$APPROVED" = "approved" ]; then
    exit 0
fi

"El timeout es demasiado corto para operaciones nocturnas"

Causa: El default de 5 minutos no es suficiente si lanzas un batch job antes de dormir.

Solución: Configura timeouts más largos para batch jobs:

if [ -f ".claude/batch-mode" ]; then
    TIMEOUT=28800  # 8 horas para batch mode
else
    TIMEOUT=300    # 5 minutos para modo normal
fi

Antes de lanzar un batch:

touch .claude/batch-mode
# ... ejecutar batch ...
rm .claude/batch-mode

"El hook de aprobación bloquea Claude Code completamente"

Causa: El hook espera con sleep dentro de un loop, y el timeout es muy largo, haciendo que Claude Code se quede colgado.

Solución: Asegúrate de que el timeout total sea razonable. Para hooks PreToolUse, mantén timeouts cortos (< 5 minutos). Si necesitas tiempos más largos, usa PermissionRequest en lugar de PreToolUse, ya que está diseñado para esperas más largas.

"Las aprobaciones se pierden entre reinicios"

Causa: Los archivos de aprobación en .claude/approvals/ se crean pero el hook que los espera ya terminó (la sesión se reinició).

Solución: Agrega limpieza de aprobaciones obsoletas en el SessionStart hook:

find .claude/approvals/ -name "*.json" -mmin +60 -delete 2>/dev/null

"No sé qué operaciones deberían necesitar aprobación"

Causa: No tienes una clasificación clara de operaciones por riesgo.

Solución: Empieza conservador — requiere aprobación para todo excepto lectura. Después de una semana, revisa el audit log y relaja las operaciones que siempre se aprueban:

# Ver qué operaciones se aprueban siempre
grep "APPROVED" .claude/logs/approval-audit.log | \
    awk -F'|' '{print $3}' | sort | uniq -c | sort -rn

Comparación: Auto-Approve vs Manual Approval

AspectoAuto-Approve TodoManual ApprovalApproval Gates (híbrido)
VelocidadMáxima — sin interrupcionesMínima — cada operación esperaAlta — solo sensibles esperan
SeguridadBaja — operaciones destructivas pasanMáxima — todo se revisaAlta — clasificación por riesgo
SupervisiónNingunaConstante (debes estar ahí)Selectiva (remote control)
Ideal paraPrototyping, proyectos personalesProducción crítica, complianceEquipos, proyectos reales
RiesgoAlto — un error destruye trabajoBajo — pero alta fricciónControlado — balance riesgo/velocidad
Con remote controlNo necesarioImprescindibleComplemento perfecto
Fatiga del developerNingunaAlta (approval fatigue)Baja (solo apruebas lo importante)

Recomendación: Empieza con approval gates (híbrido). Auto-aprueba lectura y escritura en src/ y tests/. Requiere aprobación para git push, publicación, migraciones, y operaciones de sistema. Ajusta basándote en el audit log después de la primera semana.


Ejercicios

Ejercicio 1: Clasificador de riesgo básico (Fácil)

Crea un script que clasifique operaciones en tres niveles: safe (exit 0), moderate (exit 1 con warning), dangerous (exit 2 bloqueado). Clasifica al menos 5 operaciones en cada nivel.

Ver solución

scripts/hooks/risk-classifier.sh:

#!/bin/bash

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

# SAFE: operaciones de lectura y desarrollo normal
if [ "$TOOL_NAME" = "Read" ] || [ "$TOOL_NAME" = "Grep" ] || [ "$TOOL_NAME" = "Glob" ]; then
    exit 0
fi

if echo "$COMMAND" | grep -qE "^(ls|cat|echo|pwd|which|python -m pytest|npm test)"; then
    exit 0
fi

if echo "$FILE_PATH" | grep -qE "^(src/|tests/|docs/)"; then
    exit 0
fi

# DANGEROUS: operaciones destructivas e irreversibles
if echo "$COMMAND" | grep -qiE "(rm -rf|DROP|TRUNCATE|--force|publish|mkfs|dd if=)"; then
    echo "DANGEROUS: $COMMAND"
    exit 2
fi

if echo "$FILE_PATH" | grep -qiE "(\.env|credentials|secrets|\.ssh|\.aws)"; then
    echo "DANGEROUS: acceso a archivo sensible $FILE_PATH"
    exit 2
fi

# MODERATE: todo lo demás (git push, installs, configs)
echo "MODERATE: $TOOL_NAME ${COMMAND:-$FILE_PATH}"
exit 1

Ejercicio 2: Timeout configurable por archivo (Fácil)

Crea un archivo .claude/approval-config.json que defina timeouts por tipo de operación, y un script que lea esa configuración.

Ver solución

.claude/approval-config.json:

{
  "timeouts": {
    "git_push": 300,
    "npm_publish": 600,
    "database_migration": 600,
    "file_delete": 120,
    "default": 180
  },
  "timeout_action": {
    "git_push": "reject",
    "npm_publish": "reject",
    "database_migration": "reject",
    "file_delete": "reject",
    "default": "approve"
  }
}

scripts/hooks/configurable-timeout.sh:

#!/bin/bash

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

CONFIG_FILE=".claude/approval-config.json"
if [ ! -f "$CONFIG_FILE" ]; then
    exit 0
fi

get_operation_type() {
    local cmd="$1"
    if echo "$cmd" | grep -qiE "git push"; then echo "git_push"
    elif echo "$cmd" | grep -qiE "npm publish"; then echo "npm_publish"
    elif echo "$cmd" | grep -qiE "(alembic|migrate)"; then echo "database_migration"
    elif echo "$cmd" | grep -qiE "rm -r"; then echo "file_delete"
    else echo "default"
    fi
}

OP_TYPE=$(get_operation_type "$COMMAND")
TIMEOUT=$(jq -r ".timeouts.$OP_TYPE // .timeouts.default" "$CONFIG_FILE")
ACTION=$(jq -r ".timeout_action.$OP_TYPE // .timeout_action.default" "$CONFIG_FILE")

echo "Operation: $OP_TYPE"
echo "Timeout: ${TIMEOUT}s"
echo "Timeout action: $ACTION"

# ... lógica de espera de aprobación usando $TIMEOUT y $ACTION ...
exit 0

Ejercicio 3: Audit log con análisis (Medio)

Crea un sistema que: (1) registre cada decisión de aprobación en un log CSV, y (2) un script Python que analice el log y genere estadísticas: operaciones más aprobadas, más rechazadas, tiempo promedio de respuesta.

Ver solución

scripts/hooks/audit-logger.sh:

#!/bin/bash

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

AUDIT_CSV=".claude/logs/approval-audit.csv"
mkdir -p "$(dirname "$AUDIT_CSV")"

if [ ! -f "$AUDIT_CSV" ]; then
    echo "timestamp,action,tool,command,response_time_s" > "$AUDIT_CSV"
fi

START_TIME=$(date +%s)

# ... lógica de aprobación ...

END_TIME=$(date +%s)
RESPONSE_TIME=$((END_TIME - START_TIME))

echo "$(date -u +%Y-%m-%dT%H:%M:%SZ),APPROVED,$TOOL_NAME,$COMMAND,$RESPONSE_TIME" >> "$AUDIT_CSV"
exit 0

scripts/analyze-approvals.py:

#!/usr/bin/env python3
"""Analiza el audit log de aprobaciones."""

import csv
from collections import Counter
from pathlib import Path

AUDIT_FILE = Path(".claude/logs/approval-audit.csv")


def analyze():
    if not AUDIT_FILE.exists():
        print("No audit log found.")
        return

    rows = []
    with open(AUDIT_FILE) as f:
        reader = csv.DictReader(f)
        rows = list(reader)

    if not rows:
        print("Audit log empty.")
        return

    actions = Counter(r["action"] for r in rows)
    tools = Counter(r["tool"] for r in rows)

    times = [int(r["response_time_s"]) for r in rows
             if r["response_time_s"].isdigit()]
    avg_time = sum(times) / len(times) if times else 0

    print(f"Total decisions: {len(rows)}")
    print(f"\nBy action:")
    for action, count in actions.most_common():
        print(f"  {action}: {count}")

    print(f"\nBy tool:")
    for tool, count in tools.most_common(5):
        print(f"  {tool}: {count}")

    print(f"\nAvg response time: {avg_time:.1f}s")


if __name__ == "__main__":
    analyze()

Ejercicio 4: Approval flow con fallback (Medio)

Diseña un approval flow donde, si la operación original es rechazada, el hook sugiere una alternativa más segura. Por ejemplo: si git push --force es rechazado, sugiere git push --force-with-lease.

Ver solución

scripts/hooks/approval-with-fallback.sh:

#!/bin/bash

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

suggest_fallback() {
    local cmd="$1"

    if echo "$cmd" | grep -qE "git push --force"; then
        echo "Alternativa segura: $(echo "$cmd" | sed 's/--force/--force-with-lease/')"
    elif echo "$cmd" | grep -qE "rm -rf"; then
        echo "Alternativa segura: Mover a .trash/ en lugar de eliminar"
    elif echo "$cmd" | grep -qE "DROP TABLE"; then
        echo "Alternativa segura: Renombrar tabla con prefijo _deprecated_"
    elif echo "$cmd" | grep -qE "git reset --hard"; then
        echo "Alternativa segura: git stash para preservar cambios"
    else
        echo "No hay alternativa sugerida"
    fi
}

if echo "$COMMAND" | grep -qiE "(--force|rm -rf|DROP TABLE|reset --hard)"; then
    FALLBACK=$(suggest_fallback "$COMMAND")
    echo "OPERACIÓN RIESGOSA: $COMMAND"
    echo ""
    echo "$FALLBACK"
    echo ""
    echo "La operación original fue bloqueada."
    echo "Claude puede usar la alternativa segura sin aprobación."
    exit 1
fi

exit 0

Con exit 1, Claude recibe el mensaje del hook y puede decidir usar la alternativa sugerida. Es más flexible que exit 2 (bloqueo total) porque le da opciones al agente.

Ejercicio 5: Simulación de approval flow completo (Difícil)

Sin remote control real, simula un approval flow completo con tres scripts: (1) un hook que escribe solicitudes de aprobación a archivos, (2) un "servidor de aprobaciones" que lee esas solicitudes y las presenta al usuario, (3) un script de respuesta que crea los archivos .approved o .rejected.

Ver solución

Script 1 — Hook (scripts/hooks/sim-approval-hook.sh):

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

if echo "$COMMAND" | grep -qiE "(git push|npm publish|migrate)"; then
    APPROVAL_DIR=".claude/approvals"
    mkdir -p "$APPROVAL_DIR"
    ID="sim-$(date +%s)"
    echo "{\"id\":\"$ID\",\"command\":\"$COMMAND\",\"status\":\"pending\"}" > "$APPROVAL_DIR/$ID.json"
    echo "Waiting for approval: $ID"
    for i in $(seq 1 60); do
        if [ -f "$APPROVAL_DIR/$ID.approved" ]; then
            rm -f "$APPROVAL_DIR/$ID.json" "$APPROVAL_DIR/$ID.approved"
            exit 0
        fi
        if [ -f "$APPROVAL_DIR/$ID.rejected" ]; then
            rm -f "$APPROVAL_DIR/$ID.json" "$APPROVAL_DIR/$ID.rejected"
            exit 2
        fi
        sleep 1
    done
    rm -f "$APPROVAL_DIR/$ID.json"
    exit 2
fi
exit 0

Script 2 — Approval server (scripts/approval-server.sh):

#!/bin/bash
APPROVAL_DIR=".claude/approvals"
echo "Approval Server - Watching $APPROVAL_DIR"
while true; do
    for f in "$APPROVAL_DIR"/*.json 2>/dev/null; do
        [ -f "$f" ] || continue
        ID=$(jq -r '.id' "$f")
        CMD=$(jq -r '.command' "$f")
        STATUS=$(jq -r '.status' "$f")
        if [ "$STATUS" = "pending" ]; then
            echo ""
            echo "PENDING: $ID"
            echo "Command: $CMD"
            echo "→ Approve: touch $APPROVAL_DIR/$ID.approved"
            echo "→ Reject:  touch $APPROVAL_DIR/$ID.rejected"
        fi
    done
    sleep 2
done

Script 3 — Responder (scripts/respond-approval.sh):

#!/bin/bash
APPROVAL_DIR=".claude/approvals"
ID=$1
ACTION=$2

if [ -z "$ID" ] || [ -z "$ACTION" ]; then
    echo "Uso: ./respond-approval.sh <approval-id> <approve|reject>"
    exit 1
fi

if [ "$ACTION" = "approve" ]; then
    touch "$APPROVAL_DIR/$ID.approved"
    echo "Approved: $ID"
elif [ "$ACTION" = "reject" ]; then
    touch "$APPROVAL_DIR/$ID.rejected"
    echo "Rejected: $ID"
fi

Uso (3 terminales):

# Terminal 1: Claude Code con el hook
# Terminal 2: ./scripts/approval-server.sh
# Terminal 3: ./scripts/respond-approval.sh sim-1710345600 approve

Resumen

  • Approval flows definen qué operaciones de Claude Code necesitan aprobación humana antes de ejecutarse
  • Usan hooks PermissionRequest y PreToolUse con exit codes: 0 (aprobar), 1 (escalar/decidir), 2 (rechazar)
  • Las operaciones se clasifican en safe (auto-aprobar), sensitive (requiere aprobación), y destructive (bloquear)
  • La aprobación puede hacerse via CLI remoto, interfaz web, o SDK programático
  • Timeout policies definen qué pasa cuando nadie aprueba: reject (seguro), approve (bajo riesgo), o fallback (alternativa)
  • El patrón de escalamiento usa archivos: el hook escribe una solicitud .json, espera un .approved o .rejected
  • Audit logs registran todas las decisiones de aprobación para trazabilidad y análisis
  • Patrones avanzados incluyen aprobación por entorno, doble aprobación para operaciones críticas, y fallback con alternativas seguras
  • Estado experimental (marzo 2026): la integración completa con remote control móvil está en desarrollo activo

Recursos Adicionales

  1. Claude Code Hooks (Anthropic Docs) — Documentación de PermissionRequest y otros hooks
  2. Claude Code Settings — Configuración de hooks y permisos
  3. Claude Code Security — Modelo de seguridad y permisos
  4. Claude Code CLI Reference — Flags de permisos y aprobación
  5. Claude Code Best Practices — Buenas prácticas de seguridad en automatización
  6. Claude Code Overview — Contexto general del modelo de permisos
  7. OWASP Secure Coding — Principios de seguridad aplicables a approval flows
  8. Anthropic Safety — Framework de seguridad de Anthropic

Siguiente cápsula: En la cápsula 04 diseñarás CLAUDE.md como estándar de equipo — la estructura de secciones, la jerarquía de merge (global < usuario < proyecto < subdirectorio), el enforcement con hooks que validan compliance, y un template production-ready. Pasas de controlar operaciones individuales (approval flows) a establecer las reglas que todo el equipo sigue (CLAUDE.md como constitución).