New: Try Voli The Bear, Fast package manager (and not only) for Windows
Updated Jul 10, 2026 Edit on GitHub

Keep Them Out of Code

In Phase 1 we landed on the rule: a secret is a house key, and the fastest way to lose it is to commit it. Now the practical question - where does the key go instead? The answer is a small, well-worn pattern that every professional project uses, and once you've set it up once, it becomes muscle memory. We'll build it up one layer at a time, then face the uncomfortable case: what to do when a secret is already in your history.

Layer 1: Move the secret out of the source

What it actually is. Instead of writing the key as a literal string in your code, you read it from the environment at runtime - the same NAME=value mechanism the operating system hands every process. Your code asks for STRIPE_SECRET_KEY by name; where that value comes from is decided outside the code.

The hardcoded version from Phase 1 becomes this:

   // payment.js  - the secret is no longer in the file
   const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);

Now the source code is safe to commit - it mentions the name of the secret, never the value. The value lives in the environment, and the environment is per-machine: your laptop, staging, and production each supply their own.

📝 If environment variables, process.env, and .env files are unfamiliar mechanics, the full walkthrough is in Environment Variables & Config - here we're focused only on their security.

Layer 2: The .env file - and keeping it out of Git

For local development you keep the actual values in a .env file, one per line:

STRIPE_SECRET_KEY=sk_test_51H8xK2eZvKYlo3a9qN...
DATABASE_URL=postgres://app:hunter2@localhost:5432/myapp_dev

This file holds real, live secrets. So the single most important rule in this whole phase: it must never enter Git. You tell Git to ignore it by adding one line to a .gitignore file in your project root:

.env

Then verify Git is actually ignoring it - don't assume, check:

$ git status
On branch main
nothing to commit, working tree clean

What just happened: Even though .env exists on disk with your real keys inside it, it doesn't appear in git status - the .gitignore entry told Git to treat it as if it weren't there, so you can't stage or commit it by accident. If .env had shown up in that list, it is not ignored yet - fix the .gitignore first.

⚠️ Gotcha - .gitignore only ignores files Git isn't already tracking. If you committed .env before adding it to .gitignore, the ignore rule does nothing - Git keeps tracking the file it already knows. Explicitly untrack it:

$ git rm --cached .env
rm '.env'
$ git commit -m "Stop tracking .env"

What just happened: git rm --cached removed .env from Git's tracking without deleting it from your disk (the --cached flag is the important part). From the next commit on, Git ignores it. But this does not undo the fact that the secret was already committed - more on that below.

Layer 3: .env.example - so teammates know what's needed

Since .env itself never gets committed, a new teammate cloning the repo has no idea which variables to set. The fix is a committed companion file, .env.example, listing every variable name with placeholder or blank values - never real ones:

STRIPE_SECRET_KEY=your_stripe_test_key_here
DATABASE_URL=postgres://user:password@localhost:5432/dbname

This file is safe to commit precisely because it contains no real secrets, just the shape of what's needed. The onboarding ritual becomes: copy .env.example to .env, fill in the real values you've been given. New developer productive in two minutes, zero secrets in the repo.

💡 Key point. The split is the whole trick: .env is real and ignored; .env.example is fake and committed. One tells your app what to do; the other tells your teammates what to fill in.

Layer 4: A pre-commit secret scanner - a net under the trapeze

The pattern above relies on you remembering. Humans forget - especially the day you paste a key "just to test something fast." A pre-commit secret scanner is an automated check that runs before each commit is recorded and refuses the commit if it spots something that looks like a secret.

Two common tools are git-secrets and gitleaks, often wired in via the pre-commit framework. The exact setup varies, but the experience looks like this:

$ git commit -m "Add payment retry logic"
gitleaks: detected hardcoded secret in payment.js
  Rule:    stripe-access-token
  File:    payment.js
  Line:    14
gitleaks detected 1 leak - commit aborted

What just happened: The scanner recognized the pattern of a Stripe key in payment.js and stopped the commit from being created at all. Nothing entered Git. You delete the line, move the value to your .env, and commit again - caught at the last possible safe moment.

This is a net, not a guarantee - scanners can miss novel patterns and occasionally flag harmless strings. But catching the obvious cases automatically, on every commit, dramatically lowers the odds of the classic mistake. Set it up once per repo and forget about it.

The hard truth: a committed secret lives in history forever

Now the part people most want to be untrue. Suppose you already committed a secret - maybe even pushed it. The instinct is to delete the line, commit the fix, and feel relieved. That relief is false.

Git doesn't store files as a single "current" state; it stores history - every commit is a snapshot, and old snapshots don't disappear when you change the latest one. Deleting the secret in a new commit just means the newest snapshot no longer has it. The commit where you added it is still right there, one step back in the log, secret intact - anyone can check out that older commit and read it.

And if you pushed, it's worse: the secret now exists in every clone anyone has made, and quite possibly in a scanning bot's database already. Even genuinely rewriting Git history to scrub it can't recall the copies already out in the world.

⚠️ Gotcha - removing the file is NOT enough. Rotate the secret. Because you cannot reliably un-leak a value that reached a repository, the only real fix is to rotate it: go to the service, revoke the old key, and issue a new one. Once the old key is dead, it doesn't matter who has the old value - it unlocks nothing. Rotation is the lock-change for a lost house key. (For why git revert and even force-pushing don't erase a pushed secret, see Git Disaster Recovery.)

🪖 War story. A teammate committed a cloud access key, noticed an hour later, and pushed a commit deleting the line - then went home feeling tidy. The key was scraped and used to spin up crypto-mining servers before morning, because the deletion commit changed nothing about the addition commit sitting one step back. The step skipped: revoke the key in the cloud console. Deleting the line treats the symptom; rotating treats the leak.

Recap

  1. Read secrets from the environment, never as literals in source: process.env.STRIPE_SECRET_KEY, not the key itself.
  2. Keep real values in a .env file and add .env to .gitignore - then run git status to confirm it's actually ignored.
  3. If .env was already committed, untrack it with git rm --cached .env (ignoring doesn't retroactively untrack).
  4. Commit a .env.example with placeholder values so teammates know what to set; never put real secrets in it.
  5. Add a pre-commit secret scanner (gitleaks, git-secrets) as an automated net that blocks the commit before a leak happens.
  6. ⚠️ A committed secret lives in Git history forever - deleting it from the latest commit is not enough. Rotate it: revoke the old value and issue a new one.

Next: how teams manage secrets centrally for production - with a cheat-card for the day one gets out.


← Phase 1: What Counts as a Secret · Guide overview · Phase 3: Real Secrets Management →

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. Where should a secret go instead of being a literal in your code?

2. What is the key split between `.env` and `.env.example`?

3. If you committed `.env` before adding it to `.gitignore`, what must you do?