Which OS environment variables reach Cypress's `cy.env()`, and how are their names changed?
answer
- Only one prefix is recognised
- The prefix comes off the name
- Some names are read as settings instead
- A few are reserved for the CLI
- Requested keys are matched exactly
basics
~20 sOnly names beginning with CYPRESS_ or cypress_. Cypress strips that prefix and uses the remainder as the key, so CYPRESS_tenantAdminToken is read back as cy.env(['tenantAdminToken']). If the remainder matches a configuration option name, it sets that option instead.
solid answer
~40 sCypress scans the process environment for names starting with `CYPRESS_` or `cypress_`, strips the prefix, and treats the remainder as an environment value a spec can read — `export CYPRESS_tenantAdminToken=…` becomes `cy.env(['tenantAdminToken'])`. Two exceptions matter. First, if the remainder matches a Cypress **configuration option** — compared with case, dashes and underscores ignored — the value is applied as that option and never joins the environment pool, so `CYPRESS_VIEWPORT_WIDTH=800` sets `viewportWidth` and `cy.env(['VIEWPORT_WIDTH'])` yields `undefined`. Second, `CYPRESS_INTERNAL_ENV` is reserved, and `CYPRESS_RECORD_KEY` and `CYPRESS_PROJECT_ID` are consumed by the Cypress CLI and Cypress Cloud from the OS environment; they cannot come from `cypress.env.json` or the `env` block. Requested keys are matched exactly, so casing has to line up.
code
bash · 4 linesexport CYPRESS_tenantAdminToken='tok_live_9f2c'
export cypress_admin_api_url='https://api.staging.example.com'
npx cypress run --env tenantSlug=acme-corp --expose environmentLabel=staginggo deeper
Know the prefix rule and that it is stripped. Being able to say which shell export makes a value show up in a spec covers most of what is asked here.
Explain the collision: a prefixed name matching a configuration option is applied as that option and disappears from the environment pool, silently. Name at least one example and the reserved variables.
Be ready to debug the undefined. Walk through where the value was set, whether the name collided with an option, and whether the casing the spec requests matches the casing that was exported.
Set a naming convention that cannot collide with configuration option names, and decide which of the four sources your organisation permits for real secrets given that command lines end up in logs.
## How Cypress picks a variable out of the operating system environment Before a run starts, Cypress walks the process environment and keeps only the names that look like its own. As of Cypress 16 the rule is: 1. **Match the prefix.** The name is upper-cased for comparison and must start with `CYPRESS_`, so both `CYPRESS_tenantAdminToken` and `cypress_admin_api_url` qualify. 2. **Strip the prefix.** The leading eight characters come off, and what is left is the key. The remainder keeps its own casing: `cypress_admin_api_url` becomes `admin_api_url`. 3. **Check it against the configuration option names.** The remainder is lower-cased, its dashes and underscores are removed, and it is camel-cased. If the result is the name of a real Cypress configuration option, the value is applied as **that option** and never joins the environment pool. 4. **Otherwise, keep it as an environment value**, coerced from its string form — numbers arrive as numbers. So `export CYPRESS_tenantAdminToken='tok_9f2c'` makes `cy.env(['tenantAdminToken'])` yield `'tok_9f2c'` in every spec of that run. ## When the prefix means configuration, not an environment value Step 3 is the one that surprises people. `CYPRESS_VIEWPORT_WIDTH=800` does not give you a value to read; it sets the `viewportWidth` configuration option, and `cy.env(['VIEWPORT_WIDTH'])` yields `undefined`. The collision is silent — there is no warning that your variable was interpreted as configuration. Two related details: - **`CYPRESS_env` and `CYPRESS_expose` must be valid JSON objects.** They map onto the `env` and `expose` configuration keys, and a string that does not parse into an object is rejected with a warning rather than being spread into indexed keys. - **Name your secrets so they cannot collide.** On a multi-tenant admin console, `CYPRESS_tenantAdminToken` is safe; `CYPRESS_PORT` or `CYPRESS_BASE_URL` are configuration options, not values your spec can read back. ## The `CYPRESS_` names that never become test values - **`CYPRESS_INTERNAL_ENV` is reserved.** Do not set it; it is Cypress's own internal switch. - **`CYPRESS_RECORD_KEY` and `CYPRESS_PROJECT_ID` are consumed by the Cypress CLI and Cypress Cloud**, read straight from the operating system environment when a run records. They cannot be supplied through `cypress.env.json` or the `env` block of your configuration. ## Where else a `cy.env()` value can come from | Source | Example | Notes | |---|---|---| | `env` block in the Cypress config | `env: { adminApiUrl: 'https://api.staging.example.com' }` | checked into the repository, so put only non-secret defaults here | | `cypress.env.json` in the project root | `{ "tenantAdminToken": "tok_9f2c" }` | add it to `.gitignore` when it holds anything sensitive | | `CYPRESS_`-prefixed OS variables | `export CYPRESS_tenantAdminToken=tok_9f2c` | the usual shape in CI, subject to the prefix rules above | | `--env` on the command line | `cypress run --env tenantSlug=acme-corp` | commas between pairs and **no spaces** | Two notes on `--env`. A value containing commas, spaces or quotes must be passed as a JSON string — `--env credentials='{"user":"jane","token":"abc123"}'` — because those characters are otherwise read as delimiters or eaten by the shell. And anything on a command line can end up in a CI log, so it is the weakest of the four places to put a real secret. They also stack: a key set in more than one of these resolves once per run, with the configuration file as the base, then `cypress.env.json`, then the `CYPRESS_` variables, then `--env`. ## `cypress.env.json` in practice The file form is the one teams reach for locally and then misuse in CI, so it is worth being precise about what it is: - It is a **flat JSON object** in the project root, and every JSON type works — strings, numbers, booleans, nested objects — with no prefix and no name transformation. A key written `tenantAdminToken` is read as `cy.env(['tenantAdminToken'])`. - It is **not created for you.** A missing file is not an error; the keys simply resolve from the other sources, or to `undefined`. - **Add it to `.gitignore` the moment it holds anything sensitive.** It sits beside the configuration file and is easy to commit by reflex. - It is the natural home for a developer's own values while the same keys arrive from the `CYPRESS_` form in CI, which is why the two coexisting is normal rather than a smell. ## Reading it back in a spec Requested keys are matched **exactly**, and this is the most common cause of an `undefined` that looks like a broken pipeline: - `cy.env(['tenantAdminToken'])` finds `CYPRESS_tenantAdminToken`. - `cy.env(['TENANTADMINTOKEN'])` finds nothing and yields `undefined` — no error, no warning. - `cy.env(['admin_api_url'])` finds `cypress_admin_api_url`, because only the prefix was removed. Because a missing key is `undefined` rather than a failure, a suite that depends on a value should check for it explicitly at the point it is read, and throw a message naming the key, rather than letting an empty `Authorization` header turn into a confusing 401 five commands later.
- How do you pass a value containing commas or spaces through Cypress's `--env` flag?Pass it as a JSON string: `cypress run --env credentials='{"user":"jane","token":"abc123"}'`. Plain pairs are separated by commas with no spaces, so a raw value containing a comma or a space is otherwise split or swallowed by the shell. Anything on a command line can also reach a CI log, so this is the weakest place for a real secret.
- What does `cy.env()` yield for a key that was never set anywhere?`undefined`, in the yielded object, with no error and no warning. That is why a suite depending on a value should check for it at the point it is read and throw a message naming the key, rather than letting an empty header turn into a puzzling 401 several commands later.
saying these in an interview costs you the question
- Thinks every process environment variable is readable
- Expects the CYPRESS_ prefix to survive into the key name
- Assumes CYPRESS_BASE_URL is readable through cy.env()
- Says key lookups are case-insensitive
- Believes CYPRESS_RECORD_KEY can be set in cypress.env.json