The gist
A .env file is the key ring to your house. You NEVER leave it on the front porch, which means you never publish it on GitHub. You keep the original in a safe (1Password) and hand a copy only to a system you trust (Cloudflare Secrets). This lesson is about handling API keys properly so you don't lose money or reputation.
Key concepts
- .env file: a file with environment variables. It keeps secrets on your computer and never goes into Git
- process.env: how code reads variables from .env in Node.js (in Python it's
os.environ) - wrangler secret: secure secret storage in Cloudflare Workers for production
- 1Password: a password manager that keeps all your API keys encrypted
- .gitignore: the list of files Git ignores (
.envgoes there) - Dev vs Prod secrets: separate keys for development and production (different limits and access rights)
- Key rotation: replacing keys regularly as a security measure
Theory
Why it matters: the cost of a mistake
Typical stories from forums and GitHub Issues:
- A developer accidentally committed an AWS key → bots find keys like that within minutes → a bill for thousands of dollars arrives overnight
- An Anthropic key in a public repo → someone burned through the whole limit over a weekend → the project went down
- A Google API key with no restrictions → spam bots used it for attacks
A key in a public GitHub repo is money lying on the sidewalk. Someone will pick it up.
How safe key handling is structured
Three levels of storage:
1Password (master storage)
↓ you copy it manually
.env (local development)
↓ Claude Code reads it through process.env
↓ does NOT go into Git (.gitignore)
↓ when you deploy
Cloudflare Secrets / wrangler secret (production)The single source rule: 1Password is the only place where originals are stored. Everything else is a temporary copy.
Step 1: Set up .gitignore correctly
Before you create anything else, create .gitignore in the project root:
# Secrets: NEVER in Git
.env
.env.local
.env.*.local
.env.production
# Logs
logs/
*.log
npm-debug.log*
# Dependencies
node_modules/
__pycache__/
*.pyc
# System
.DS_Store
.cursor/Check that .env is ignored:
git status
# .env should not show up in the file listIf .env has already ended up in Git (a mistake):
git rm --cached .env
git commit -m "Remove .env from tracking"
# Replace every key that was in this file!What a real .env setup looks like in a typical project
Here is how environment variables are actually organized in a working project:
project-root/
├── .env ← Real keys (NOT in Git!)
├── .env.example ← Template without values (in Git)
├── .env.test ← Mock keys for tests (not in Git)
├── .gitignore ← Contains .env, .env.local, .env.*.local
├── validate-env.js ← Script that checks the keys are present
└── wrangler.toml ← Cloudflare Workers config (new Cloudflare projects create wrangler.jsonc; toml is still supported). Prod secrets go through wrangler secretStep 2: Create .env.example (a template without values)
This file goes into Git. It shows which variables are needed, but without real values:
# .env.example — COMMIT THIS FILE
# === Anthropic ===
# Get it: platform.claude.com → Settings → API Keys → Create Key
ANTHROPIC_API_KEY=sk-ant-your-key-here
# === Perplexity (for search) ===
# Get it: perplexity.ai → Settings → API
PERPLEXITY_API_KEY=pplx-your-key-here
# === Gmail API ===
# Get it: Google Cloud Console → Credentials → OAuth 2.0
GMAIL_CLIENT_ID=your-client-id.apps.googleusercontent.com
GMAIL_CLIENT_SECRET=GOCSPX-your-secret
GMAIL_REFRESH_TOKEN=1//your-refresh-token
# === Google Sheets ===
# The ID from the sheet URL: docs.google.com/spreadsheets/d/THIS-IS-ID/edit
GOOGLE_SHEETS_ID=your-spreadsheet-id
# === App settings ===
NODE_ENV=development
LOG_LEVEL=infoThen copy it to a real .env and fill in the values:
cp .env.example .env
# Open .env and replace every "your-key-here" with real keysStep 3: Read the variables in code
Node.js / JavaScript:
// Install the package: npm install dotenv
require('dotenv').config();
// Read the variables
const anthropicKey = process.env.ANTHROPIC_API_KEY;
const sheetsId = process.env.GOOGLE_SHEETS_ID;
// Check they exist before starting
function validateEnv() {
const required = ['ANTHROPIC_API_KEY', 'GMAIL_CLIENT_ID'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing required env vars: ${missing.join(', ')}`);
}
}
validateEnv(); // Call it when the app startsPython:
import os
from dotenv import load_dotenv
load_dotenv() # pip install python-dotenv
anthropic_key = os.environ.get('ANTHROPIC_API_KEY')
if not anthropic_key:
raise ValueError("ANTHROPIC_API_KEY not found in .env")What you should NEVER do:
// ❌ Don't do this: the key is visible in the code
const client = new Anthropic({ apiKey: "sk-ant-abc123..." });
// ✅ Do this instead
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });Step 4: Dev vs Prod, separate keys for separate environments
Why separate keys:
- The prod key has a high limit → an accidental bug during development gets expensive
- A dev key can be revoked easily without touching production
- Different access rights (dev: read-only, prod: full)
File layout:
.env ← local development (not in Git)
.env.test ← for tests (can use mock keys, not in Git)
.env.production ← not used directly (secrets live in Cloudflare)Switching environments:
const env = process.env.NODE_ENV || 'development';
console.log(`Running in ${env} mode`);
// development → reads .env
// production → secrets come through the worker's env object (see step 5)Step 5: Cloudflare Secrets for production
When you deploy to Cloudflare Workers, you do NOT upload .env. You use wrangler secret:
# Set one secret (wrangler will ask for the value interactively)
wrangler secret put ANTHROPIC_API_KEY
# List all secrets (shows only names, not values)
wrangler secret list
# Delete a secret
wrangler secret delete OLD_API_KEYOnce it's set through wrangler secret, you read it inside the Cloudflare Worker like this:
// In a Cloudflare Worker, env is a special object
export default {
async fetch(request, env) {
const key = env.ANTHROPIC_API_KEY; // Not process.env!
// ...
}
};Step 6: 1Password as master storage
1Password holds the originals of all your keys. A structure that works:
1Password → AI Projects (a separate vault)
├── Newsletter Automation
│ ├── Anthropic API Key (prod)
│ ├── Anthropic API Key (dev)
│ ├── Perplexity API Key
│ └── Gmail Credentials
├── Lead Gen Project
│ └── ...
└── Shared Infrastructure
├── Cloudflare API Token
└── GitHub TokenHow to use the 1Password CLI to fill in keys automatically:
# Install: 1password.com/downloads/command-line
op signin
# Fill in .env from 1Password automatically
op inject -i .env.example -o .envFor this to work, you put 1Password references in .env.example:
ANTHROPIC_API_KEY=op://AI-Projects/Newsletter/api-keyKey rotation: when and how
When to replace keys:
- Someone left the team
- You suspect a leak
- Regularly, every 3-6 months (good practice)
- After any incident
The procedure:
- Create a new key in the service's console
- Update it in 1Password
- Update it in Cloudflare with
wrangler secret put KEY_NAME - Test that production works
- Revoke the old key
Never revoke the old key first. Add the new one first, check it, then remove the old one.
Practice
Assignment: a safe environment setup for Newsletter Automation
Create a project folder and initialize Git:
bash mkdir newsletter-automation && cd newsletter-automation git initCreate
.gitignore(copy the template from this lesson)Create
.env.examplewith all the variables you need (no values)Copy it to
.envand fill in at leastANTHROPIC_API_KEY:bash cp .env.example .envWrite
validate-env.js, a script that checks all the required variables are set:bash node validate-env.js # Should print: ✅ All required env vars are setMake your first commit and make sure
.envisn't in the list:bash git add . git status # .env should not be in the list git commit -m "Initial setup with env template"(Optional) Open 1Password, create a separate vault called "AI Projects" and add your Anthropic key
Goal: A working environment where no key ever goes into Git, while all the code reads keys through process.env.
Tools and resources
- Claude Console: create and manage Anthropic API keys
- 1Password: a password manager with a CLI and team sharing
- 1Password CLI: fills in
.envfrom your vault automatically - dotenv (npm):
npm install dotenv, loads .env in Node.js - python-dotenv (pip):
pip install python-dotenv, loads .env in Python - Cloudflare Workers Secrets: secure secret storage in production
- wrangler:
npm install -g wrangler, the CLI for Cloudflare Workers and secrets - git-secrets: a pre-commit hook that blocks commits containing keys
- gitleaks: a scanner that finds leaked secrets in Git repositories
Common mistakes
Mistake 1: Hardcoding a key "just for a minute" "I'll test it real quick and take it out later." You forget, you commit, and the key is in your Git history forever. Even if you delete the file, it stays in the commit history. The rule: no keys in code, ever, not even for a second.
// ❌ NEVER, not even "temporarily"
const client = new Anthropic({ apiKey: "sk-ant-abc123..." });
// ✅ ALWAYS through an environment variable
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });Mistake 2: One key for dev and prod A bug in a test script used up the production key's limit, and production went down. Always use two keys: dev with a low limit, prod with the full one.
Mistake 3: Forgetting .gitignore before the first commit .env ended up in the first commit. Now, even after git rm --cached .env, the key stays in the history. The only fix is to replace every key from that file.
Related lessons
- Building your first workflow LIVE: putting
.envto work while building a newsletter - Deployment: Cloudflare Workers: how to move secrets from
.envto Cloudflare Workers withwrangler secret - Permissions and security: advanced practices for access control and key rotation
Key takeaways
A .env file is your local key ring. Only
.env.example, a template without values, goes into Git.
Never hardcode keys into code. Not even in private repos: a repo can become public, or someone can get access to it.
Dev and prod use different keys. A mistake during development shouldn't cost money or break production.
1Password is the single source of truth. Every other storage place holds a temporary copy.
Next lesson
The mark stays in this browser only and is never sent anywhere. My progress