En esta guía te muestro los guardrails de seguridad en Claude Code con código funcional, no con conceptos.
La escribí pensando en quien ya entendió que los guardrails hacen falta, pero se quedó sin saber qué escribir ni cómo. Toco 4 temas: el diseño de permissions en settings.json, el bloqueo de comandos peligrosos con el hook PreToolUse, la implementación de puertas de confirmación y la prevención de fugas de información confidencial. En cada sección te dejo ejemplos que puedes copiar y pegar.
La seguridad en Claude Code se monta con guardrails: reglas
allow/deny/askensettings.json, hooks que interceptan los comandos antes de ejecutarlos y puertas de confirmación que devuelven el control al humano antes de una acción irreversible.
Para quién es esto
- Para quienes ya metieron Claude Code en su trabajo o en proyectos personales y quieren montar un sistema que funcione de forma segura.
- Para quienes sienten inquietud de dejarle al agente operaciones irreversibles (eliminar archivos, envíos externos, commits).
- Para quienes, desde un rol de seguridad, buscan ejemplos reales de control de riesgos en el desarrollo con IA.
Entorno de ejecución y requisitos previos
- Claude Code (basado en las especificaciones vigentes a partir de junio de 2026).
- macOS / Linux (los scripts asumen el uso de bash).
- Una estructura donde la configuración global vive bajo
~/.claude/y la del proyecto bajo.claude/directamente en el repositorio.
El código de ejemplo está para explicar cómo funciona. En la operación real, personalízalo según tu entorno y tu diseño de permisos. Sobre todo con los scripts de hook, verifica de antemano el comportamiento de exit 2 (puede cambiar según la versión de Claude Code).
Por qué se necesitan guardrails
Claude Code trabaja combinando de forma autónoma la lectura/escritura de archivos, la ejecución de Bash y las llamadas a API externas. Esto dispara la velocidad de desarrollo, pero también abre la puerta a que haga algo que no debía sin que intervenga la revisión humana. Los accidentes más comunes se resumen en tres tipos:
- Ejecución de operaciones destructivas — eliminar directorios enteros con
rm -rf, unUPDATEdirecto a la base de datos de producción, etc. - Envíos no intencionados al exterior — commits de salidas que incluyen API keys o tokens de autenticación, envíos a servicios externos.
- Toma excesiva de permisos — acceso a archivos que excede el mínimo necesario u operaciones de red no intencionadas.
Todos son accidentes que no se pueden deshacer una vez hechos, la misma clase de riesgo que el desarrollo de software seguro previene por diseño. El objetivo de los guardrails no es limitar a la IA, sino crear un espacio donde el humano pueda decidir antes de una operación irreversible.
1. Diseñar los permisos mínimos con permissions en settings.json
El comportamiento de Claude Code se controla con ~/.claude/settings.json (global) y .claude/settings.json (proyecto). Usando allow y deny dentro de permissions, diseñas explícitamente los recursos que el agente puede tocar.
Estructura básica
{
"permissions": {
"allow": [],
"deny": []
}
}
Combinas dos enfoques: allow preaprueba las reglas que listas —Claude Code las ejecuta sin pedirte confirmación— y deny bloquea patrones concretos. Lo que no cae en ninguna lista no queda rechazado de entrada: pasa por el flujo normal de permisos, que puede pedirte confirmación. Y si quieres que una operación sensible pida confirmación en lugar de permitirla o bloquearla, tienes las reglas ask.
Ejemplo de configuración global
Primero declaro en la configuración global lo que no quiero dejarle hacer bajo ningún concepto. La del proyecto se combina con esta capa global, no la reemplaza: un deny global no se puede pisar con un allow del proyecto, porque deny se evalúa primero.
{
"permissions": {
"deny": [
"Bash(rm -rf *)",
"Bash(sudo rm *)",
"Bash(git push --force *)",
"Bash(git push -f *)",
"Bash(chmod 777 *)",
"Bash(curl * | bash)",
"Bash(wget * | bash)",
"Bash(eval *)"
]
}
}
Aquí listo en deny los patrones de comandos que no quiero que se ejecuten. Los objetivos principales son los del tipo rm -rf, el push forzado, la apertura total de permisos y la ejecución directa de scripts como curl | bash.
Ejemplo de configuración del proyecto
La configuración específica del proyecto se escribe en .claude/settings.json, directamente bajo el repositorio. Además del deny global, aquí delimito qué permito en este proyecto.
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(npx *)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git add *)",
"Bash(git commit *)",
"Bash(git log *)",
"Bash(git checkout *)",
"Bash(git branch *)"
],
"deny": [
"Bash(git push *)",
"WebFetch(domain:internal.example.com)",
"WebFetch(domain:staging.example.com)"
]
}
}
El punto clave es que hago un deny explícito a git push a nivel de proyecto. Trazo una frontera: le dejo a su cargo hasta el commit, pero el push lo hace un humano después de verificarlo. Entre el commit y el push está la frontera natural para parar: la misma que trazas cuando dominas los comandos básicos de Git.
Enfoque de diseño
Al diseñar los permissions, conviene preguntarse uno por uno: “¿Me causaría problemas si esta operación se inicia desde Claude Code?”.
| Tipo de operación | Criterio de decisión para allow / deny |
|---|---|
De lectura (git log, cat, find) | Básicamente allow |
De escritura local (git add, npm install) | allow por proyecto |
De envío externo (git push, curl POST) | Por principio deny, ejecutar manualmente |
De eliminación o sobrescritura (rm, git reset --hard) | Por principio deny |
De cambio de permisos de ejecución (chmod, chown) | Por principio deny |
2. Bloquear comandos peligrosos con el hook PreToolUse
El deny de settings.json se aplica desde el sistema de permisos de Claude Code. Los hooks te permiten añadir una validación externa más fina antes de que una herramienta se ejecute: no son una capa inferior del shell, sino un comando tuyo que recibe el contexto en JSON en un evento del ciclo de vida. PreToolUse se dispara justo antes de que Claude llame a una herramienta. Si el script devuelve exit 2, Claude Code bloquea esa llamada.
Nota de vigencia:
exit 2sigue siendo válido y es la vía más simple para bloquear. Además, hoy Claude Code admite un método más estructurado: salir conexit 0y devolver por stdout un JSON conhookSpecificOutput.permissionDecision: "deny"(y un motivo), que permite decisiones más ricas (allow,ask,deny). En esta guía usoexit 2para bloquear por ser directo, y más abajo uso la salida JSON con"ask"cuando lo que quiero es pedir confirmación.
Configuración del hook
Especifica la ruta del script del hook en .claude/settings.json.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/guard-dangerous-bash.sh"
}
]
}
]
}
}
Ejemplo de script para bloquear comandos peligrosos
#!/usr/bin/env bash
# PreToolUse hook: Bloquea comandos Bash peligrosos
# Si devuelve exit 2, Claude Code cancela la llamada a la herramienta
# Recibe JSON desde stdin
INPUT=$(cat)
# Extrae tool_input.command
COMMAND=$(echo "$INPUT" | python3 -c "
import sys, json
data = json.load(sys.stdin)
print(data.get('tool_input', {}).get('command', ''))
" 2>/dev/null)
if [ -z "$COMMAND" ]; then
exit 0
fi
# Patrones prohibidos (expresiones regulares)
DANGEROUS_PATTERNS=(
'rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*f'
'rm[[:space:]]+-[a-zA-Z]*f[a-zA-Z]*r'
'sudo[[:space:]]+rm'
':(){:|:&};:'
'mkfs\.'
'dd[[:space:]]+if='
'>[[:space:]]*/dev/sd'
'curl[^|]*\|[[:space:]]*(ba)?sh'
'wget[^|]*\|[[:space:]]*(ba)?sh'
'chmod[[:space:]]+777'
'git[[:space:]]+push[[:space:]]+(-f|--force)'
)
for PATTERN in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qE "$PATTERN"; then
# Los mensajes a stderr se muestran en los logs de Claude Code
echo "Guard: Se bloqueó la ejecución porque se detectó un patrón de comando peligroso: $COMMAND" >&2
echo "Guard: Patrón: $PATTERN" >&2
exit 2
fi
done
exit 0
El script recibe el JSON desde stdin y hace coincidir la cadena del comando con los patrones. Si coincide con un patrón prohibido, cancela con exit 2 y escribe el motivo en stderr. Como el contenido de stderr se muestra en la salida de Claude Code, puedes verificar por qué se detuvo.
Comprueba que el guard funciona
Antes de confiar en el hook, pruébalo a mano: pásale por stdin el mismo JSON que le enviaría Claude Code y mira el código de salida.
# Debe bloquear un comando peligroso (espera exit 2)
echo '{"tool_input":{"command":"rm -rf /"}}' | bash .claude/hooks/guard-dangerous-bash.sh
echo "exit: $?" # 2 = bloqueado
# Debe dejar pasar un comando inofensivo (espera exit 0)
echo '{"tool_input":{"command":"git status"}}' | bash .claude/hooks/guard-dangerous-bash.sh
echo "exit: $?" # 0 = permitido
Si la primera devuelve 2 y la segunda 0, tu patrón funciona. Es la forma más rápida de iterar sobre la lista de patrones sin tener que provocar el comando real dentro de Claude Code.
Información adicional sobre la lista de patrones
En el ejemplo anterior incluí los patrones más representativos. Otros patrones que podrías agregar o quitar según tu entorno:
- Ejecución directa de SQL que incluya
DROP TABLE/TRUNCATE. - Combinaciones como leer el archivo
.envy enviarlo a una URL externa. - Sobrescritura del
crontab.
Hay un equilibrio: más patrones prohibidos no significan más seguridad, y si bloqueas demasiado, ya no puedes usar Claude Code de forma efectiva. Es más fácil de mantener si asumes que los patrones se van desarrollando poco a poco con el uso en la práctica.
3. Pedir confirmación antes de operaciones irreversibles
Bloquear no es la única respuesta. Para operaciones que a veces sí quieres ejecutar —pero solo después de verlas— está el patrón de pedir confirmación humana antes de ejecutar. En lugar de exit 2 (bloqueo directo), el hook devuelve permissionDecision: "ask" y Claude Code muestra su diálogo de confirmación nativo; tú apruebas o cancelas ahí.
Ejemplo de puerta de confirmación (hook dinámico)
#!/usr/bin/env bash
# PreToolUse hook: Pide confirmación antes de una operación irreversible
# Devuelve permissionDecision: "ask" para que Claude Code muestre su diálogo nativo
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | python3 -c "
import sys, json
data = json.load(sys.stdin)
print(data.get('tool_input', {}).get('command', ''))
" 2>/dev/null)
if [ -z "$COMMAND" ]; then
exit 0
fi
# Patrones de operaciones que requieren confirmación
CONFIRM_PATTERNS=(
'git[[:space:]]+push'
'git[[:space:]]+reset[[:space:]]+--hard'
'git[[:space:]]+clean[[:space:]]+-f'
'git[[:space:]]+stash[[:space:]]+drop'
'npm[[:space:]]+publish'
'docker[[:space:]]+push'
)
for PATTERN in "${CONFIRM_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qE "$PATTERN"; then
# Salida JSON estructurada: pide confirmación al usuario
python3 -c "
import json
print(json.dumps({
'hookSpecificOutput': {
'hookEventName': 'PreToolUse',
'permissionDecision': 'ask',
'permissionDecisionReason': 'Operación potencialmente irreversible: requiere confirmación humana'
}
}, ensure_ascii=False))
"
exit 0
fi
done
exit 0
El hook detecta el patrón y, en lugar de bloquear, devuelve por stdout un JSON con permissionDecision: "ask" y un motivo. Con eso, Claude Code muestra su propio diálogo de confirmación (con opción de “sí, no volver a preguntar”) y espera tu decisión.
Importante: no intentes pedir el y/N tú mismo escribiendo a /dev/tty desde el hook. Desde la versión 2.1.139, los hooks corren sin terminal controladora —así lo recoge la documentación oficial de hooks—, así que abrir /dev/tty falla o se cuelga. El camino soportado es el de arriba: devolver "ask" y dejar que Claude Code muestre su diálogo.
Alternativa declarativa con permissions.ask
Si los patrones son fijos, ni siquiera necesitas el script: declara las reglas ask directamente en settings.json y Claude Code pedirá confirmación por ti.
{
"permissions": {
"ask": [
"Bash(git push *)",
"Bash(git reset --hard *)",
"Bash(npm publish *)",
"Bash(docker push *)"
]
}
}
Deja el hook dinámico para cuando necesites lógica que una regla estática no cubre (por ejemplo, decidir según el contenido del comando).
Cuándo usar el bloqueo y cuándo la confirmación
| Naturaleza de la operación | Respuesta recomendada |
|---|---|
No se quiere dejar ejecutar bajo ninguna circunstancia (rm -rf, etc.) | Bloquear con exit 2 en el hook |
Normalmente innecesario, pero requerido según la situación (git push, etc.) | Pedir confirmación con ask |
| De uso frecuente, pero requiere verificar el contenido (migración de BD, etc.) | Pedir confirmación con ask |
Operaciones rutinarias sin problemas (git status, etc.) | Sin restricciones |
Al separar lo que no se ejecuta nunca de lo que solo pasa tras confirmar, mantienes únicamente las puertas necesarias sin imponer restricciones excesivas.
4. Proteger las API keys y evitar fugas de información confidencial
La fuga de información confidencial llega tarde: cuando te das cuenta, el secreto ya está escrito. Es la versión automatizada de saber qué no subir a GitHub: en lugar de recordarlo tú, lo vigilan los hooks. Lo abordo con una estrategia de dos pasos.
4-1. Evitar que los lea con permissions.deny (reglas Read)
Claude Code no usa un archivo .claudeignore: es una convención de la comunidad que no existe en la configuración oficial. El mecanismo vigente para que el agente no pueda leer archivos sensibles es permissions.deny con reglas de tipo Read(...) en settings.json. Desde el principio evito que lea los .env o cualquier archivo que pueda contener secretos:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(./credentials/**)",
"Read(./*.pem)",
"Read(./*.key)",
"Read(./*.p12)",
"Read(./*.pfx)",
"Read(~/.aws/credentials)",
"Read(~/.ssh/**)",
"Read(./*.token)"
]
}
}
Con esto, esos archivos quedan fuera del acceso normal de Claude Code: no aparecen en búsquedas y las operaciones de lectura sobre esas rutas quedan denegadas. Mantén además esas rutas en tu .gitignore: son dos capas distintas y complementarias. El .gitignore evita que el archivo llegue al repositorio; permissions.deny evita que Claude Code acceda a esos archivos desde su flujo normal de herramientas.
4-2. Escanear el contenido de los commits con el hook PostToolUse
Justo después de que se completa un git add, reviso el contenido preparado (staged). Puedo disparar el hook también tras un git commit, pero este script mira git diff --cached, así que su utilidad real está antes del commit, mientras los cambios siguen en staging. Recuerda que PostToolUse corre después de que la herramienta tuvo éxito.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/scan-staged-secrets.sh"
}
]
}
]
}
}
#!/usr/bin/env bash
# PostToolUse hook: Escanea si hay información confidencial en los archivos preparados (staged)
# Como PostToolUse ya se ejecutó, exit 2 no tiene sentido
# Aquí solo emitiremos una advertencia
# Verifica si es el contexto donde se ejecutó git add
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | python3 -c "
import sys, json
data = json.load(sys.stdin)
print(data.get('tool_input', {}).get('command', ''))
" 2>/dev/null)
# Si no es un comando relacionado con git add o git commit, omitir
if ! echo "$COMMAND" | grep -qE 'git[[:space:]]+(add|commit)'; then
exit 0
fi
# Escanea el contenido preparado
STAGED_DIFF=$(git diff --cached 2>/dev/null)
if [ -z "$STAGED_DIFF" ]; then
exit 0
fi
# Patrones de información confidencial
SECRET_PATTERNS=(
'AKIA[0-9A-Z]{16}'
'sk-[a-zA-Z0-9]{32,}'
'ghp_[a-zA-Z0-9]{36}'
'xoxb-[0-9a-zA-Z-]+'
'Bearer[[:space:]]+[a-zA-Z0-9._-]{20,}'
'password[[:space:]]*=[[:space:]]*[^$\n]{8,}'
'secret[[:space:]]*=[[:space:]]*[^$\n]{8,}'
'api_key[[:space:]]*=[[:space:]]*[^$\n]{8,}'
)
FOUND=false
for PATTERN in "${SECRET_PATTERNS[@]}"; do
if echo "$STAGED_DIFF" | grep -qiE "$PATTERN"; then
echo "SecretScan: Advertencia — Se detectó un patrón que podría ser información confidencial en un archivo preparado" >&2
echo "SecretScan: Patrón: $PATTERN" >&2
echo "SecretScan: Elimínalo del stage con git reset HEAD <nombre_del_archivo> y luego verifícalo" >&2
FOUND=true
fi
done
if [ "$FOUND" = true ]; then
# En PostToolUse, exit 2 no tiene efecto, por lo que solo es una advertencia
# Agrega la lógica para hacer un unstage automático si es necesario
exit 1
fi
exit 0
En la etapa de PostToolUse, el git add ya se completó. No puedes detener la operación con exit 2. El papel de este script es emitir una advertencia para que el usuario se dé cuenta. Si quieres detener el commit, inserta una puerta de confirmación en el momento del git commit usando PreToolUse.
4-3. Escribirlo en CLAUDE.md como instrucción para el agente
Además de los hooks y la configuración de permisos, también sirve escribir reglas directamente en CLAUDE.md (el archivo de configuración que lee Claude Code).
Manejo de información confidencial
- No incluyas el contenido del archivo
.envtal cual en el código, los commits ni la salida. - Maneja las API keys, tokens de autenticación y contraseñas haciendo referencia a variables de entorno (como
process.env.API_KEY) y no escribas los valores directamente. - No envíes archivos que puedan contener secretos a URLs externas.
- No incluyas información confidencial en los mensajes de los commits.
Los LLM siguen las instrucciones hasta cierto punto. Combinar esto con la prevención mecánica de los hooks da una configuración más difícil de saltarse.
5. Consejos de operación: haz que los guardrails se noten
Después de implementar un guardrail, no saber por qué se detuvo algo te cuesta tiempo cada vez. Es más fácil de mantener si lo diseñas para que el humano se dé cuenta en cuanto algo se detiene.
Dejar un log
Si escribes en un archivo de log los eventos que bloqueó el script del hook, podrás revisarlos más tarde.
# ... (Agrega lo siguiente al script mencionado anteriormente)
LOG_DIR="${HOME}/.claude/logs"
mkdir -p "$LOG_DIR"
LOG_FILE="${LOG_DIR}/guardrail-$(date +%Y-%m-%d).log"
log_event() {
local level="$1"
local message="$2"
echo "$(date -Iseconds) [$level] $message" >> "$LOG_FILE"
}
# Cuando se bloquea
log_event "BLOCKED" "Comando: $COMMAND / Patrón: $PATTERN"
echo "Guard: Se ha bloqueado: $COMMAND" >&2
exit 2
Especificar claramente las excepciones de los guardrails
Si escribes en la documentación (CLAUDE.md) qué se está limitando y por qué, reduces la confusión al usarlo en equipo.
Operaciones que se detienen automáticamente (bloqueo mediante hook). Las siguientes operaciones se bloquean automáticamente por el hook. Si se detienen, ejecútalas manualmente.
- Comandos del tipo
rm -rf. git push --force.- Ejecución por tubería de
curl/wget(comocurl ... | bash, etc.). chmod 777.
Operaciones donde aparece la puerta de confirmación. Para las siguientes operaciones se muestra un diálogo de confirmación antes de la ejecución.
git push(incluido un push normal).npm publish.git reset --hard.
Revisarlos periódicamente
Los guardrails no son algo de “lo implementas y listo”. A medida que operas, te das cuenta de cosas como “esta restricción en realidad era innecesaria” o “este patrón todavía no estaba incluido”. Si te acostumbras a mirar los logs y verificar qué se está deteniendo alrededor de una vez al mes, es más fácil descubrir tanto las restricciones excesivas como las omisiones.
Los 4 guardrails de seguridad en Claude Code, de un vistazo
Estas son las mejores prácticas de seguridad en Claude Code que organicé en 4 temas.
| Tema | Punto de partida de la implementación |
|---|---|
| Diseño de permisos mínimos | Empezar con permissions.deny en settings.json |
| Bloqueo de comandos peligrosos | Hook PreToolUse + exit 2 |
| Puerta de confirmación para operaciones irreversibles | Regla ask o hook que devuelve permissionDecision: "ask" |
| Prevención de fuga de información confidencial | permissions.deny (Read) + escaneo en PostToolUse + estipulación explícita en CLAUDE.md |
No hace falta introducir todo a la vez. Con solo escribir 3 líneas de deny en settings.json ya marcas una gran diferencia frente a no tener guardrails. A partir de ahí, la forma más realista de avanzar es ir desarrollándolo poco a poco mientras miras los logs de lo que realmente se ha detenido.
Un guardrail bien puesto no le quita autonomía a Claude Code: te devuelve la decisión en el punto exacto que importa. Cuando eres tú quien traza la frontera entre lo que le dejas hacer y lo que confirmas, la confiabilidad del desarrollo con IA cambia por completo.