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:
- Dar todos los permisos → Productivo pero peligroso
- 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 Code | Significado | Claude Code hace... |
|---|---|---|
0 | Aprobado | Ejecuta la operación |
1 | Error (decidir) | Reporta a Claude, que decide si insistir o buscar alternativa |
2 | Rechazado | La 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ítica | Comportamiento | Cuándo usar |
|---|---|---|
| Timeout → Reject | Si nadie aprueba en X minutos, rechaza la operación | Default seguro. Para operaciones destructivas |
| Timeout → Approve | Si nadie aprueba en X minutos, aprueba automáticamente | Operaciones de bajo riesgo que pueden esperar |
| Timeout → Fallback | Si nadie aprueba, usa una alternativa segura | Cuando hay opciones menos riesgosas |
| No timeout | Espera indefinidamente | Operaciones 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
| Aspecto | Auto-Approve Todo | Manual Approval | Approval Gates (híbrido) |
|---|---|---|---|
| Velocidad | Máxima — sin interrupciones | Mínima — cada operación espera | Alta — solo sensibles esperan |
| Seguridad | Baja — operaciones destructivas pasan | Máxima — todo se revisa | Alta — clasificación por riesgo |
| Supervisión | Ninguna | Constante (debes estar ahí) | Selectiva (remote control) |
| Ideal para | Prototyping, proyectos personales | Producción crítica, compliance | Equipos, proyectos reales |
| Riesgo | Alto — un error destruye trabajo | Bajo — pero alta fricción | Controlado — balance riesgo/velocidad |
| Con remote control | No necesario | Imprescindible | Complemento perfecto |
| Fatiga del developer | Ninguna | Alta (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.approvedo.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
- Claude Code Hooks (Anthropic Docs) — Documentación de PermissionRequest y otros hooks
- Claude Code Settings — Configuración de hooks y permisos
- Claude Code Security — Modelo de seguridad y permisos
- Claude Code CLI Reference — Flags de permisos y aprobación
- Claude Code Best Practices — Buenas prácticas de seguridad en automatización
- Claude Code Overview — Contexto general del modelo de permisos
- OWASP Secure Coding — Principios de seguridad aplicables a approval flows
- 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).