SOPs
Env Files — `.env` and `.env.example`
The Rule
Section titled “The Rule”Every repo that reads configuration uses two files:
.env— the real values. Gitignored. Lives on each machine. Never leaves the machine it was created on..env.example— the shape of.envwith safe placeholders. Committed. The canonical reference for “what keys does this app need?”
No exceptions. Websites, CLIs, Chrome extensions, cron scripts, daemons, scrapers, agents — if it reads a config value, it reads it from .env, and its sibling .env.example is checked in.
Why this matters
Section titled “Why this matters”.env.exampleis the self-documenting contract. Anyone (future me, a fresh machine, a second Claude session) can clone the repo and know exactly which keys to fill in. No separate “setup notes” that go stale..envnever hits GitHub. That is the single most common way secrets leak. Gitignore is non-negotiable and the pattern enforces it by making the real file invisible to git by default.- New machines bootstrap deterministically.
cp .env.example .env && $EDITOR .env. That is the entire setup step on the Mac Studio or VPS after agit clone. - Diffs stay clean. When a new env var is added, only
.env.examplechanges in the commit. The values live outside git history forever.
Conventions
Section titled “Conventions”.env.example
Section titled “.env.example”- One key per line.
KEY=placeholder. - Placeholders describe the shape, never leak a real value.
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, not the actual key. - Group related keys with blank lines and a
# Comment headerabove each group. - Mark optional keys with
# optionalat end of line. - Order keys by “must set to run” first, “nice to have” last.
Example:
# Resend — required for error alertsRESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxRESEND_FROM=alerts@example.com
# Google OAuth — required for Gmail + GSCGOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.comGOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx
# Debug toggles — optionalDEBUG=false # optionalLOG_LEVEL=info # optional, one of: debug|info|warn|error- Never committed. Add
.envto.gitignoreon the first commit of every repo. - Mode
600on any shared machine:chmod 600 .env. - If a key is used in only one environment (local dev vs VPS), keep two files:
.envon each machine with only the values it needs. Do not commit environment-specific variants.
Gitignore baseline
Section titled “Gitignore baseline”Every .gitignore must include:
.env.env.local.env.*.localThe extra patterns catch framework-specific variants (Next.js uses .env.local, etc.). .env.example is NOT ignored — it must be committed.
Loading the values
Section titled “Loading the values”- Node / Next.js / Astro: Next and Astro load
.envautomatically. For raw Node scripts, useimport 'dotenv/config'at the top of the entry file. - Python:
from dotenv import load_dotenv; load_dotenv()at the top of the entry module. Package:python-dotenv. - Bash / cron scripts:
set -a; source ./.env; set +aat the top. Theset -aauto-exports every var so child processes see them. - Chrome extensions: see the Chrome extension caveat below.
Chrome extensions — build-time injection
Section titled “Chrome extensions — build-time injection”Chrome extensions cannot read .env at runtime — the browser has no process.env. The .env / .env.example pattern still applies, but the values are baked into the extension at build time.
Two options:
-
Config file generated by
bump-build.sh(preferred). The build script reads.envand writesextension/config.js:Terminal window # in bump-build.sh, after the manifest rewritepython3 -c "import osfrom dotenv import load_dotenvload_dotenv('$REPO_DIR/.env')with open('$REPO_DIR/extension/config.js', 'w') as f:f.write('export const CONFIG = ' + repr(dict(os.environ)) + ';\n')"Then
extension/config.jsis gitignored (alongside.env). Extension code doesimport { CONFIG } from './config.js';. -
Hardcoded in the logger/background file if there is only one value (e.g., the localhost log receiver URL). Still put the value in
.env.exampleas documentation even if the live code does not read it yet — future-me will thank present-me.
Either way: no secrets live in committed extension code. Treat extension/config.js exactly like .env — gitignored, regenerated on build.
Shared secrets vault
Section titled “Shared secrets vault”The canonical source of truth for credentials that multiple apps reuse (Resend, Google OAuth, Claude API, etc.) is ~/apps/cc/shared-secrets.env. Individual app .env files either:
- Reference it via
source ~/apps/cc/shared-secrets.envat the top of the local.env(for bash-loaded apps), OR - Copy the specific key needed into the local
.env(for apps where source-importing is awkward).
The shared vault itself is gitignored and lives only on machines that need it. tms-internal has a credentials map page (pointers only, never values) for finding things.
Bootstrap checklist — every new repo
Section titled “Bootstrap checklist — every new repo”In order, at the start of every new app:
- Add
.envto.gitignorebefore the first commit. Include the variant patterns above. - Create
.env.examplewith every key the app needs, safe placeholders, and grouped comments. Commit it in the first commit. - Create
.envlocally by copying:cp .env.example .env. Fill in real values. This file stays local forever. - Wire up the loader in the entry point (
dotenv/config,load_dotenv(), orsource). - Verify the app fails loudly if a required key is missing. Do not silently fall back — crash with
Missing env var FOOso the operator knows to fill.env.
Adding a new env var to an existing app
Section titled “Adding a new env var to an existing app”- Add the key to
.env.examplewith a placeholder and a comment explaining what it is. - Add the real value to your local
.env(and to~/apps/cc/shared-secrets.envif it is shared). - Propagate to every other machine that runs this app — Mac Studio, VPS, etc. The
post-push-sync.shscript handles repo files, but.envis NOT synced (by design). Edit.envon each machine manually. - Commit
.env.exampleasBuild X: add FOO env var for Y feature. - Read the key in code using the app’s standard loader — no ad-hoc
os.environ.get("FOO")buried in random files. Centralize in aconfig.py/config.jsmodule.
Never commit
Section titled “Never commit”.env.env.local,.env.production,.env.anything.localextension/config.js(Chrome extensions)- Anything with a real API key, password, or token in it
If one slips through, rotate the key immediately — git history is forever. Then purge the file with git filter-repo or bfg and force-push. Rotation first, purge second. Assume the key was leaked the second it hit the public commit, even if the repo is private.
Related
Section titled “Related”- How We Build Anything — the umbrella build SOP; this page is the config layer of that checklist
- Chrome Extension SOP — extension-specific build and load flow
- Infrastructure Overview — the credentials map (pointers only)