Claude Code y archivos .env: cómo evitar que tus claves acaben en el contexto

Claude Code puede ser una herramienta muy potente para programar, revisar repositorios, ejecutar pruebas y automatizar tareas de desarrollo. Pero esa misma capacidad lo convierte en un riesgo si se abre dentro de un proyecto que contiene secretos en texto plano: claves API, tokens de Stripe, credenciales de base de datos, certificados privados, ficheros .npmrc, perfiles de AWS o claves SSH.

El problema no es exclusivo de Claude Code. Cualquier agente de programación con acceso al sistema de archivos y a la terminal puede leer más contexto del que el usuario tenía en mente. La diferencia está en que Claude Code trabaja de forma muy integrada con el repositorio: puede usar herramientas de lectura, búsqueda, edición y Bash. Si una clave entra en el contexto de la conversación, aparece en la salida de comandos o se guarda en el historial local, ya se ha producido una exposición que conviene tratar como incidente.

La documentación oficial de Claude Code es clara en un punto que muchos desarrolladores pasan por alto: los transcripts e historiales locales no están cifrados en reposo, y si una herramienta lee un .env o un comando imprime una credencial, ese valor puede quedar escrito en los ficheros de sesión locales. Además, las reglas de denegación de lectura no bloquean por sí solas lo que pueda hacer un subproceso Bash. Por eso una configuración segura no puede depender solo de escribir “no leas .env” en CLAUDE.md.

Por qué CLAUDE.md no basta para proteger secretos

CLAUDE.md es útil para dar instrucciones al agente: estilo del proyecto, comandos habituales, reglas de arquitectura, convenciones de código o advertencias operativas. Puede incluir una norma del tipo “no leas archivos .env”, pero eso es una instrucción al modelo, no una barrera técnica completa.

La diferencia importa. Una instrucción puede ayudar, pero no impide físicamente el acceso. Si el agente interpreta que necesita revisar una configuración, si una herramienta de búsqueda devuelve coincidencias o si un comando de test imprime variables sensibles, la clave puede terminar en el contexto igualmente.

La protección debe situarse en varias capas. Primero, permisos de Claude Code para bloquear lecturas y escrituras directas. Segundo, límites estrictos a Bash, porque una regla Read(**/.env*) no evita necesariamente un cat .env ejecutado desde la terminal. Tercero, entornos de test con claves falsas. Cuarto, detección de secretos antes de hacer commit o push. Quinto, aislamiento con contenedores o sandbox cuando se trabaja con repositorios sensibles.

CapaQué protegeQué no resuelve por sí sola
CLAUDE.mdOrienta al agenteNo impide acceso técnico
settings.json con denyBloquea herramientas internas de lectura/escrituraNo bloquea todos los subprocesos Bash
Limitación de BashReduce comandos peligrosos o lecturas indirectasPuede romper flujos si se configura mal
.env.testEvita que los tests impriman secretos realesNo protege producción
Gitleaks / pre-commitEvita commits con secretosNo evita lectura local por el agente
Contenedor / sandboxAísla el sistema de archivosRequiere más preparación

Las tres formas habituales de fuga

La primera vía es la lectura directa. Claude Code puede intentar abrir un archivo para entender cómo se configura la aplicación. Si el archivo es .env, .env.local, .dev.vars, credentials.json, database.yml, .npmrc, .pypirc o una clave privada, el contenido puede entrar en la conversación.

La segunda vía es la salida de comandos. El agente no necesita leer .env para que una clave se filtre. Basta con ejecutar un test que falla y vuelca process.env, una petición HTTP que imprime Authorization: Bearer ..., un cliente de base de datos que muestra la cadena de conexión completa o una herramienta de depuración que registra variables de entorno. Todo lo que aparece en stdout o stderr puede acabar en el historial de sesión.

La tercera vía es la búsqueda. Herramientas como grep, rg, find, scripts de inspección o comandos de diagnóstico pueden devolver líneas que contienen credenciales. Es un problema frecuente en repositorios antiguos donde una clave no está en .env, sino en un fichero YAML, JSON, .ini, docker-compose.override.yml o en documentación interna.

