skip to content

Config and Node Events

The one config file and the Node-side seam it opens: which keys shape a run, where a value is read from, and what only Node code can do. Interviewers probe where the two runtimes meet.

on this pageshow

explore

questions

20

Why did Cypress 16 remove `Cypress.env()`, and what replaced it?

level: juniorimportance: must knowfreq 74%

answer

  1. One old call became two new surfaces
  2. The old reader reached every configured value
  3. Node keeps secrets; the browser gets the rest
  4. Sensitive reads are commands now
  5. cy.env() and Cypress.expose()

basics

~20 s

Cypress.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 lines
javascript
const { 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

In a Cypress `cypress.config.js`, which options must sit inside the `e2e` block, not the root?

level: juniorimportance: must knowfreq 74%

basics

~20 s

The testing-type-only keys: baseUrl, specPattern, supportFile, excludeSpecPattern, slowTestThreshold and testIsolation. Cypress refuses to start and names the offending key if one of them appears at the root of the exported object, telling you to move it into e2e or component.

open as a page

Which Cypress commands let a spec running in the browser reach the file system?

level: juniorimportance: must knowfreq 72%

basics

~10 s

Only four: cy.fixture(), cy.readFile(), cy.writeFile() and cy.task(). Spec code is browser JavaScript with no fs module, so each of those hands the work to the Node process that loaded the Cypress config.

open as a page

In Cypress, what must an `on('task')` handler in `setupNodeEvents` return?

level: juniorimportance: must knowfreq 62%

basics

~20 s

It must return a value, null, or a promise that resolves to one of those. Returning undefined fails the calling command with a message naming the task, because Cypress reads undefined as "no handler answered this event".

open as a page

Cypress's `cy.env()` never logs a value, so how do secrets still reach the Command Log?

level: middleimportance: must knowfreq 58%

basics

~20 s

cy.env() logs only the key names you asked for, and that protection ends when it yields. The object you receive is an ordinary JavaScript object that Cypress does not mask or track, so assertions, .its(), .invoke() and failing commands all print what they touch.

open as a page

In Cypress, if `defaultCommandTimeout` is set at the root and in the `e2e` block, which wins?

level: middleimportance: must knowfreq 56%

basics

~20 s

The e2e block wins. Before the run starts, Cypress flattens the configuration for the testing type it is launching by spreading that block over the root object, so a key declared in both takes the block's value.

open as a page

In Cypress, what survives the cy.task() boundary between the spec and Node?

level: middleimportance: must knowfreq 60%

basics

~20 s

Only what JSON.stringify() can represent. Cypress serialises both the argument and the returned value, so functions, regular expressions and symbols arrive as null, a Date arrives as a string, and a live handle cannot cross at all.

open as a page

Which OS environment variables reach Cypress's `cy.env()`, and how are their names changed?

level: middleimportance: should knowfreq 50%

basics

~20 s

Only 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.

open as a page

In a Cypress spec, how long does a value set with `Cypress.config()` last?

level: middleimportance: should knowfreq 50%

basics

~20 s

For the rest of the current spec file, and no longer. Cypress exits the browser between specs, so the change never reaches another spec, and it is never written back to the configuration file on disk.

open as a page

In Cypress's `before:browser:launch`, what must the handler do with `launchOptions`?

level: middleimportance: should knowfreq 48%

basics

~20 s

Change the launchOptions it was given - args, preferences, extensions or env - and then return it. Only the returned object is merged back into the real launch options, so a handler that forgets to return contributes nothing at all.

open as a page

Why is a tenant admin token in a Cypress `expose` block a leak, and what does moving it cost?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Everything in the expose block is deliberately browser-visible: application code, third-party scripts and extensions on the page can read it. Moving the token to cy.env() fixes that, but every read becomes an asynchronous command, so synchronous module-scope reads must be restructured.

open as a page

Why can `cypress run --config defaultCommandTimeout=15000` leave your e2e timeout unchanged?

level: seniorimportance: should knowfreq 44%

basics

~10 s

Because --config merges onto the root of the configuration being resolved, and Cypress then spreads the e2e block over that root. A defaultCommandTimeout declared inside the block overwrites the flag, silently, with no warning.

open as a page

How does a Cypress spec verify the suspension email the admin console sent?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Not from the browser. The spec has no mailbox client, and cy.intercept() only observes traffic the browser makes, so the check goes through cy.task(), where Node can poll a mail API, or cy.request() against a test-only inbox endpoint.

open as a page

Why does a Cypress `after:spec` hook run under `cypress run` but never in `cypress open`?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Because before:run, after:run, before:spec and after:spec only fire in interactive mode when experimentalInteractiveRunEvents is enabled, and it defaults to false. Cypress logs nothing when it skips them, so the handler looks broken rather than switched off.

open as a page

How strictly should a Cypress suite keep every cy.task() payload to plain JSON?

level: principalimportance: should knowfreq 36%

basics

~20 s

Strictly, in most suites. The seam already serialises, so the only real choice is whether that loss is explicit in a small set of plain payload shapes, or hidden behind a revive layer somebody has to keep honest.

open as a page

In Cypress, how much of a suite's logic should live inside `setupNodeEvents`?

level: principalimportance: should knowfreq 35%

basics

~20 s

As little as the seam allows. Treat setupNodeEvents as a registry of named handlers over ordinary tested modules, because code there carries no assertions, no retries, no isolation and no coverage from the suite's own pass or fail signal.

open as a page

What does Cypress's cy.writeFile() yield, and whose disk does it write to?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

It yields null, never the contents. The write happens in the Node process running Cypress, at a path resolved from the project root — the folder holding the Cypress config file — not in the browser and not beside the spec.

open as a page

Why does a Cypress component spec matched by `e2e.specPattern` vanish from the component run?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Cypress folds the e2e block's specPattern into the ignore list for a component run, so any file the e2e pattern claims is dropped from component discovery. Widening e2e.specPattern therefore hides component specs, with no error naming the cause.

open as a page

A Cypress `after:screenshot` handler crops the image, but the run reports the old size. Why?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

Because Cypress only updates the recorded details from what the handler returns. It reads exactly three keys - path, size and dimensions - and ignores anything else, so a handler that edits the file but returns nothing leaves the original metadata in place.

open as a page

How should a team decide which Cypress config values go in `env` and which in `expose`?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Default everything to the env block and promote to expose only with a stated reason. The test is not whether the value looks like a password, but whether the page, its third-party scripts and its extensions may hold it for the length of a run.

open as a page