Two failures come from the same root cause, and they are not equally bad.
A test key in production is loud. Requests fail, monitoring fires, someone fixes it within minutes. Embarrassing, quickly resolved.
A production key in staging is silent. Everything works. Your integration tests charge real cards, your load test sends real emails, your seed script writes to the live database. Nobody notices until the invoice arrives or a customer asks why they received 400 identical notifications.
The second one is why this check exists.
What the format tells you
Some providers make the environment explicit and machine-readable:
| Provider | Live | Test |
|---|---|---|
| Stripe | sk_live_, pk_live_, rk_live_ | sk_test_, pk_test_ |
| AWS | AKIA (long-lived IAM) | ASIA (temporary STS) |
| Clerk | sk_live_ | sk_test_ |
| Shopify | shpat_ (no test marker) | — |
Many do not. GitHub tokens, OpenAI keys, Slack tokens and most internal credentials carry no environment marker at all. For those, the tool reports "indeterminate" rather than inventing an answer.
Make it structural, not vigilant
A check you have to remember to run is a check that will be skipped. Enforce the invariant in code instead:
// Fail at startup, not at the first charge.
const key = process.env.STRIPE_SECRET_KEY;
if (!key) {
throw new Error("STRIPE_SECRET_KEY is not set");
}
const isLiveKey = key.startsWith("sk_live_");
const isProduction = process.env.NODE_ENV === "production";
if (isLiveKey !== isProduction) {
throw new Error(
`Refusing to start: ${isLiveKey ? "live" : "test"} key in ` +
`${isProduction ? "production" : "non-production"} environment`
);
}Six lines, checked at boot, and the class of incident disappears.
Defence beyond the prefix
- Separate the accounts. Staging should use a different provider account, not a different key on the same one. Then a mistake cannot reach production data at all.
- Restrict by IP. A production key that only works from production egress addresses is useless in a developer's terminal.
- Scope the test key down. If staging never issues refunds, its key should not be able to.
- Name the variable for the environment.
STRIPE_LIVE_SECRET_KEYis harder to paste into the wrong file thanSTRIPE_KEY.
Frequently asked questions
The tool says indeterminate. What now?
Check the provider's dashboard, which is authoritative. Then adopt a naming convention in your own configuration so the ambiguity does not recur — the environment belongs in the variable name even when the provider does not put it in the key.
Can I just check the key length?
No. Live and test keys from the same provider are almost always the same length, because they are generated by the same code path with a different prefix.
What about keys that are neither live nor test?
Some providers issue restricted or read-only keys that are live but scope-limited — Stripe's rk_live_ is one. Those are classified as live here, correctly: they operate against real data. Use the blast radius analyzer to reason about how much a restricted key can actually do.
Should staging and production keys ever be in the same file?
No. Not in the same .env, not in the same secrets manager path, not in the same password manager item. The .env file generator emits per-environment files for this reason.