Configuración global segura para ~/.claude/settings.json

Claude Code permite definir ajustes globales en ~/.claude/settings.json, ajustes compartidos por proyecto en .claude/settings.json y ajustes locales no versionados en .claude/settings.local.json. Para reglas de seguridad personales, el fichero global es un buen punto de partida. Para reglas de equipo, conviene usar configuración gestionada o, como mínimo, una configuración de proyecto revisada.

Esta configuración bloquea lecturas directas de secretos comunes y limita operaciones peligrosas. Es deliberadamente conservadora:

{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Read(src/**)",
"Read(tests/**)",
"Read(package.json)",
"Read(tsconfig.json)",
"ReadREADME.md",
"Glob(src/**)",
"Glob(tests/**)",
"Grep",
"Edit(src/**)",
"Edit(tests/**)",
"Write(src/**)",
"Write(tests/**)",
"Bash(npm run lint)",
"Bash(npm run test)",
"Bash(npm test)",
"Bash(npx tsc --noEmit)",
"Bash(git status)",
"Bash(git diff *)",
"Bash(git log *)"
],
"deny": [
"Read(**/.env*)",
"Read(**/.dev.vars*)",
"Read(**/*.pem)",
"Read(**/*.key)",
"Read(**/*.p12)",
"Read(**/*.pfx)",
"Read(**/secrets/**)",
"Read(**/credentials/**)",
"Read(**/.aws/**)",
"Read(**/.ssh/**)",
"Read(**/config/database.yml)",
"Read(**/config/credentials.json)",
"Read(**/.npmrc)",
"Read(**/.pypirc)",

"Write(**/.env*)",
"Write(**/.dev.vars*)",
"Write(**/*.pem)",
"Write(**/*.key)",
"Write(**/secrets/**)",
"Write(**/credentials/**)",
"Write(**/.aws/**)",
"Write(**/.ssh/**)",
"Write(.github/workflows/**)",

"Bash(cat *env*)",
"Bash(cat *.pem)",
"Bash(cat *.key)",
"Bash(head *env*)",
"Bash(tail *env*)",
"Bash(printenv)",
"Bash(env)",
"Bash(set)",
"Bash(rm -rf *)",
"Bash(sudo *)",
"Bash(git push *)",
"Bash(npm publish *)",
"Bash(curl * | sh)",
"Bash(wget *)"
],
"defaultMode": "acceptEdits"
},
"cleanupPeriodDays": 7
}

Hay dos matices importantes. El primero: las reglas Read y Write siguen patrones similares a gitignore, y ** permite aplicar la regla de forma recursiva. El segundo: según la documentación oficial, las reglas Read y Edit se aplican a herramientas internas de Claude Code, pero no sustituyen a un aislamiento del sistema operativo. Para proyectos sensibles, el sandbox o el contenedor son la opción más seria.

También conviene activar variables de entorno de seguridad al lanzar Claude Code cuando se trabaje con repositorios delicados:

export CLAUDE_CODE_SKIP_PROMPT_HISTORY=1
export CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1
export DISABLE_TELEMETRY=1
export DISABLE_ERROR_REPORTING=1

claude

CLAUDE_CODE_SKIP_PROMPT_HISTORY=1 evita escribir historial y transcripts locales en disco. CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1 reduce la exposición de credenciales de Anthropic y proveedores cloud en subprocesos. DISABLE_TELEMETRY y DISABLE_ERROR_REPORTING limitan tráfico operativo no esencial.

El .env.test que sí puede ver un agente

La regla más práctica es simple: Claude Code no debería necesitar secretos reales para ejecutar tests. Los tests deben usar valores falsos, revocables y claramente identificables.

Un ejemplo de .env.test seguro:

# .env.test — valores falsos, seguros para logs y sesiones de IA
NODE_ENV=test

DATABASE_URL=postgres://test:test@localhost:5432/app_test

STRIPE_SECRET_KEY=sk_test_dummy_not_real
STRIPE_WEBHOOK_SECRET=whsec_dummy_not_real

