{
  "id": "ai-forgets-env-validation",
  "type": "failures",
  "category": "failures",
  "locale": "en",
  "url": "/failures/ai-forgets-env-validation",
  "title": "How to Fix AI Forgetting Environment Variable Validation",
  "description": "AI agents read process.env values directly without validation, causing silent undefined bugs and missing startup errors when env vars are absent.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "TypeScript",
    "Cloudflare"
  ],
  "tags": [
    "security",
    "typescript",
    "cloudflare"
  ],
  "difficulty": null,
  "updated": "2026-06-08",
  "markdown": "The agent accesses `process.env.SOME_KEY` directly, so when the variable is\nmissing the app starts silently broken — no crash, no warning, just `undefined`\nflowing through business logic.\n\n## The symptom\n\nRaw `process.env` reads scattered across the codebase with no schema, no type\nsafety, and no startup assertion.\n\n```ts\n// lib/stripe.ts — WRONG\nimport Stripe from \"stripe\";\n\nexport const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {\n  apiVersion: \"2024-06-20\",\n});\n// If STRIPE_SECRET_KEY is undefined, Stripe SDK accepts it and every\n// charge silently fails at runtime instead of at startup.\n```\n\n## Why it happens\n\nThe agent optimizes for brevity and reaching working code fast. Env validation\nis boilerplate that doesn't appear in most training examples, so the model skips\nit and goes straight to the integration.\n\n## How to spot it\n\n- `process.env.FOO` used without a non-null assertion or fallback check.\n- No `env.ts` / `env.mjs` validation file in the project.\n- TypeScript type of an env value is `string | undefined` at the call site.\n- App boots without error even when `.env.local` is empty.\n\n## How to fix it\n\nValidate all required env vars at startup using a Zod schema so the process\ncrashes with a clear message before serving a single request.\n\n```ts\n// lib/env.ts — CORRECT\nimport { z } from \"zod\";\n\nconst envSchema = z.object({\n  STRIPE_SECRET_KEY: z.string().min(1),\n  DATABASE_URL: z.string().url(),\n  NEXTAUTH_SECRET: z.string().min(32),\n  NODE_ENV: z.enum([\"development\", \"production\", \"test\"]).default(\"development\"),\n});\n\nexport const env = envSchema.parse(process.env);\n//                             ^^^^^ throws at startup if any var is missing\n```\n\n```ts\n// lib/stripe.ts — CORRECT (import validated env)\nimport Stripe from \"stripe\";\nimport { env } from \"@/lib/env\";\n\nexport const stripe = new Stripe(env.STRIPE_SECRET_KEY, {\n  apiVersion: \"2024-06-20\",\n});\n```\n\n```txt\n[ ] Create lib/env.ts with a Zod schema covering every required variable\n[ ] Import env from lib/env everywhere — never use process.env directly\n[ ] Add lib/env.ts to the module graph so it runs at server startup (import in next.config.ts)\n[ ] Document all variables in .env.example with placeholder values\n[ ] Use z.string().url() / z.string().min(n) for format constraints, not just presence\n```\n\n## Fix Prompt\n\n```txt title=\"Fix Prompt\"\nEvery raw process.env access in this file is unvalidated. Create a lib/env.ts\nmodule that parses and validates all required environment variables with Zod at\nstartup. Replace every process.env.FOO reference in the codebase with the\ntyped env.FOO import. Add a .env.example file listing every variable with a\nplaceholder value and comment.\n```\n\n## Test\n\n```bash\n# Find any remaining raw process.env reads outside of lib/env.ts\ngrep -rn \"process\\.env\\.\" --include=\"*.ts\" --include=\"*.tsx\" . \\\n  | grep -v \"lib/env.ts\" \\\n  | grep -v \"next.config\" \\\n  | grep -v \"node_modules\" \\\n  && echo \"FAIL: raw process.env reads found\" || echo \"OK\"\n```"
}