Claude Code Environment Variables Guide: .env, Zod, Secrets, and Production Deploys
Manage Claude Code app env vars and secrets with .env.example, Zod validation, CI/CD injection, redaction, and rotation.
When beginners ask Claude Code to build authentication, payments, webhooks, or AI integrations, the first serious production bug is often not the UI. It is configuration. A missing database URL, a copied staging key, a webhook secret printed to CI logs, or a real API key committed to GitHub can turn an otherwise good feature into an incident.
This guide gives you an implementation-ready pattern for environment variables and secrets management with Claude Code. An environment variable is a value passed to the app at runtime, such as PORT or APP_ORIGIN. A secret is a value that can be abused if exposed, such as ANTHROPIC_API_KEY, DATABASE_URL, or WEBHOOK_SECRET. Secrets may be delivered through environment variables, but they deserve stricter rules.
For Claude Code’s own configuration, use the official Claude Code environment variables reference. For application validation we will use Zod. For CI/CD and deployment, compare the official docs for GitHub Actions secrets, Vercel environment variables, Cloudflare Workers variables and secrets, and Docker secrets. Each platform differs, but the rule is the same: code the list of required keys, never code the secret values.
For broader safeguards, pair this with Claude Code security best practices and JWT authentication with Claude Code.
Treat .env as a Contract
.env files are useful, but they should not become private notebooks. A team needs three layers:
- Declaration: list required keys in
.env.example - Validation: fail fast with a Zod schema when a value is missing or malformed
- Operations: inject production values from platform secrets, not from a copied local file
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
The practical point is simple: every entry point passes through the same schema. Claude Code should read key names, validation rules, and failure behavior. It should not receive the actual production values in a prompt.
Concrete Use Cases
| Use case | Typical values | Failure mode |
|---|---|---|
| Local development | APP_ORIGIN, DATABASE_URL, ANTHROPIC_API_KEY | The app works only on one developer’s machine |
| Webhook verification | STRIPE_WEBHOOK_SECRET, WEBHOOK_SECRET | Unsigned or forged requests are accepted |
| CI testing | CI_DATABASE_URL, TEST_API_KEY | Pull requests pass but deployment fails |
| Production deploy | DATABASE_URL, SESSION_SECRET, APP_ORIGIN | The app connects to the wrong DB or leaks credentials |
| Secret rotation | ANTHROPIC_API_KEY_NEXT | Old compromised credentials stay valid too long |
In Masa’s ClaudeCodeLab workflow, the biggest improvement was not just creating .env.example; it was making the app refuse to boot when the contract was incomplete. That turns deployment surprises into reviewable pull request failures.
1. Split the Files First
Start by separating examples from real values. .env.example is documentation. .env.local is for one developer’s machine. .env.production.example is a deployment checklist without real production values.
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
The placeholder strings are not safe defaults. They are reminders that a value must be supplied somewhere else.
2. Validate Everything With Zod
Install Zod and a TypeScript runner for local checks:
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 ?? "",
};
}
Run it locally:
cp .env.example .env.local
npx tsx src/config/env.ts
Then ask Claude Code to remove scattered process.env reads:
Find every direct process.env read in this repository.
Only src/config/env.ts should read process.env directly.
For other files, propose changes that import env from src/config/env.ts.
Do not print secrets in logs, errors, or test snapshots.
3. Redact Before Logging
Secret leaks often happen outside Git: CI logs, debug output, error tracking, screen recordings, or pasted prompts. Add a small redaction utility and use it whenever configuration must be printed.
// 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));
Redaction is a backstop, not permission to log secrets casually. The best log is one that never includes the secret in the first place.
4. Inject Values in CI/CD
GitHub Actions can pass repository, environment, or organization secrets to a workflow. Do not reuse production credentials for normal pull request tests; create smaller CI-specific values.
# .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
The trap is assuming every workflow receives secrets. Forked PRs, reusable workflows, and automated events can behave differently. Keep the validation job explicit and avoid writing secrets into generated files.
5. Deploy Notes: Docker, Vercel, Cloudflare
For Docker, do not write real secrets into Dockerfile with ENV. Local testing can use an env file, while production should use the orchestrator or secret store available in your runtime.
# local only
docker run --rm --env-file .env.local my-app:latest
If your runtime exposes secrets as files, support the NAME_FILE convention:
// 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();
}
On Vercel, separate Production, Preview, and Development values. Be extra careful with client-exposed prefixes such as NEXT_PUBLIC_. On Cloudflare Workers, values are provided through Worker bindings, the env parameter, or platform secrets depending on your setup. Do not overfit to one provider; make your schema the source of truth and keep platform-specific injection documented.
Use this Claude Code prompt for deployment review:
Review the Vercel, Cloudflare, and Docker environment setup.
Do not read or request real production values.
Check required key names, public vs secret keys, build-time vs runtime usage, and missing rotation notes.
6. Write a Rotation Playbook
Secret rotation should not start during an incident. Write the playbook first:
- Identify scope: service, environment, permissions, and owner
- Create the new value with minimum necessary permissions
- Add it as
*_NEXTwhere dual running is possible - Deploy code that accepts both old and new values for a short window
- Switch traffic and verify health checks
- Revoke the old value
- Search Git history, CI logs, error logs, and prompts for exposure
- Update
.env.exampleand operational notes
Webhook secrets, API keys, and database passwords have different rotation mechanics. Document the exact owner and rollback path for each one.
Common Failures
| Failure | Cause | Fix |
|---|---|---|
.env committed | .gitignore was added too late | Revoke keys immediately; history cleanup is not enough |
Secret placed in NEXT_PUBLIC_ | Public prefix misunderstood | Separate public and private naming conventions |
console.log(process.env) | Debugging under pressure | Use redaction and log review |
| Production boot fails | Required key missing | Run src/config/env.ts in CI before deploy |
| Local value used in production build | Build-time and runtime env mixed | Document platform-specific injection |
| Real key pasted into Claude Code | Prompting confused with secret sharing | Share key names and validation rules only |
Copy-Paste Claude Code Prompt
Implement environment variable management for this project.
Requirements:
- Create .env.example and .env.production.example
- Keep .env, .env.*, and .dev.vars* out of Git
- Add a Zod schema in src/config/env.ts and fail fast on missing or invalid values
- Centralize direct process.env reads in src/config/env.ts
- Redact secrets in any diagnostic logs
- Add a GitHub Actions job that runs env validation on pull requests
- Add short deployment notes for Vercel, Cloudflare, and Docker
Do not read real API keys or production database URLs. Work only from key names and validation rules.
Conclusion
The right way to use Claude Code for environment management is not to paste secrets into the chat. Define the contract in .env.example, validate it with Zod, redact diagnostics, inject real values through CI/CD and deployment secrets, and keep a rotation playbook ready.
ClaudeCodeLab offers Claude Code consulting, team training, repository security reviews, and implementation templates for authentication, payments, CI/CD, and content operations. If your team wants Claude Code to move faster without leaking production keys, this is one of the first systems worth standardizing.
In Masa’s test repository, this pattern caught three issues before deploy: a missing production key, a webhook secret that would have appeared in logs, and a stale .env.example. The Zod boot check is simple, but it turns configuration from tribal knowledge into an enforceable contract.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Safety Ladder: Expand Access Without Losing Control
A beginner-friendly ladder for moving Claude Code from read-only to limited edits, proof commands, and deploy checks.
Claude Code Small PR Proof Pack: Make Tiny Changes Reviewable
A practical proof pack for Claude Code PRs: diff, checks, public URL, CTA path, and rollback note.
Claude Code Review Gate Before Commit: Diff, Tests, Public URL, and CTA Checks
A commit-time review gate for Claude Code work: diff scope, build, public URL, revenue CTA links, missing tests, and unrelated files.
Related Products
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.