OPENAI_API_KEY=sk-test-dummy-key
ANTHROPIC_API_KEY=sk-ant-test-dummy-key

AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_REGION=eu-west-1

JWT_SECRET=test-jwt-secret-not-for-production
SESSION_SECRET=test-session-secret-not-for-production

Después se fuerza a la herramienta de test a cargar ese fichero, no el .env real. En Node.js, por ejemplo:

{
"scripts": {
"test": "dotenv -e .env.test -- vitest run",
"test:watch": "dotenv -e .env.test -- vitest"
},
"devDependencies": {
"dotenv-cli": "^7.4.4",
"vitest": "^3.0.0"
}
}

En Python con pytest se puede hacer algo parecido:

# .env.test
DATABASE_URL=postgresql://test:test@localhost:5432/app_test
STRIPE_SECRET_KEY=sk_test_dummy_not_real
# conftest.py
from dotenv import load_dotenv

load_dotenv(".env.test")

Y en Rails:

# config/database.yml
test:
adapter: postgresql
database: app_test
username: test
password: test
host: localhost

El objetivo es que cualquier error, stack trace o log capturado por Claude Code contenga únicamente valores ficticios.

Cómo evitar fugas por logs

Muchas fugas no nacen en .env, sino en logs demasiado verbosos. Por ejemplo:

console.error("Request failed", {
url,
headers,
body,
});

Si headers incluye Authorization, ya hay un problema. Es mejor usar redacción explícita:

function redact(value) {
if (!value) return value;
return String(value).replace(/(Bearer\s+)[A-Za-z0-9._-]+/g, "$1[REDACTED]");
}

console.error("Request failed", {
url,
headers: {
...headers,
authorization: headers.authorization ? "[REDACTED]" : undefined,
},
message: redact(error.message),
});

En aplicaciones con pino, se puede usar redacción nativa:

import pino from "pino";

export const logger = pino({
redact: {
paths: [
"req.headers.authorization",
"headers.authorization",
"password",
"token",
"apiKey",
"secret",
"*.password",
"*.token"
],
censor: "[REDACTED]"
}
});

Y en Python:

import re
import logging

SECRET_PATTERNS = [
re.compile(r"sk_live_[A-Za-z0-9]+"),
re.compile(r"sk-ant-[A-Za-z0-9_-]+"),
re.compile(r"Bearer\s+[A-Za-z0-9._-]+"),
]

class RedactingFilter(logging.Filter):
def filter(self, record):
msg = record.getMessage()
for pattern in SECRET_PATTERNS:
msg = pattern.sub("[REDACTED]", msg)
record.msg = msg
record.args = ()
return True

logger = logging.getLogger()
logger.addFilter(RedactingFilter())

Pre-commit: mejor Gitleaks que una lista casera

Un hook con expresiones regulares propias puede ayudar, pero es fácil quedarse corto. Para proyectos reales conviene usar una herramienta mantenida como Gitleaks, diseñada para detectar contraseñas, API keys y tokens en repositorios Git, ficheros y entradas estándar.

Instalación con pre-commit:

pipx install pre-commit
brew install gitleaks

.pre-commit-config.yaml:

repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.28.0
hooks:
- id: gitleaks

Activación:

pre-commit install
pre-commit run --all-files

También puede añadirse un hook local simple:

#!/bin/sh
# .git/hooks/pre-commit

if command -v gitleaks >/dev/null 2>&1; then
gitleaks protect --staged --redact --verbose
else
echo "AVISO: gitleaks no está instalado. Instálalo antes de trabajar con secretos."
exit 1
fi
chmod +x .git/hooks/pre-commit

En GitHub, además, conviene activar secret scanning y push protection cuando esté disponible. La protección de push bloquea envíos que contienen secretos compatibles antes de que lleguen al repositorio remoto.

.gitignore no es suficiente si el secreto ya se subió

Todo proyecto que use .env debería tener una regla básica:

.env
.env.*
!.env.example
!.env.test

Y un .env.example sin secretos:

DATABASE_URL=
STRIPE_SECRET_KEY=
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
AWS_REGION=eu-west-1

