Claude Code 环境变量管理指南:.env、Zod、Secrets 与生产部署
用 Claude Code 安全管理环境变量和 Secrets:.env.example、Zod 校验、CI/CD 注入、脱敏与轮换。
很多初学者让 Claude Code 做登录、支付、Webhook 或 AI 接口时,最早出问题的往往不是页面,而是配置。数据库 URL 少一个环境变量、把 staging key 用到了生产、Webhook secret 被打印进 CI 日志,或者真实 API key 被提交到 GitHub,都会让一个看似完成的功能变成安全事故。
本文给出一套可以直接落地的环境变量与 Secrets 管理模式。环境变量是运行时传给应用的配置值,例如 PORT、APP_ORIGIN。Secrets 是泄露后会被滥用的秘密值,例如 ANTHROPIC_API_KEY、DATABASE_URL、WEBHOOK_SECRET。Secrets 也常通过环境变量传入,但它们需要更严格的规则。
Claude Code 自身的配置请参考官方 Claude Code environment variables。应用侧校验用 Zod。CI/CD 与部署可对照官方文档:GitHub Actions secrets、Vercel environment variables、Cloudflare Workers variables and secrets 以及 Docker secrets。不同平台的界面不同,但原则一致:代码里保存“需要哪些 key”,不要保存“真实 secret 值”。
如果要从整体安全角度补强,可以继续阅读 Claude Code 安全最佳实践 和 Claude Code JWT 认证。
把 .env 当成契约,而不是私人备忘录
.env 很方便,但不能变成每个人自己的小纸条。团队至少需要三层机制:
- 声明:用
.env.example列出项目需要哪些 key - 校验:应用启动时用 Zod 检查必填、URL 格式、最小长度和默认值
- 运维:CI/CD 和生产环境从平台 Secrets 注入,不复制本地
.env
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
这张图的重点是:所有入口都经过同一个 Zod schema。让 Claude Code 帮你实现时,也只给它 key 名、校验规则和失败时的行为,不要把真实生产值贴到 prompt 里。
典型使用场景
| 场景 | 常见变量 | 出错后果 |
|---|---|---|
| 本地开发 | APP_ORIGIN, DATABASE_URL, ANTHROPIC_API_KEY | 只有某个人电脑能运行,别人无法复现 |
| Webhook 验签 | STRIPE_WEBHOOK_SECRET, WEBHOOK_SECRET | 接受伪造请求,订单或权限被篡改 |
| CI 测试 | CI_DATABASE_URL, TEST_API_KEY | PR 通过,但部署阶段失败 |
| 生产部署 | DATABASE_URL, SESSION_SECRET, APP_ORIGIN | 连错数据库、Cookie 域名错误、凭证泄露 |
| Secret 轮换 | ANTHROPIC_API_KEY_NEXT | 旧 key 泄露后长期有效,影响范围扩大 |
Masa 在 ClaudeCodeLab 的小型 SaaS 项目里实践后发现,最有价值的不是单纯创建 .env.example,而是让应用在配置不完整时直接启动失败。这样部署事故会提前变成 PR 阶段可以审查的问题。
1. 先拆分文件
.env.example 是文档,不放真实值。.env.local 只属于本机。.env.production.example 是生产配置清单,也不放真实生产值。
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
注意:replace-with-local-dev-key 不是安全默认值,只是提醒“这里需要一个值”。生产环境必须从部署平台或 Secrets 管理器注入。
2. 用 Zod 在启动时校验
环境变量进入 Node.js 后都是字符串。PORT 看起来是数字,但实际也是字符串,所以需要 z.coerce.number() 转换并校验。
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 ?? "",
};
}
本地检查:
cp .env.example .env.local
npx tsx src/config/env.ts
然后让 Claude Code 查找散落的 process.env:
请找出仓库里所有直接读取 process.env 的位置。
除了 src/config/env.ts 以外,其他文件都应改为从 env 对象读取。
不要把 secret 打印到日志、错误信息或测试快照里。
3. 日志必须脱敏
Secret 泄露不只发生在 Git。CI 日志、调试输出、错误监控、截图、甚至贴给 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));
脱敏只是最后一道保险。更好的做法是:日志设计时就不要把 secret 放进去。
4. CI/CD 从 Secrets 注入
GitHub Actions 可以把 repository、environment 或 organization secrets 注入 workflow。不要把生产凭证拿来跑普通 PR 测试,CI 应该使用权限更小、范围更窄的值。
# .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
常见误区是以为所有 workflow 都能拿到 secrets。Fork PR、reusable workflow、Dependabot 等场景可能不同。把验证 job 写清楚,不要把 secret 写入生成文件。
5. Docker、Vercel、Cloudflare 的边界
Docker 中不要在 Dockerfile 里写 ENV API_KEY=...。本地测试可以用 env file,生产则应使用运行平台或 secret store。
# local only
docker run --rm --env-file .env.local my-app:latest
如果平台把 secret 作为文件挂载,可以支持 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();
}
Vercel 需要区分 Production、Preview、Development;同时要注意 NEXT_PUBLIC_ 这类会暴露到浏览器的变量。Cloudflare Workers 通常通过 Worker binding、env 参数或平台 Secrets 提供值。不要把文章里的例子当成某个平台的唯一正确配置,真正的准绳是你的 schema 和官方文档。
请审查 Vercel、Cloudflare、Docker 的环境变量设置。
不要读取或索要真实生产值。
只检查必填 key、公开 key 与 secret key 的边界、build-time/runtime 的区别,以及是否缺少轮换说明。
6. 先写轮换 playbook
Secret 轮换不能等泄露之后才想。建议提前写好:
- 确认影响范围:服务、环境、权限、负责人
- 创建新值,尽量最小权限并设置有效期
- 可双写时先加
*_NEXT - 短时间内让应用同时接受新旧值
- 部署并检查健康状态
- 失效旧值
- 搜索 Git 历史、CI 日志、监控日志和 prompt 历史
- 更新
.env.example与运维文档
Webhook secret、API key、数据库密码的轮换方式不同。每一种都应写清负责人和回滚方式。
常见失败
| 失败 | 原因 | 对策 |
|---|---|---|
.env 被提交 | .gitignore 加得太晚 | 立即吊销 key,不能只清理 Git 历史 |
secret 放进 NEXT_PUBLIC_ | 不理解公开前缀 | 用命名规则区分 public/private |
console.log(process.env) | 临时调试太急 | 使用脱敏工具,并做日志审查 |
| 生产启动失败 | 平台缺少必填 key | CI 中先运行 src/config/env.ts |
| 本地值进了生产构建 | 混淆 build-time 与 runtime | 为每个平台写注入说明 |
| 把真实 key 贴给 Claude Code | 把实现请求当成 secret 分享 | 只给 key 名和校验规则 |
可直接使用的 Claude Code prompt
请为这个项目实现环境变量管理。
要求:
- 创建 .env.example 和 .env.production.example
- .env, .env.*, .dev.vars* 不进入 Git
- 在 src/config/env.ts 中用 Zod schema 校验必填和格式
- 把直接读取 process.env 的逻辑集中到 src/config/env.ts
- 诊断日志必须对 secret 脱敏
- 添加 GitHub Actions job,在 PR 时执行环境变量校验
- 为 Vercel、Cloudflare、Docker 写简短部署说明
不要读取真实 API key 或生产数据库 URL,只根据 key 名和校验规则工作。
总结
Claude Code 环境变量管理的重点不是把 secret 贴给它,而是让它实现一套可验证的契约:.env.example 声明 key,Zod 在启动时校验,日志脱敏,CI/CD 与生产平台负责注入真实值,并提前准备轮换流程。
ClaudeCodeLab 提供 Claude Code 导入咨询、团队培训、仓库安全审查,以及认证、支付、CI/CD、内容运营相关模板。如果你的团队想让 Claude Code 更快落地,同时避免生产 key 外泄,环境变量管理是最值得先标准化的部分之一。
Masa 在测试仓库中应用这套模式后,部署前拦下了三个问题:生产 key 缺失、Webhook secret 可能进日志、.env.example 已过期。Zod 启动校验很简单,但它能把“只有某个人知道的配置”变成可执行的团队契约。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code权限安全阶梯:逐步放开访问而不失控
从只读到有限编辑、验证命令和部署检查的 Claude Code 权限升级流程。
Claude Code 小PR证据包:让小改动真正可审查
用差异、验证命令、公开URL、CTA路径和回滚说明,把Claude Code的小PR变得可审查。
Claude Code 提交前 Review Gate:同时检查差异、测试、公开 URL 和 CTA
提交前用 Claude Code 审查差异范围、build、公开 URL、Gumroad 链接、咨询 CTA、缺少测试和无关文件。