Why did Cypress 16 remove `Cypress.env()`, and what replaced it?
answer
- One old call became two new surfaces
- The old reader reached every configured value
- Node keeps secrets; the browser gets the rest
- Sensitive reads are commands now
- cy.env() and Cypress.expose()
basics
~20 sCypress.env() hydrated every configured environment variable into the browser, so reading one value exposed them all. Cypress 16 removed it and split the job: cy.env() reads secrets asynchronously and keeps them in Node, while Cypress.expose() serves public values synchronously.
solid answer
~40 s`Cypress.env()` was deprecated in Cypress 15.10.0 and removed in 16.0. The problem was all-or-nothing exposure: Cypress serialised the whole resolved `env` object into the browser, so every configured value — including ones no test ever read — was reachable from devtools, from application code, and from any third-party script or extension on the page, and the whole set was copied into every `cy.origin()` context. Cypress 16 replaces it with two APIs chosen by sensitivity. `cy.env(['tenantAdminToken'])` is a read-only command that yields only the keys you name and keeps everything else in Node, so it is asynchronous and used inside a chain. `Cypress.expose('environmentLabel')` is synchronous and deliberately browser-visible, for feature flags, API versions and environment labels. `allowCypressEnv`, the 15.x lock that banned the old call, is gone too.
code
javascript · 12 linesconst { defineConfig } = require('cypress')
module.exports = defineConfig({
env: {
tenantAdminToken: process.env.TENANT_ADMIN_TOKEN,
adminApiUrl: process.env.ADMIN_API_URL,
},
expose: {
environmentLabel: 'staging',
consoleApiVersion: 'v2',
},
})go deeper
Be ready to name the replacement pair and say which one is for secrets. Knowing that Cypress.env() is gone in Cypress 16, not merely discouraged, is most of the answer at this level.
Explain the mechanism behind the removal: the old call serialised the entire configured environment into the browser, so exposure never depended on what a test actually read. Say where each replacement's values live.
An interviewer expects you to plan this migration on a suite you did not write: classify every value, convert reads that used to be synchronous, and deal with plugins that still call the removed API and therefore throw.
Own the standard. Decide what your organisation treats as sensitive, price what the asynchronous read costs in code shape, and keep the classification visible in review rather than rediscovered test by test.
## The API that went away `Cypress.env()` was a synchronous accessor on the `Cypress` object. You put values under the `env` key of your configuration and any spec could call `Cypress.env('tenantAdminToken')` and get one back immediately. It was deprecated in Cypress 15.10.0 and **removed in Cypress 16.0**. There is no configuration flag that brings it back, and a plugin that still calls it throws. The reason for the removal was not that the API read awkwardly. It was **where the values lived**. To make a synchronous read possible inside the browser, Cypress serialised the *entire* resolved `env` object into the browser context before your first test ran. That had three consequences: 1. **All-or-nothing exposure.** Every configured value sat in the browser whether or not any test read it. A suite with one admin token and nine harmless settings shipped all ten. 2. **Cross-origin spread.** When a test entered a `cy.origin()` block, the whole environment travelled with it — including to origins your team does not own. 3. **Reachable by the page.** Anything running in that context could read the serialised values: the application under test, an analytics or session-recording script, a browser extension, or a devtools console. ## The two APIs that replaced it Cypress 16 splits the job by **sensitivity**, and the split is deliberate — you now have to say which kind of value you are handling. | | `cy.env(['key'])` | `Cypress.expose('key')` | |---|---|---| | Shape | a Cypress command, asynchronous | a plain function, synchronous | | Values live in | Node; only requested keys cross | browser state, by design | | Readable where | inside a test or hook, yields into `.then()` | anywhere, including module scope | | Can it write? | no, it is read-only | yes: `Cypress.expose(key, value)` | | Intended for | keys, tokens, passwords, credentials | feature flags, API versions, environment labels | Both landed in Cypress 15.10.0, so a 15.x suite can migrate before taking the major. `cy.env()` takes an **array of keys** and yields an object: `cy.env(['adminApiUrl', 'tenantAdminToken']).then(({ adminApiUrl, tenantAdminToken }) => { ... })`. A key that was never set yields `undefined` rather than throwing, and names are matched exactly, so `cy.env(['TENANTADMINTOKEN'])` will not find `tenantAdminToken`. It accepts `log` (default `true`, and only key *names* are ever written to the Command Log) and `timeout` (default 4000). `Cypress.expose()` has four shapes: `Cypress.expose()` returns everything exposed, `Cypress.expose(key)` returns one value, and `Cypress.expose(key, value)` or `Cypress.expose(object)` set values, which are then merged with the existing ones. ## What did not change - The **`env` configuration key still exists**, and is still where a secret is declared. - So do the other sources that feed it: a `cypress.env.json` file, `CYPRESS_`-prefixed operating system variables, and the `--env` CLI flag. They now supply `cy.env()` instead of the removed call. - Public values get a parallel surface of their own: the top-level `expose` block and the `--expose` CLI flag. ## What else moved in 16 - **`env` is no longer valid in a per-test or per-suite configuration override.** Those overrides are applied in the browser, so an `env` value set there could never take effect on the Node side. `expose` is accepted there instead: suite- and test-level keys merge, test-level wins, and the overridden keys are restored after each test. - **`allowCypressEnv` is gone.** On Cypress 15.10.0 and later you could set `allowCypressEnv: false` to make the old call throw while you migrated. In 16 there is nothing left to switch off. - **Plugins that still call the removed API always throw.** Migrating a real suite means updating those dependencies to versions built on the new APIs, not only editing your own specs. ## The trap worth naming out loud The migration is often described as a rename. It is not, and two things bite teams that treat it that way: - Moving a value into `expose` because the new command is inconvenient puts it **back into browser state** — precisely the problem the removal was meant to end. `Cypress.expose()` is not a secure store; it is the public shelf. - `cy.env()` cannot set anything. If old code wrote to the environment at runtime, there is no drop-in equivalent, and that state has to live somewhere outside the environment surface. As of Cypress 16, the one-line summary is that the environment is no longer a single bag the browser holds. It is two surfaces with different reach, and you choose per value, once, in configuration.
- Can `cy.env()` set a value at runtime the way the removed `Cypress.env()` could?No. `cy.env()` is read-only and has no setter. `Cypress.expose(key, value)` can set a value, but only a public one, only for the remainder of the current spec file, and it never propagates back to Node. A secret produced during a run has to be held somewhere other than the environment surface.
- What happened to `env` inside a per-test or per-suite Cypress configuration override?It was removed in Cypress 16. Test configuration overrides are applied in the browser, so an `env` value set there could never take effect on the Node side. `expose` is accepted in a suite- or test-level override instead: suite and test keys merge, test-level wins, and the overridden keys are restored after each test.
saying these in an interview costs you the question
- Says Cypress.env() still works if you configure it
- Treats Cypress.expose() as a safe home for an API key
- Thinks the env config key was removed along with the old command
- Calls cy.env() synchronously and assigns its return value
- Believes allowCypressEnv: false still bans the old call in 16