Pero esto solo protege hacia delante. Si una clave ya fue committeada, añadirla después a .gitignore no la elimina del historial. En ese caso hay que rotar la clave, revocarla en el proveedor y limpiar el historial si procede. La rotación es imprescindible: una vez que una clave ha estado en Git, en logs o en una sesión de IA, debe considerarse comprometida.

Aislamiento con contenedores: la opción para clientes y producción

Para trabajo con credenciales de producción, lo más seguro es que Claude Code no vea esos ficheros. No basta con pedirle que no los lea. Hay que arrancar el entorno sin montarlos.

Ejemplo con Docker:

docker run --rm -it \
-v "$PWD":/workspace \
-v /dev/null:/workspace/.env:ro \
-w /workspace \
node:22-bookworm bash

Dentro del contenedor, .env aparece vacío. Para desarrollo, se puede montar un fichero de test:

docker run --rm -it \
-v "$PWD":/workspace \
-v "$PWD/.env.test":/workspace/.env:ro \
-w /workspace \
node:22-bookworm bash

Y para evitar que archivos sensibles entren en la imagen:

.env
.env.*
*.pem
*.key
.aws
.ssh
secrets
credentials

Si se usa bypassPermissions, debe hacerse solo dentro de entornos aislados. La propia documentación de Claude Code recomienda reservar ese modo para contenedores, máquinas virtuales o dev containers donde el agente no pueda dañar el sistema anfitrión.

Checklist antes de abrir Claude Code en un repositorio

PreguntaMotivo
¿Existe ~/.claude/settings.json con reglas deny para secretos?Evita lecturas directas con herramientas internas
¿Bash está limitado?Reduce lecturas indirectas y comandos peligrosos
¿Los tests cargan .env.test?Evita logs con credenciales reales
¿Hay Gitleaks o secret scanning?Bloquea commits con secretos
¿.env está en .gitignore?Evita añadirlo por error
¿Las claves de producción están en un vault?Reduce secretos en texto plano
¿Los logs redactan tokens y cabeceras?Evita fugas por stdout/stderr
¿El historial local está desactivado en sesiones sensibles?Reduce exposición en disco
¿Se usa contenedor para clientes o producción?Aísla el sistema real

La configuración no elimina todos los riesgos, pero reduce mucho la superficie de exposición. La idea principal es tratar Claude Code como se trataría a cualquier herramienta con acceso a código y terminal: con permisos mínimos, secretos falsos en desarrollo, auditoría y aislamiento cuando el contexto lo exige.

La comodidad de los agentes de programación no debe llevar a meter claves reales en cada proyecto. Un .env es práctico, pero también es una bomba si queda al alcance de herramientas que leen, ejecutan y resumen. La solución no es dejar de usar Claude Code, sino preparar el entorno para que trabaje con el código sin tocar las credenciales.

Preguntas frecuentes

¿Claude Code puede leer archivos .env?
Sí, si tiene acceso al proyecto y no existen reglas o aislamiento que lo impidan, puede leer archivos del repositorio mediante sus herramientas o mediante comandos ejecutados en la terminal.

¿Una regla en CLAUDE.md evita la fuga de secretos?
No de forma fiable. CLAUDE.md orienta el comportamiento del agente, pero no sustituye a permisos técnicos, sandbox, contenedores o herramientas de detección de secretos.

¿Basta con añadir Read(**/.env*) a deny?
No siempre. Esa regla ayuda frente a herramientas internas de lectura, pero la documentación de Claude Code advierte que no bloquea por sí sola subprocesos Bash como cat .env. Para protección fuerte hace falta sandbox o aislamiento del sistema operativo.

¿Qué hago si una clave ya apareció en una sesión o en un commit?
Hay que revocarla y rotarla de inmediato. Después se revisa dónde se ha expuesto: historial Git, logs, transcripts locales, CI/CD, gestores de errores y repositorios remotos.

¿Cuál es la mejor práctica para proyectos sensibles?
No montar secretos reales en el entorno donde trabaja el agente. Usar .env.test, vaults, contenedores, permisos restrictivos, Gitleaks y push protection.

Scroll al inicio