Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when deciding where an application's configuration and secrets live and how the running code reads them: put every value that varies between deploys in its own environment variable — read at runtime, never a hard-coded constant, never a committed config file with credentials, never a DevelopmentConfig/ProductionConfig class switched on an environment name. Do NOT use for other twelve-factor factors (build/release/run, backing services, processes) or for values that are identical in every deploy.
.claude/skills/twelve-factor-config/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 1 |
| Model | Lift | Δ tokens | Δ turns | Cases | Verified |
|---|---|---|---|---|---|
| gemini-3.6-flashbest | +9% | +30% | 0% | 22 | 54d ago |
| gemini-3.5-flash | pending re-run | — | |||
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | — | — |
| case-17 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✓→✓ | = Same ✓ | — | — |
| case-02 | ✓→✓ | = Same ✓ | — | — |
| case-03 | ✗→✗ | = Same ✗ | — | — |
Anything that changes between one deploy and the next is stored in its own environment variable, read at runtime. Nothing that varies between deploys is a code constant, a committed file with secrets, or a named-environment switch. Apply this to how an app obtains configuration and credentials; it does not cover the other twelve factors.
each production deploy (a database URL, an API key, a hostname, a log level) is read from an environment variable at runtime. This is the whole rule; everything below is a consequence.
source file. os.environ["DATABASE_URL"], not DATABASE_URL = "postgres://…".
config.py, secrets.yaml, orconfig.json full of credentials into version control. The environment supplies them; the repo does not.
DATABASE_URL,REDIS_URL, STRIPE_KEY as separate variables. Not one packed blob, not values that only make sense together as a set.
DevelopmentConfig / StagingConfig /ProductionConfig classes, per-tier files, or if ENV == "production": switch blocks that select a bundle of values by an environment name. Each deploy sets its own variables directly; there is no named group to switch on. Adding a new deploy must need only new variable values, never a new class, file, or branch.
credential? If a checked-in file would leak a secret, it fails factor III.
size, a protocol constant, a timeout that never varies — is an ordinary code constant, not an environment variable. Factor III moves only what varies between deploys; it does not push invariant constants or app internals into the environment.
The base's default is on the left; the conforming form on the right.
Hard-coded connection string → environment variable:
python# BEFORE DATABASE_URL = "postgres://app:s3cret@db.internal:5432/app" # AFTER DATABASE_URL = os.environ["DATABASE_URL"] # set per deploy; no default, absence fails startup
A committed secrets file → read from the environment instead:
python# BEFORE (config.py, committed) STRIPE_KEY = "sk_live_51H..." SENDGRID_KEY = "SG.xxxx" # AFTER STRIPE_KEY = os.environ["STRIPE_KEY"] SENDGRID_KEY = os.environ["SENDGRID_KEY"]
The named-environment class ladder → flat, per-value variables:
python# BEFORE class ProductionConfig: DB = "postgres://prod…" DEBUG = False class StagingConfig: DB = "postgres://stg…" DEBUG = True CONFIG = ProductionConfig if os.environ["APP_ENV"] == "prod" else StagingConfig # AFTER DB = os.environ["DATABASE_URL"] DEBUG = os.environ.get("DEBUG", "false") == "true" # no APP_ENV switch; each deploy sets DATABASE_URL and DEBUG itself
A value that never varies stays a constant — do not environment-ize it:
python# CORRECT (unchanged) MAX_UPLOAD_BYTES = 10_485_760 # the same in every deploy → a plain constant, not an env var
.env for developer convenience → fine only if gitignored and the code still reads fromthe process environment (.env populates it); the code must not parse a committed file as its source of truth.
hard-coded default secret.
environment variable; "varies between deploys" is the test, not "is it secret."
magic number) → keep it a code constant; moving it to the environment is over-config.
parse a generated file full of secrets; injecting each value as a variable is the conforming path.
if ENV == ….config.py / secrets.yaml carrying real credentials.DevelopmentConfig / ProductionConfig classes, or if env == "production": blocks, grouping configby a named environment.
DevelopmentConfig/ProductionConfig class, per-tier file, or ENV == switch block.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.5-flash | verified | 7/10/2026 | +63% |
Other measured skills in the registry, with their headline benchmark lift.