style(no-slop): remove every em-dash + banned words across all modules + capstone

Apply the no-ai-slop standard (now binding in AGENTS.md): the em-dash character is
banned outright (restructured, not blind-replaced), plus the banned word/phrase
list (delve, leverage, robust, seamless, truly, unlock, etc.). 0 em-dashes remain
in modules + capstone; the only "robust" left is the planted M10 ai-change.patch
trap. Module H1 titles use a colon separator.

All deliberate teaching devices preserved; labs compile/parse (py/sh/yaml/json);
no junk. AGENTS.md updated with the hard no-slop rules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
This commit is contained in:
2026-06-22 23:21:09 -04:00
parent 513d7e7ac8
commit 389ac2e460
99 changed files with 1324 additions and 1315 deletions
@@ -1,4 +1,4 @@
# Module 17 Secrets, Config, and Environments
# Module 17: Secrets, Config, and Environments
> **Ask an AI to "connect to the API" and it will paste your secret key straight into a source
> file, the one place it must never go.** This module gives you the standard, boring, correct
@@ -9,14 +9,14 @@
## Prerequisites
- **Module 2 Version Control as a Safety Net.** You need `.gitignore` and the habit of reading
- **Module 2: Version Control as a Safety Net.** You need `.gitignore` and the habit of reading
`git diff` before you commit. Both matter here.
- **Module 12 Revert, Reset, and Recovery.** You learned that Git history is forever and that
secrets *don't belong in it* this module is the practical follow-through on that promise.
- **Module 15 Security Scanning for AI-Generated Code.** Secret scanning is the automated gate
- **Module 12: Revert, Reset, and Recovery.** You learned that Git history is forever and that
secrets *don't belong in it*; this module is the practical follow-through on that promise.
- **Module 15: Security Scanning for AI-Generated Code.** Secret scanning is the automated gate
that catches a hardcoded key after the fact. This module is the *prevention* that means the gate
rarely has to fire.
- **Module 16 Containers and Reproducible Environments.** A container is a sealed box; config and
- **Module 16: Containers and Reproducible Environments.** A container is a sealed box; config and
secrets are how you pass the outside world *into* it at run time. That handoff is environment
variables, which is exactly what this module is about.
@@ -34,7 +34,7 @@ By the end of this module you can:
`.env` file), and have the app read it back at run time.
3. Keep config you *can* commit (a committed template) separate from secrets you *can't* (the real
`.env`), so a teammate or a fresh AI session knows exactly what to supply.
4. Apply the 12-factor rule *config lives in the environment, not the build* to run one codebase
4. Apply the 12-factor rule (*config lives in the environment, not the build*) to run one codebase
unchanged across dev, staging, and prod.
5. Describe what a secrets manager buys you over `.env` files, in vendor-neutral terms, and know
when you've outgrown a file on disk.
@@ -70,7 +70,7 @@ rest of this module:
| Kind | Example | Where it lives | Goes in Git? |
|------|---------|----------------|--------------|
| **Code** | The logic of your app | Source files | **Yes** that's the point |
| **Code** | The logic of your app | Source files | **Yes**, that's the point |
| **Config** | Which backend URL, log level, feature flags, timeouts | The environment (often a `.env` *template* you commit + real values you don't) | The *template* yes, the *values* it depends |
| **Secrets** | API keys, passwords, tokens | The environment, sourced from a secret store in real deployments | **Never** |
@@ -129,7 +129,7 @@ Two non-negotiable rules come with it:
most important line in this module:
```gitignore
# secrets and local config never commit
# secrets and local config, never commit
.env
.env.*
!.env.example
@@ -164,7 +164,7 @@ The principle behind all of this comes from the [12-factor app](https://12factor
and factor III states it plainly: **store config in the environment.** The payoff for this audience:
> You build the artifact **once** and run the *same* artifact in every environment. Nothing about
> dev, staging, or prod is baked into the code or the container image the differences are injected
> dev, staging, or prod is baked into the code or the container image; the differences are injected
> at run time as environment variables.
This is why it pairs so tightly with containers (Module 16). A container image is your immutable,
@@ -184,9 +184,9 @@ promote one artifact through environments instead of rebuilding per stage.
"Environments" here means the distinct places your code runs, each with its own config and its own
secrets. The standard three:
- **dev** your machine. A dev backend, a dev key with low privileges, verbose logging.
- **staging** a production-like rehearsal. Separate backend, separate key, real-ish data.
- **prod** the real thing. Real users, the powerful key, conservative settings.
- **dev**: your machine. A dev backend, a dev key with low privileges, verbose logging.
- **staging**: a production-like rehearsal. Separate backend, separate key, real-ish data.
- **prod**: the real thing. Real users, the powerful key, conservative settings.
The rule that catches people: **each environment gets its own secrets, and they never mix.** A dev
key must not be able to touch prod data, and a prod key must never sit in a developer's `.env`. The
@@ -217,8 +217,8 @@ reasons that show up fast in real operations:
- A plaintext file on a server is readable by anything that compromises that box.
- You can't **rotate** a key across fifty machines by editing fifty files.
- You get no **audit trail** no record of who read which secret when.
- There's no **access control** "this service can read the DB password but not the signing key."
- You get no **audit trail**: no record of who read which secret when.
- There's no **access control**: "this service can read the DB password but not the signing key."
A **secret manager** (also called a secrets store or vault, categorically) solves these. It's a
dedicated service that stores secrets encrypted at rest, hands them out only to authenticated
@@ -226,12 +226,12 @@ callers, logs every access, and supports rotation and fine-grained access polici
app (or the platform it runs on) fetches the secret from the manager into memory instead of reading
a file. The categories you'll encounter:
- **Cloud-provider managers** every major cloud has one, tightly integrated with that cloud's
- **Cloud-provider managers**: every major cloud has one, tightly integrated with that cloud's
identity system.
- **Standalone / self-hostable vaults** dedicated secret-management products you run yourself, a
- **Standalone / self-hostable vaults**: dedicated secret-management products you run yourself, a
good fit for the on-prem and air-gapped scenarios this audience often lives in (the same
self-host instinct from Module 8).
- **Platform-native secrets** your container orchestrator and your CI/CD system both have a
- **Platform-native secrets**: your container orchestrator and your CI/CD system both have a
built-in concept of "secrets" you can inject as environment variables, which is how secrets reach
a pipeline (Module 14) or a deployment (Module 18) without ever touching the repo.
@@ -291,7 +291,7 @@ type the commands by hand. Then you'll make it select config per environment.
- The starter files in this module's `lab/starter/`: `sync.py` (the before) and `.env.example`.
- Claude Code in your terminal (`claude --version` to confirm it's installed; sub your own agent).
### Part A See the smell
### Part A: See the smell
1. Copy `lab/starter/sync.py` and `lab/starter/.env.example` into your `tasks-app` folder, then run
the before-picture:
@@ -306,7 +306,7 @@ type the commands by hand. Then you'll make it select config per environment.
this getting committed and pushed: the key is now in history forever (Module 12) and a secret
scanner (Module 15) would light up, if you were lucky enough to have one.
### Part B Gitignore the secret *first*
### Part B: Gitignore the secret *first*
2. Before any real secret exists, close the door. Tell Claude Code (sub your own agent) to set up
the ignore rules:
@@ -319,7 +319,7 @@ type the commands by hand. Then you'll make it select config per environment.
(ignore the secret before the secret exists). The rules should land like this:
```gitignore
# secrets and local config never commit
# secrets and local config, never commit
.env
.env.*
!.env.example
@@ -334,7 +334,7 @@ type the commands by hand. Then you'll make it select config per environment.
If `.env` shows up in `git status`, the ignore rule is wrong; have the agent fix it before going
further. This verification is the step that prevents the leak.
### Part C Refactor the secret into the environment
### Part C: Refactor the secret into the environment
4. Now move the secret and the environment-specific URL out of the code. Ask Claude Code (sub your
own agent):
@@ -353,7 +353,7 @@ type the commands by hand. Then you'll make it select config per environment.
from pathlib import Path
def load_dotenv(path: Path) -> None:
"""Minimal .env loader no dependency. Real projects use a library for this."""
"""Minimal .env loader, no dependency. Real projects use a library for this."""
if not path.exists():
return
for line in path.read_text().splitlines():
@@ -393,7 +393,7 @@ type the commands by hand. Then you'll make it select config per environment.
stomp on what's already in the environment. If the AI hands you plain assignment, that's the
correction to make.
### Part D Run it from the environment
### Part D: Run it from the environment
5. Run it reading from your `.env`:
@@ -420,7 +420,7 @@ type the commands by hand. Then you'll make it select config per environment.
set:** it's using `os.environ[key] = value` where it needs `os.environ.setdefault(...)` (see
Part C). Fix the loader so the command line wins, and the override takes effect.
### Part E Commit, and verify the secret didn't tag along
### Part E: Commit, and verify the secret didn't tag along
7. Have the agent commit the refactor, then **read the diff yourself before you accept it** (the
review reflex from the AI angle). Tell Claude Code (sub your own agent):
@@ -498,7 +498,7 @@ publishing:
products. If you add specific product names, re-verify each still exists, is current, and
isn't pinned as *the* answer (vendor-neutral rule, AGENTS.md).
- [ ] **Re-check the 12-factor reference.** Confirm the [12factor.net](https://12factor.net) link
resolves and that "factor III config" is still phrased as "store config in the environment."
resolves and that "factor III, config" is still phrased as "store config in the environment."
- [ ] **Re-verify `.gitignore` negation behavior.** Confirm `!.env.example` still un-ignores the
template under the `.env.*` rule with a current Git, and that `git status` behaves as the lab
claims.
@@ -1,4 +1,4 @@
# .env.example the TEMPLATE you DO commit.
# .env.example: the TEMPLATE you DO commit.
#
# This file documents which variables the app needs, with no real values. Teammates (and the
# next AI session) copy it to a real `.env`, fill in the secrets, and never commit that copy.
@@ -1,4 +1,4 @@
"""A 'sync' command for the tasks-app the BEFORE picture for Module 17.
"""A 'sync' command for the tasks-app: the BEFORE picture for Module 17.
This is exactly the kind of file an AI hands you when you ask it to "add a command that syncs
tasks to our backend." It works. It also has two AI-classic mistakes baked in:
@@ -8,7 +8,7 @@ tasks to our backend." It works. It also has two AI-classic mistakes baked in:
prod at the prod one without editing code.
Your job in the lab is to refactor BOTH out of the source and into the environment. Don't read
ahead and fix it yet first run it as-is so you can see the smell.
ahead and fix it yet; first run it as-is so you can see the smell.
Run it:
python sync.py