QA How-To
Cypress environment variables: Examples
Learn a secure cypress environment variables example using cy.env, Cypress.expose, CLI overrides, TypeScript, CI secrets, precedence, and safe migration.
23 min read | 3,046 words
TL;DR
In Cypress 15.10 and newer, request secrets asynchronously with cy.env(), read public configuration with Cypress.expose(), and use Cypress.config() for runner settings. Inject CI secrets as protected OS variables, validate them early, and never commit working credentials.
Key Takeaways
- Use cy.env for sensitive values and Cypress.expose only for public browser-safe configuration.
- Treat Cypress.config values, Cypress test environment values, and application runtime variables as separate channels.
- Keep working credentials out of cypress.env.json, source control, CLI logs, screenshots, and uploaded artifacts.
- Normalize operating-system variable names and validate required types at the configuration boundary.
- Use setupNodeEvents with an allowlisted target map instead of scattering environment conditions through specs.
- Migrate deprecated Cypress.env calls before setting allowCypressEnv to false.
A practical cypress environment variables example should separate secrets from public test configuration. In current Cypress, read sensitive values asynchronously with cy.env(), and read intentionally public values synchronously with Cypress.expose(). The older Cypress.env() API was deprecated in Cypress 15.10.0, so new 2026 suites should not build more code around it.
This guide shows where values come from, how precedence works, how to use them safely in TypeScript tests and CI, and how to migrate older suites. The examples use reserved test domains and placeholder credentials so you can copy the patterns without copying a secret.
TL;DR
| Value | Recommended 2026 API | Typical source |
|---|---|---|
| API token, password, client secret | cy.env(['key']) |
CI secret or CYPRESS_* OS variable |
| Feature flag, API version, environment label | Cypress.expose('key') |
expose config or --expose |
Cypress setting such as baseUrl |
Cypress.config('baseUrl') |
Cypress configuration or --config |
| Application runtime variable | Application-specific API | Build system, server, or application config |
cy.env(['apiToken'], { log: false }).then(({ apiToken }) => {
cy.request({
url: '/api/profile',
headers: { authorization: `Bearer ${apiToken}` },
}).its('status').should('eq', 200)
})
const releaseChannel = Cypress.expose('releaseChannel')
expect(releaseChannel).to.eq('qa')
The essential rule is classification. A value that must remain controlled belongs behind cy.env(). A value that any browser script may see can use Cypress.expose(). Cypress environment variables are test-runner inputs, not the same thing as your application's operating-system environment.
1. What a Cypress Environment Variables Example Must Explain
The phrase "environment variable" describes several different mechanisms in a Cypress project. Mixing them causes confusing overrides and, more importantly, accidental secret exposure. Cypress test environment values are the values configured under env, in cypress.env.json, with CYPRESS_* variables, through --env, or returned from setupNodeEvents. Tests retrieve selected values with cy.env().
Public exposed configuration is a separate channel. It is configured under expose or passed with --expose, and tests retrieve it using Cypress.expose(). That API is synchronous because all exposed configuration is deliberately available in the browser context. Never place a token there.
Cypress configuration values form a third channel. Options such as baseUrl, viewportWidth, retries, and defaultCommandTimeout are read with Cypress.config(). An OS variable named CYPRESS_BASE_URL can override the baseUrl Cypress configuration option, but baseUrl is still configuration, not a secret accessed with cy.env().
Finally, your application under test has its own runtime settings. A Vite application may use import.meta.env, a Node service may use process.env, and a remote deployment may load a configuration endpoint. Cypress does not automatically make its test values part of the application. Pass data across that boundary only when the scenario requires it.
A strong design names these categories in the project README. Team members should know which values are safe in browser state, which are secret inputs, and which control Cypress itself before they add another flag.
2. Cypress Environment Variables Example with cy.env()
Use cy.env() for credentials, API keys, tokens, passwords, and other values that should not be hydrated wholesale into the browser-accessible Cypress object. The command accepts an array of keys and yields an object containing only those requested values.
describe('profile API', () => {
it('loads the authenticated profile', () => {
cy.env(['apiBaseUrl', 'apiToken'], { log: false }).then(
({ apiBaseUrl, apiToken }) => {
if (typeof apiBaseUrl !== 'string' || typeof apiToken !== 'string') {
throw new Error('apiBaseUrl and apiToken are required')
}
cy.request({
method: 'GET',
url: `${apiBaseUrl}/profile`,
headers: {
authorization: `Bearer ${apiToken}`,
},
log: false,
}).then((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.keys('id', 'displayName', 'roles')
})
},
)
})
})
cy.env() is a Cypress command, so it is asynchronous and participates in the command queue. You cannot assign its result to a constant and use it synchronously:
// Incorrect: cy.env() returns a Chainable, not the token string.
const token = cy.env(['apiToken'])
Request several related keys once instead of scattering lookups through a test. Validate required values at the boundary and give a useful error. An undefined token that later causes a 401 sends engineers toward the service, while "apiToken is required" points directly to configuration.
The { log: false } option hides the command and requested key names from the Command Log. Also set log: false on a request that carries the secret. This reduces accidental disclosure, but it does not make screenshots, application logs, proxies, or third-party code safe. Use a restricted test credential with the minimum privileges and rotate it through your secret manager.
3. Choose Between cy.env, Cypress.expose, and Cypress.config
The correct API depends on sensitivity and purpose, not on whether the team casually calls a value an environment variable.
| Question | cy.env() |
Cypress.expose() |
Cypress.config() |
|---|---|---|---|
| Is the value intended to be secret? | Yes | No | Usually no |
| Is access asynchronous? | Yes | No | No |
| Typical values | Tokens, passwords, private endpoints | Feature flags, labels, API versions | baseUrl, timeouts, retries |
| Configuration location | env and secret input sources |
expose and --expose |
Top-level Cypress config |
| Browser visibility | Requested values only | Intentionally browser-accessible | Cypress runner configuration |
Here is one configuration using all three channels correctly:
import { defineConfig } from 'cypress'
export default defineConfig({
baseUrl: 'https://shop.example.test',
env: {
apiToken: process.env.QA_API_TOKEN,
},
expose: {
releaseChannel: 'qa',
checkoutVariant: 'compact',
},
e2e: {
setupNodeEvents(on, config) {
return config
},
},
})
it('uses each configuration channel for its intended purpose', () => {
expect(Cypress.config('baseUrl')).to.eq('https://shop.example.test')
expect(Cypress.expose('checkoutVariant')).to.eq('compact')
cy.env(['apiToken'], { log: false }).then(({ apiToken }) => {
expect(apiToken, 'a restricted CI token is configured').to.be.a('string')
})
})
Do not put a public base URL in the secret channel merely because it changes by environment. Do not put a token in expose merely because synchronous access is convenient. This classification improves security reviews, simplifies migration, and makes failures easier to diagnose.
For broader setup choices, see the Cypress testing tutorial and the Cypress configuration guide.
4. Configure Values in cypress.config.ts and cypress.env.json
The checked-in configuration is suitable for safe defaults and for mapping operating-system secrets into Cypress. Avoid literal credentials.
import { defineConfig } from 'cypress'
export default defineConfig({
allowCypressEnv: false,
env: {
apiBaseUrl: process.env.QA_API_BASE_URL ?? 'https://api.example.test',
apiToken: process.env.QA_API_TOKEN,
},
expose: {
environmentName: process.env.QA_ENVIRONMENT_NAME ?? 'local',
enableNewCart: false,
},
e2e: {
baseUrl: process.env.QA_WEB_BASE_URL ?? 'http://localhost:3000',
},
})
allowCypressEnv: false prevents deprecated Cypress.env() usage. Add it only after code and plugins have migrated to cy.env() or Cypress.expose(). A plugin that still reads Cypress.env() can fail after the switch, so check dependency migration notes first.
A cypress.env.json file in the project root can provide local values:
{
"apiBaseUrl": "http://localhost:4000",
"apiToken": "local-development-token"
}
Because this file often contains secrets, add it to .gitignore and commit a safe template instead:
{
"apiBaseUrl": "http://localhost:4000",
"apiToken": "replace-with-a-restricted-local-token"
}
Name the committed file cypress.env.example.json so Cypress does not load placeholders as real settings. Document the copy step in project setup.
Do not confuse env inside the Cypress config with process.env. The former becomes Cypress test environment input. The latter is the operating-system environment available to the Node configuration process. Mapping process.env.QA_API_TOKEN to env.apiToken is an explicit bridge. That boundary is a good place to normalize names and reject missing CI inputs.
5. Pass CYPRESS Variables and CLI Overrides Correctly
Cypress can load OS-level variables prefixed with CYPRESS_. For a custom test value, the prefix is removed before access. For example, CYPRESS_apiToken supplies apiToken to cy.env().
CYPRESS_apiToken='restricted-test-token' \
CYPRESS_apiBaseUrl='https://api.qa.example.test' \
npx cypress run
The --env flag is convenient for local, non-production overrides:
npx cypress run --env apiBaseUrl=https://api.qa.example.test,tenantId=tenant-a
Values are comma-separated without spaces. Shell quoting matters when a value contains punctuation or a JSON object:
npx cypress run --env 'credentials={"username":"qa-user","scope":"read-only"}'
Avoid passing secrets through CLI arguments in CI. Commands can appear in job logs, process listings, shell history, and diagnostic artifacts. Store secrets in the CI platform, inject them as masked OS variables, and reference them from configuration.
Some CYPRESS_* variables have special meanings. CYPRESS_RECORD_KEY and CYPRESS_PROJECT_ID, for example, are consumed for Cypress Cloud workflows rather than treated like ordinary test keys. Do not attempt to provide those through cypress.env.json.
Public values have their own flag:
npx cypress run --expose environmentName=staging,enableNewCart=true
Cypress configuration options use --config, not --env:
npx cypress run --config baseUrl=https://shop.qa.example.test,retries=1
Keeping these three flags distinct makes a run command self-documenting: --env for controlled test inputs, --expose for public values, and --config for runner behavior.
6. Understand Precedence Without Guessing
Multiple sources are useful only if overrides are predictable. A typical workflow starts with values in cypress.config.ts, allows local cypress.env.json values, accepts prefixed operating-system variables, and applies command-line overrides for the run. setupNodeEvents can then inspect or change the resolved config before returning it.
When a value appears wrong, open the Cypress Project Settings panel and inspect resolved configuration. Cypress shows the contributing sources, which is faster and safer than printing the full environment object.
Use one canonical key spelling. Operating systems differ in case sensitivity, and teams often mix API_URL, apiUrl, and api_url. Normalize external names at the config boundary:
import { defineConfig } from 'cypress'
export default defineConfig({
env: {
apiUrl: process.env.QA_API_URL,
apiToken: process.env.QA_API_TOKEN,
},
})
Tests then request only apiUrl and apiToken. They do not need to know the CI naming convention.
Boolean and number coercion also deserves attention. Values supplied through Cypress-supported CLI and prefixed-variable parsing can be normalized, but arbitrary process.env values are strings or undefined. Parse explicit Node inputs yourself:
const auditEnabled = process.env.QA_AUDIT_ENABLED === 'true'
const requestTimeout = Number(process.env.QA_REQUEST_TIMEOUT_MS ?? 10000)
if (!Number.isFinite(requestTimeout)) {
throw new Error('QA_REQUEST_TIMEOUT_MS must be numeric')
}
Do not rely on truthiness for a string such as "false", which is truthy in JavaScript. Validate at startup so all affected specs fail with one clear configuration message rather than branching differently.
7. Create Dynamic Environment Values in setupNodeEvents
setupNodeEvents runs in the Cypress Node process and can adjust the resolved config. This is useful for selecting an endpoint from a non-secret environment label or loading credentials through an approved secret provider.
import { defineConfig } from 'cypress'
const apiUrls: Record<string, string> = {
local: 'http://localhost:4000',
qa: 'https://api.qa.example.test',
staging: 'https://api.staging.example.test',
}
export default defineConfig({
env: {
target: 'local',
},
e2e: {
setupNodeEvents(on, config) {
const target = String(config.env.target)
const apiBaseUrl = apiUrls[target]
if (!apiBaseUrl) {
throw new Error(`Unsupported target: ${target}`)
}
config.env.apiBaseUrl = apiBaseUrl
config.env.apiToken = process.env.QA_API_TOKEN
return config
},
},
})
Run it with a safe selector:
CYPRESS_target=staging QA_API_TOKEN='restricted-token' npx cypress run
Use an allowlist rather than constructing a hostname from untrusted text. It prevents typos from sending tests toward an unintended system. Add an explicit block for production unless the suite and credentials are designed for production-safe checks.
Returning config is essential after mutation. Keep external secret retrieval efficient because configuration executes before the specs. If secret retrieval needs network access, fail with a sanitized message and never print the returned payload.
Node tasks are appropriate when an operation must remain entirely in Node. Passing a secret into cy.task() arguments moves it across the runner boundary and may expose it in logging, so prefer a task that reads the secret from process.env internally and returns only a non-sensitive result.
8. Build a Typed Custom Command for Secret-Backed API Calls
Repeated environment access can be centralized without hiding the request contract. A custom command should return the Cypress chain and avoid logging secret-bearing requests.
// cypress/support/e2e.ts
declare global {
namespace Cypress {
interface Chainable {
apiRequest<T>(
method: 'GET' | 'POST',
path: string,
body?: unknown,
): Chainable<Cypress.Response<T>>
}
}
}
Cypress.Commands.add('apiRequest', (method, path, body) => {
return cy
.env(['apiBaseUrl', 'apiToken'], { log: false })
.then(({ apiBaseUrl, apiToken }) => {
if (typeof apiBaseUrl !== 'string' || typeof apiToken !== 'string') {
throw new Error('API test configuration is incomplete')
}
return cy.request({
method,
url: `${apiBaseUrl}${path}`,
headers: { authorization: `Bearer ${apiToken}` },
body,
log: false,
})
})
})
export {}
type HealthResponse = {
status: 'ok'
version: string
}
it('checks the service health contract', () => {
cy.apiRequest<HealthResponse>('GET', '/health').then((response) => {
expect(response.status).to.eq(200)
expect(response.body.status).to.eq('ok')
expect(response.body.version).to.match(/^v\d+/)
})
})
TypeScript describes the expected response, but it does not validate runtime data automatically. Assert critical fields or use a runtime schema in contract-focused tests.
Keep the helper narrow. A universal command that reads dozens of keys, changes tenants, seeds databases, and swallows errors becomes difficult to audit. Separate authentication from test data setup and from UI actions. The Cypress cy.request API testing guide explains how to combine programmatic setup with browser assertions.
9. Use Environment Variables Safely in CI
A secure pipeline stores credentials in the CI provider's protected secret store and exposes them only to the test job. The repository should contain names and documentation, not values.
name: Cypress checks
on:
pull_request:
jobs:
e2e:
runs-on: ubuntu-latest
env:
QA_API_TOKEN: ${{ secrets.QA_API_TOKEN }}
QA_API_BASE_URL: https://api.qa.example.test
QA_WEB_BASE_URL: https://shop.qa.example.test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx cypress run
The configuration maps QA_API_TOKEN into env.apiToken, and the spec requests it with cy.env(). Keep forked pull requests and untrusted code away from privileged secrets. Many CI systems intentionally withhold secrets from external contributions.
Use a dedicated service account with narrow scope, short lifetime where possible, and data restricted to the test environment. Masking a long-lived administrator token in logs does not reduce what it can do if compromised.
Review every place a secret could travel: Cypress commands, request logs, screenshots, videos, browser console output, application telemetry, proxy traces, reporter output, and uploaded artifacts. log: false is one control, not a complete security boundary.
Fail before test execution when a required secret is absent. A CI preflight script or a check in configuration can report the missing variable by name without printing any value. This keeps the first hundred tests from failing with identical authentication errors.
10. Migrate from Deprecated Cypress.env()
Starting with Cypress 15.10.0, Cypress.env() is deprecated. Migration requires classifying each key rather than mechanically replacing every call.
| Legacy use | Replacement | Reason |
|---|---|---|
Cypress.env('apiToken') |
cy.env(['apiToken']) |
Sensitive value, controlled asynchronous access |
Cypress.env('featureFlag') |
Cypress.expose('featureFlag') |
Public value, synchronous access is acceptable |
Cypress.env('baseUrl') |
Cypress.config('baseUrl') |
It is runner configuration |
| Plugin configuration | Updated plugin plus expose when public |
Plugins may need a compatible major version |
A secret lookup must move into the Cypress chain:
// Before
const token = Cypress.env('apiToken')
cy.request({
url: '/api/me',
headers: { authorization: `Bearer ${token}` },
})
// After
cy.env(['apiToken'], { log: false }).then(({ apiToken }) => {
cy.request({
url: '/api/me',
headers: { authorization: `Bearer ${apiToken}` },
log: false,
})
})
A public feature flag moves from env to expose:
// cypress.config.ts
export default defineConfig({
expose: {
checkoutVariant: 'compact',
},
})
// spec
const variant = Cypress.expose('checkoutVariant')
Search support files and plugins, not only specs. Update compatible versions of packages that formerly read Cypress.env(). Once the suite is clean, set allowCypressEnv: false and run both component and end-to-end tests. This converts future deprecated access into an immediate error.
11. Debug Missing, Stale, or Mis-Typed Values
Begin with the resolved configuration panel. Confirm the key exists, its source is expected, and its spelling matches the lookup. Do not print all environment values to the terminal.
If cy.env() yields undefined, check whether the key is nested. Requesting apiToken does not retrieve credentials.apiToken unless that object is configured and requested as credentials. Prefer flat keys for independent secret rotation.
If a public value is missing from Cypress.expose(), verify it is under expose, not env, and that the run uses --expose, not --env. If Cypress.config('baseUrl') is wrong, inspect CYPRESS_BASE_URL and --config.
Watch for shell behavior. Quotes may be preserved or stripped differently across Bash, PowerShell, and package scripts. JSON values need valid quoting. Put complex, non-secret data in a versioned configuration file rather than fighting nested shell escaping.
Runtime changes have limited scope. Cypress.expose() mutations apply within the current spec and do not propagate to the Node process. Older Cypress.env() mutations were also spec-scoped, which is another reason not to treat Cypress state as a cross-spec data store.
Finally, isolate configuration from application defects. Assert the selected public environment label, make one direct authenticated health request, and only then debug the UI flow. This boundary check tells you whether the problem is value loading, service access, or application behavior.
12. Scale a Cypress Environment Variables Example Across Environments
A scalable suite uses a small environment manifest, not conditionals scattered through specs. Map a safe target label to endpoints in setupNodeEvents, keep credentials in the secret store, and expose only UI-relevant labels.
type Target = 'local' | 'qa' | 'staging'
type TargetConfig = {
webBaseUrl: string
apiBaseUrl: string
}
const targets: Record<Target, TargetConfig> = {
local: {
webBaseUrl: 'http://localhost:3000',
apiBaseUrl: 'http://localhost:4000',
},
qa: {
webBaseUrl: 'https://shop.qa.example.test',
apiBaseUrl: 'https://api.qa.example.test',
},
staging: {
webBaseUrl: 'https://shop.staging.example.test',
apiBaseUrl: 'https://api.staging.example.test',
},
}
Validate the target once, set config.baseUrl and config.env.apiBaseUrl, and keep spec code environment-neutral. A checkout test should say cy.visit('/checkout'), not branch on hostname.
Separate environment selection from business test data. A tenant ID, seeded customer, or order template may vary independently from the deployment. Builders and API setup commands express those variations better than more global environment flags.
Document ownership and expiration for every secret. Include a safe example value, expected scope, and the job that consumes it. Do not document the actual value. Add a smoke check that confirms endpoint identity so a staging job cannot silently point to production.
This pattern supports local, QA, and staging runs while keeping the same test intent. It also improves interview explanations because you can describe a clear security model rather than a collection of ad hoc variables.
13. Treat Environment Configuration as a Reviewed Contract
Environment keys become an interface between the repository, developer machines, CI jobs, plugins, and deployed test systems. Manage that interface deliberately. Keep a versioned inventory containing the canonical key, sensitivity class, expected type, required environments, owner, and safe example. The inventory must never contain a working secret.
| Key | Class | Type | Required in | Owner |
|---|---|---|---|---|
apiToken |
Secret | Non-empty string | QA and staging CI | QA platform |
apiBaseUrl |
Controlled endpoint | HTTPS URL | API suites | Service team |
environmentName |
Public | Allowed label | All runs | Release engineering |
checkoutVariant |
Public | Allowed variant | Checkout specs | Web team |
runId |
Public coordination | String | Parallel CI | Build platform |
Validate this contract once before expensive browser work. Reject unknown targets, missing required values, non-HTTPS remote endpoints, malformed numbers, and unexpected public variants. Report key names and expected forms, never secret contents. A focused configuration error prevents hundreds of misleading authentication or navigation failures.
Review consumers when renaming a key. Search configuration, specs, support commands, package scripts, workflow files, container definitions, and plugin settings. During a controlled transition, map the old external name to one canonical internal name at the configuration boundary rather than teaching every test both spellings.
Apply least privilege to configuration ownership too. Engineers who can edit a public feature label do not automatically need access to the secret that authorizes destructive API setup. CI environments should restrict secrets by branch, job, and deployment target. Production credentials should not be reachable from an ordinary pull request suite.
Finally, test the contract itself with a small preflight in the same Node and Cypress versions used by CI. Confirm required presence, parse types, verify allowlisted hosts, and display only sanitized public context. This turns environment setup from informal tribal knowledge into a maintained, auditable part of the test architecture.
Interview Questions and Answers
Q: What replaced Cypress.env() in current Cypress?
Use cy.env() for sensitive values and Cypress.expose() for public, browser-safe configuration. Cypress.env() was deprecated in Cypress 15.10.0.
Q: Why is cy.env() asynchronous?
It is a Cypress command that reveals only explicitly requested keys through the command chain. Consume its result in .then() or return it from a custom command.
Q: What is the difference between CYPRESS_BASE_URL and CYPRESS_apiToken?
CYPRESS_BASE_URL overrides the Cypress baseUrl configuration option. A custom CYPRESS_apiToken value becomes a test environment input that can be requested as apiToken.
Q: Should secrets be passed with --env?
It works technically, but CI command lines may appear in logs and process data. Prefer the CI secret store and masked operating-system variables.
Q: How do you prevent deprecated access after migration?
Set allowCypressEnv: false after all specs, support code, and plugins have migrated. Then run the complete suite to catch hidden consumers.
Q: How do you share a value between specs?
Configuration sources supply the value independently to each spec. Do not mutate browser-side Cypress state as a cross-spec store because specs run in isolation.
Q: How would you debug an unexpected value?
Inspect Cypress resolved configuration, identify the contributing source, verify key spelling and type conversion, and test the boundary with one focused assertion without printing secrets.
Common Mistakes
- Putting tokens in
Cypress.expose()because synchronous access is easier. - Copying
Cypress.env()examples without noticing the Cypress 15.10.0 deprecation. - Treating
cy.env()as a synchronous function instead of a queued command. - Committing
cypress.env.jsonwith working credentials. - Passing secrets on the CI command line where job logs can capture them.
- Using the same key with several spellings and casing conventions.
- Reading a string
"false"as if it were the Booleanfalse. - Printing the full resolved environment while debugging one missing key.
- Forgetting to return the mutated
configfromsetupNodeEvents. - Allowing a target label to construct arbitrary production hostnames.
- Assuming Cypress test variables automatically become application variables.
- Enabling
allowCypressEnv: falsebefore checking plugin compatibility.
Conclusion
A secure cypress environment variables example in 2026 uses cy.env() for selected sensitive values, Cypress.expose() for intentional public configuration, and Cypress.config() for runner options. Define clear sources, validate required inputs once, and keep secrets out of source control, command logs, and public browser state.
Start by inventorying every existing Cypress.env() call. Classify the key, migrate it to the correct API, update dependent plugins, set allowCypressEnv: false, and verify the suite in local and CI modes.
Interview Questions and Answers
How do you manage secrets in a modern Cypress suite?
I keep secrets in a CI or local secret manager, map them into Cypress under env, and request only needed keys with cy.env(). I suppress secret-bearing command and request logs, use least-privilege test identities, and never put secrets in Cypress.expose().
Explain cy.env versus Cypress.expose.
cy.env() is an asynchronous command for sensitive or controlled values and returns only requested keys. Cypress.expose() is synchronous and intended for public configuration that browser scripts may access. The choice is based on sensitivity, not convenience.
Why was Cypress.env deprecated?
The legacy API could hydrate configured environment data into browser-accessible state and make accidental secret exposure easier. Current Cypress separates controlled access through cy.env() from intentional public values through Cypress.expose().
How do environment overrides work in CI?
I define safe defaults in cypress.config.ts, map protected OS variables into env, and use explicit CLI overrides only for non-secret run choices. I inspect resolved configuration when precedence is unclear and normalize naming at one boundary.
How do you support several test environments without conditional specs?
I select a safe target label, map it to allowlisted endpoints in setupNodeEvents, and set baseUrl plus API configuration there. Specs use relative URLs and remain independent of the deployment name.
What risks remain when log false is used?
A secret can still reach application logs, browser scripts, proxies, screenshots, videos, reporters, or uploaded artifacts. Log suppression is one layer, so I also use short-lived least-privilege credentials and review the full data path.
How would you migrate a suite from Cypress.env?
I inventory every use in specs, support code, and plugins, classify each key, then move secrets to cy.env and public values to Cypress.expose. I update plugin versions, enable allowCypressEnv: false, and run both E2E and component suites.
Frequently Asked Questions
How do I read an environment variable in Cypress in 2026?
Use cy.env(['key']).then(({ key }) => { ... }) for sensitive or controlled test values. Use Cypress.expose('key') only when the value is intentionally public and safe for browser access.
Is Cypress.env deprecated?
Yes. Cypress.env() was deprecated in Cypress 15.10.0. Classify each old key and migrate secrets to cy.env(), public settings to Cypress.expose(), or runner options to Cypress.config().
Where should I store Cypress secrets?
Use your CI platform's protected secret store and inject values as masked operating-system variables. For local development, use an ignored file or approved local secret manager, never a committed working cypress.env.json.
What is the difference between --env and --expose?
--env supplies controlled test environment inputs retrieved with cy.env(). --expose supplies public browser-accessible configuration retrieved with Cypress.expose().
Why does cy.env return a Chainable?
cy.env() is an asynchronous Cypress command designed to reveal only the requested values. Consume the yielded object in .then() or return the chain from a custom command.
Can Cypress environment variables change between specs?
Each spec runs in isolation and receives resolved configuration for that run. Runtime browser-side changes should not be used as a cross-spec store.
How do I debug a missing Cypress variable safely?
Inspect the resolved configuration panel, confirm the source and exact key, and validate only the required value's presence and type. Do not print the complete environment or any secret value.