Variables de entorno en Claude Code: .env, Zod, Secrets y despliegue seguro
Gestiona variables de entorno y Secrets con Claude Code: .env.example, validación Zod, CI/CD, redacción y rotación.
Cuando un principiante pide a Claude Code que implemente login, pagos, webhooks o una integración de IA, el primer problema serio rara vez es el diseño visual. Casi siempre aparece en la configuración: falta una URL de base de datos, se usa una clave de staging en producción, un WEBHOOK_SECRET queda impreso en CI o una API key real termina en GitHub.
Esta guía muestra un patrón listo para implementar gestión de variables de entorno y Secrets con Claude Code. Una variable de entorno es un valor que la aplicación recibe al arrancar, como PORT o APP_ORIGIN. Un Secret es un valor que puede ser abusado si se filtra, como ANTHROPIC_API_KEY, DATABASE_URL o WEBHOOK_SECRET. Muchos Secrets llegan como variables de entorno, pero no deben tratarse como valores normales.
Para la configuración propia de Claude Code, revisa la referencia oficial de Claude Code environment variables. Para validar la configuración de la aplicación usaremos Zod. En CI/CD y despliegue conviene consultar GitHub Actions secrets, Vercel environment variables, Cloudflare Workers variables and secrets y Docker secrets. Cada plataforma cambia la interfaz, pero la regla es estable: versiona los nombres y reglas, nunca los valores secretos.
Para una visión más amplia, complementa con buenas prácticas de seguridad en Claude Code y autenticación JWT con Claude Code.
Trata .env como un contrato
Un archivo .env es útil, pero no debe convertirse en una libreta privada de cada desarrollador. El equipo necesita tres capas:
- Declaración:
.env.exampleenumera las claves necesarias - Validación: Zod falla al arrancar si falta un valor o tiene formato incorrecto
- Operación: CI/CD y producción inyectan valores desde Secrets de la plataforma
flowchart LR
Dev["local .env.local"] --> Schema["Zod schema"]
CI["GitHub Actions secrets"] --> Schema
Prod["Vercel / Cloudflare / Docker secrets"] --> Schema
Schema --> App["Type-safe app config"]
Schema --> Logs["Redacted logs"]
Example[".env.example"] --> Dev
El punto práctico es que todas las entradas pasan por el mismo schema. A Claude Code debes darle nombres de claves, reglas de validación y comportamiento de error, no valores reales de producción.
Casos de uso concretos
| Caso | Variables típicas | Fallo común |
|---|---|---|
| Desarrollo local | APP_ORIGIN, DATABASE_URL, ANTHROPIC_API_KEY | Solo funciona en el equipo de una persona |
| Verificación de webhooks | STRIPE_WEBHOOK_SECRET, WEBHOOK_SECRET | Se aceptan peticiones falsificadas |
| Tests en CI | CI_DATABASE_URL, TEST_API_KEY | El PR pasa, pero falla el despliegue |
| Producción | DATABASE_URL, SESSION_SECRET, APP_ORIGIN | Conexión a DB equivocada o credenciales expuestas |
| Rotación de secretos | ANTHROPIC_API_KEY_NEXT | Una clave antigua comprometida sigue activa |
En el flujo de Masa para ClaudeCodeLab, la mejora más útil no fue solo crear .env.example; fue hacer que la aplicación se negara a arrancar si el contrato estaba incompleto. Eso convierte sorpresas de despliegue en fallos revisables dentro del PR.
1. Separa primero los archivos
.env.example documenta, no guarda secretos reales. .env.local pertenece a una máquina local. .env.production.example es una checklist para producción sin valores reales.
mkdir -p src/config
touch .env.example .env.local .env.production.example src/config/env.ts
# .gitignore
.env
.env.*
!.env.example
!.env.production.example
# Cloudflare local secrets
.dev.vars
.dev.vars.*
# .env.example
APP_ENV=local
NODE_ENV=development
PORT=3000
APP_ORIGIN=http://localhost:3000
DATABASE_URL=postgresql://app:app@localhost:5432/app
ANTHROPIC_API_KEY=replace-with-local-dev-key
WEBHOOK_SECRET=replace-with-32-plus-character-secret
PUBLIC_ANALYTICS_KEY=
LOG_LEVEL=info
# .env.production.example
APP_ENV=production
NODE_ENV=production
PORT=3000
APP_ORIGIN=https://example.com
DATABASE_URL=<set-in-platform-secret-store>
ANTHROPIC_API_KEY=<set-in-platform-secret-store>
WEBHOOK_SECRET=<set-in-platform-secret-store>
PUBLIC_ANALYTICS_KEY=<optional-public-key>
LOG_LEVEL=info
Los placeholders no son valores seguros. Solo indican que la clave debe existir en otra parte.
2. Valida con Zod al arrancar
En Node.js, las variables de entorno llegan como texto. Incluso PORT=3000 es una cadena, por eso usamos z.coerce.number() para convertir y validar.
npm install zod dotenv
npm install -D tsx typescript @types/node
// src/config/env.ts
import "dotenv/config";
import { z } from "zod";
const secretNamePattern = /(SECRET|TOKEN|PASSWORD|API_KEY|DATABASE_URL|DSN)/i;
function redactValue(key: string, value: unknown): string {
if (value === undefined || value === null || value === "") return "<empty>";
const text = String(value);
if (!secretNamePattern.test(key)) return text;
if (text.length <= 8) return "<redacted>";
return `${text.slice(0, 4)}...${text.slice(-4)}`;
}
const envSchema = z.object({
APP_ENV: z.enum(["local", "development", "staging", "production"]).default("local"),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
APP_ORIGIN: z.string().url(),
DATABASE_URL: z.string().url(),
ANTHROPIC_API_KEY: z.string().min(20, "ANTHROPIC_API_KEY is too short"),
WEBHOOK_SECRET: z.string().min(32, "WEBHOOK_SECRET must be at least 32 characters"),
PUBLIC_ANALYTICS_KEY: z.string().optional(),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error("Environment validation failed:");
for (const issue of parsed.error.issues) {
const key = String(issue.path[0] ?? "unknown");
console.error(`- ${key}: ${issue.message}; current=${redactValue(key, process.env[key])}`);
}
process.exit(1);
}
export const env = Object.freeze(parsed.data);
export type AppEnv = typeof env;
export function isProduction(): boolean {
return env.APP_ENV === "production";
}
export function publicEnv() {
return {
APP_ENV: env.APP_ENV,
APP_ORIGIN: env.APP_ORIGIN,
PUBLIC_ANALYTICS_KEY: env.PUBLIC_ANALYTICS_KEY ?? "",
};
}
Comprueba el módulo:
cp .env.example .env.local
npx tsx src/config/env.ts
Luego pide a Claude Code que centralice los accesos:
Busca todas las lecturas directas de process.env en este repositorio.
Solo src/config/env.ts debe leer process.env directamente.
En otros archivos, propone importar env desde src/config/env.ts.
No imprimas secrets en logs, errores ni snapshots de tests.
3. Redacta antes de registrar
Las fugas de secretos no ocurren solo en Git. También aparecen en logs de CI, salidas de depuración, herramientas de error tracking, grabaciones de pantalla o prompts pegados en Claude Code.
// src/config/redact.ts
const sensitiveKeyPattern = /(SECRET|TOKEN|PASSWORD|API_KEY|DATABASE_URL|AUTH|COOKIE|PRIVATE)/i;
export function redactSecrets(input: Record<string, unknown>): Record<string, string> {
return Object.fromEntries(
Object.entries(input).map(([key, value]) => {
if (value === undefined || value === null || value === "") return [key, "<empty>"];
const text = String(value);
if (!sensitiveKeyPattern.test(key)) return [key, text];
return [key, text.length <= 10 ? "<redacted>" : `${text.slice(0, 4)}...${text.slice(-4)}`];
}),
);
}
import { env } from "./env";
import { redactSecrets } from "./redact";
console.info("Loaded config", redactSecrets(env));
La redacción es una defensa final. El mejor log es el que nunca recibe el secreto.
4. Inyecta valores desde CI/CD
GitHub Actions permite pasar secrets de repositorio, entorno u organización. No reutilices credenciales de producción para pruebas normales de PR; crea valores de CI con permisos limitados.
# .github/workflows/env-check.yml
name: env-check
on:
pull_request:
push:
branches: [main]
jobs:
validate-env:
runs-on: ubuntu-latest
env:
APP_ENV: development
NODE_ENV: test
PORT: 3000
APP_ORIGIN: http://localhost:3000
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }}
LOG_LEVEL: info
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
- run: npm ci
- name: Mask runtime-only values
run: echo "::add-mask::$APP_ORIGIN"
- run: npx tsx src/config/env.ts
- run: npm test -- --runInBand
No todos los workflows reciben secrets bajo las mismas condiciones. Fork PRs, workflows reutilizables y eventos automáticos pueden comportarse de forma distinta. Mantén explícito el job de validación y no escribas secrets en archivos generados.
5. Docker, Vercel y Cloudflare
En Docker, no escribas ENV API_KEY=... dentro del Dockerfile. Para pruebas locales puedes usar un env file; en producción usa el mecanismo de secretos de tu runtime.
# local only
docker run --rm --env-file .env.local my-app:latest
Si el runtime entrega secretos como archivos, soporta la convención NAME_FILE:
// src/config/secret-file.ts
import fs from "node:fs";
export function readEnvOrFile(name: string): string | undefined {
const direct = process.env[name];
if (direct) return direct;
const filePath = process.env[`${name}_FILE`];
if (!filePath) return undefined;
return fs.readFileSync(filePath, "utf8").trim();
}
En Vercel separa Production, Preview y Development. Cuidado con prefijos expuestos al navegador como NEXT_PUBLIC_. En Cloudflare Workers, los valores llegan mediante bindings, el parámetro env o secrets de la plataforma. No sobreprometas portabilidad: documenta el punto de inyección por plataforma y deja que el schema sea la fuente de verdad.
Revisa la configuración de entorno para Vercel, Cloudflare y Docker.
No leas ni pidas valores reales de producción.
Comprueba nombres obligatorios, claves públicas vs secretas, build-time vs runtime y notas de rotación.
6. Escribe un playbook de rotación
La rotación de Secrets no debe improvisarse durante un incidente:
- Identifica alcance: servicio, entorno, permisos y responsable
- Crea el nuevo valor con permisos mínimos
- Añádelo como
*_NEXTsi puedes aceptar dos valores temporalmente - Despliega soporte para clave antigua y nueva durante una ventana corta
- Cambia el tráfico y verifica health checks
- Revoca el valor antiguo
- Busca exposición en Git, CI, logs y prompts
- Actualiza
.env.exampley notas operativas
Un Webhook secret, una API key y una contraseña de base de datos rotan de forma distinta. Documenta dueño, ventana y rollback para cada uno.
Fallos frecuentes
| Fallo | Causa | Corrección |
|---|---|---|
.env en Git | .gitignore llegó tarde | Revoca claves; limpiar historia no basta |
Secret en NEXT_PUBLIC_ | Se confundió el prefijo público | Separa nombres públicos y privados |
console.log(process.env) | Depuración apresurada | Usa redacción y revisión de logs |
| Producción no arranca | Falta una clave obligatoria | Ejecuta src/config/env.ts en CI |
| Valor local en build productivo | Confusión build-time/runtime | Documenta inyección por plataforma |
| Clave real pegada en Claude Code | Se mezcló prompt con secret sharing | Comparte nombres y reglas, no valores |
Prompt listo para Claude Code
Implementa la gestión de variables de entorno para este proyecto.
Requisitos:
- Crear .env.example y .env.production.example
- Mantener .env, .env.* y .dev.vars* fuera de Git
- Añadir un schema Zod en src/config/env.ts y fallar al arrancar si faltan valores
- Centralizar lecturas directas de process.env en src/config/env.ts
- Redactar secrets en logs de diagnóstico
- Añadir un job de GitHub Actions que valide env en pull requests
- Escribir notas breves de despliegue para Vercel, Cloudflare y Docker
No leas API keys reales ni URLs de base de datos de producción. Trabaja solo con nombres de claves y reglas de validación.
Conclusión
Gestionar entorno con Claude Code no significa pegar secretos en el chat. Significa implementar un contrato: .env.example declara las claves, Zod valida al arrancar, los logs se redactan, CI/CD y la plataforma inyectan valores reales, y el equipo tiene un playbook de rotación.
ClaudeCodeLab ofrece consultoría, formación de equipos, revisión de seguridad de repositorios y plantillas para autenticación, pagos, CI/CD y operaciones de contenido con Claude Code. Si tu equipo quiere avanzar más rápido sin filtrar claves de producción, esta es una de las primeras prácticas que conviene estandarizar.
En el repositorio de prueba de Masa, este patrón detectó antes del despliegue una clave de producción ausente, un Webhook secret que podía aparecer en logs y un .env.example desactualizado. La validación de arranque con Zod es sencilla, pero convierte la configuración en un contrato verificable.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Escalera de permisos de Claude Code para ampliar acceso sin perder control
Pasa de read-only a ediciones limitadas, comandos de prueba y checks de deploy con menos riesgo.
Claude Code Small PR Proof Pack: cambios pequeños que sí se pueden revisar
Un paquete de prueba para PRs de Claude Code: diff, checks, URL pública, CTA y rollback.
Gate de revisión antes del commit con Claude Code
Cómo revisar con Claude Code antes del commit: diff, build, URL pública, Gumroad, consultoría, tests y archivos ajenos.