Tips & Tricks (Updated: 6/2/2026)

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.

Claude Code Environment Variables Guide: .env, Zod, Secrets, and Production Deploys

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 caseTypical valuesFailure mode
Local developmentAPP_ORIGIN, DATABASE_URL, ANTHROPIC_API_KEYThe app works only on one developer’s machine
Webhook verificationSTRIPE_WEBHOOK_SECRET, WEBHOOK_SECRETUnsigned or forged requests are accepted
CI testingCI_DATABASE_URL, TEST_API_KEYPull requests pass but deployment fails
Production deployDATABASE_URL, SESSION_SECRET, APP_ORIGINThe app connects to the wrong DB or leaks credentials
Secret rotationANTHROPIC_API_KEY_NEXTOld 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:

  1. Identify scope: service, environment, permissions, and owner
  2. Create the new value with minimum necessary permissions
  3. Add it as *_NEXT where dual running is possible
  4. Deploy code that accepts both old and new values for a short window
  5. Switch traffic and verify health checks
  6. Revoke the old value
  7. Search Git history, CI logs, error logs, and prompts for exposure
  8. Update .env.example and 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

FailureCauseFix
.env committed.gitignore was added too lateRevoke keys immediately; history cleanup is not enough
Secret placed in NEXT_PUBLIC_Public prefix misunderstoodSeparate public and private naming conventions
console.log(process.env)Debugging under pressureUse redaction and log review
Production boot failsRequired key missingRun src/config/env.ts in CI before deploy
Local value used in production buildBuild-time and runtime env mixedDocument platform-specific injection
Real key pasted into Claude CodePrompting confused with secret sharingShare 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.

#Claude Code #environment variables #secrets #Zod #CI/CD #TypeScript
Free

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.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.