Cypress's `cy.env()` never logs a value, so how do secrets still reach the Command Log?
answer
- The promise covers one log entry only
- Key names are logged; values are not
- The plain object afterwards is yours
- Assertions take no logging option
- Prefer .then() over .its() here
basics
~20 scy.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.
solid answer
~40 s`cy.env()` writes the requested key names to the Command Log and never the values, and `{ log: false }` removes even that entry. The guarantee stops at the command boundary: the yielded object is a plain JavaScript object that Cypress does not mask, redact or track. Several things print it. Assertions write both compared values into the log and accept no logging option, so assert on a derived boolean instead — Chai's `expect(Boolean(tenantAdminToken)).to.be.true`. `.its()` and `.invoke()` add both their subject and their result to the console output, so `cy.env(['tenantAdminToken']).its('tenantAdminToken')` prints it twice. And any downstream command that fails prints its arguments. Keep the value inside a `.then()` callback, which adds no log entry, and pass `{ log: false }` to commands that accept it, such as `cy.request()` and `.type()`.
code
javascript · 15 linesdescribe('tenant admin console', () => {
it('loads billing settings for a tenant', () => {
cy.env(['tenantAdminToken'], { log: false }).then(({ tenantAdminToken }) => {
expect(Boolean(tenantAdminToken)).to.be.true
cy.request({
url: 'https://api.staging.example.com/tenants/acme-corp/settings',
headers: { Authorization: `Bearer ${tenantAdminToken}` },
log: false,
})
.its('status')
.should('eq', 200)
})
})
})go deeper
Remember that cy.env() logs key names, not values, and that { log: false } hides the whole entry. Knowing where to put the value — inside the .then() callback — is the practical takeaway.
Explain that the guarantee is scoped to one log entry and ends when the command yields. Name the three printers: assertions, .its() or .invoke(), and any failing downstream command.
Show that you have read a captured run and found a credential in it. Describe the spec shape you would enforce so the value never leaves its callback, and what you would do about the credential afterwards.
Decide what your organisation treats as an acceptable place for run output to travel, and make the safe read pattern the default in shared commands so no individual author has to remember these rules.
## What the command actually guarantees `cy.env()` makes one narrow promise: the entry it writes to the Cypress Command Log names **the keys you asked for and never their values**. `cy.env(['tenantAdminToken'])` shows the key `tenantAdminToken`; the token itself never appears in that entry. Passing `{ log: false }` removes even the key name, which is worth doing when the key name alone tells a reader something — a key called `acmeMigrationKey` names a customer. That is the whole guarantee, and it is a guarantee about **one log entry**. ## Where the guarantee ends It ends the moment the command yields. The object handed to your `.then()` callback is an ordinary JavaScript object. Cypress does not mask it, does not redact it, and does not track where it goes afterwards. Anything you do with that value that writes to the Command Log or the console output writes the value. Four mechanisms account for nearly every real leak: 1. **Assertions print both sides of the comparison, and cannot be silenced.** Assertions accept no logging option at all. `cy.env(['tenantAdminToken']).should('deep.include', { tenantAdminToken: 'tok_9f2c' })` puts the token in the log twice — once as the expected value and once as the actual. Assert on something you *derive* instead: inside a `.then()`, write Chai's `expect(Boolean(tenantAdminToken)).to.be.true`, which logs `expected true to be true`. 2. **`.its()` and `.invoke()` echo their subject and their result.** Both add the value they were applied to and the value they produce to the console output, so `cy.env(['tenantAdminToken']).its('tenantAdminToken')` prints the token twice over. Read the property inside a `.then()` callback instead of chaining a query onto the yielded object. 3. **A failing downstream command prints its arguments.** A `cy.request()` that 500s, a `.type()` that cannot find its target — the failure message carries what you passed in, and a failure is exactly the moment a run is captured and shared. 4. **A secret in a URL is printed as the URL.** The URL is the most visible part of a `cy.visit()` or `cy.request()` entry, so a token in a query string is on screen by default. Both commands accept `log`, but the durable fix is to send the value in a header instead of the address. ## `{ log: false }` hides; it does not redact This distinction is what an interviewer is usually listening for. | Mechanism | What it does | What it does not do | |---|---|---| | `cy.env(..., { log: false })` | omits that command's entry, key names included | nothing about the value afterwards | | `.then()` and `.spread()` | add no Command Log entry at all | stop an assertion inside them from logging | | `cy.request({ ..., log: false })` | omits the request entry and its headers | redact the value, or affect other commands | | `.type(value, { log: false })` | omits the typed text from the entry | hide the value from the DOM or the app | | an assertion | always logs both compared values | accept any logging option | ## A safe shape for a spec For a multi-tenant admin console whose specs authenticate as a tenant administrator, the pattern that holds up is short: - **Fetch late and narrowly.** Ask for the keys you need at the point you need them, not a whole bag of them at the top of the file. - **Stay inside the callback.** `.then()` adds no log entry, so the value has no reason to leave it. - **Pass `{ log: false }` to whatever consumes it.** `cy.request()` and `.type()` both take it. - **Assert on a derived boolean**, never on the value, because assertions cannot be quietened. - **Never `.its()` or `.invoke()` the yielded object.** ## Why this matters more than it looks A Cypress run records what the Command Log contains. Anything printed there travels into whatever a failed run produces and into whatever a reviewer opens, and it is read by people who had no reason to see a production-adjacent credential. As of Cypress 16 the runner has done the hard half of the work by keeping the value in Node until you ask for it; the remaining half — what happens after it yields — is entirely the test author's, and the API is explicit that this is the boundary.
- How do you assert that a value read with Cypress's `cy.env()` was actually set?Derive a boolean inside a `.then()` and assert on Chai's `expect(Boolean(tenantAdminToken)).to.be.true`, which logs only `expected true to be true`. Asserting on the value itself — with `should('deep.include', …)`, for instance — writes both the expected and the actual value into the Command Log, and assertions accept no logging option to suppress it.
- Why avoid `.its()` on the object that Cypress's `cy.env()` yields?`.its()` and `.invoke()` both add the subject they were applied to and the value they produce to the console output, so reading a key that way prints the secret twice over. Destructure the property inside a `.then()` callback instead, which produces no Command Log entry at all.
Cypress hands you the secret in a sealed envelope and reads only the label aloud. Once you open it on stage, what the audience sees is up to you.
saying these in an interview costs you the question
- Thinks { log: false } redacts the value everywhere
- Asserts on the secret and expects it to stay hidden
- Chains .its() onto the object cy.env() yields
- Believes Cypress tracks and masks the yielded value
- Assumes a failing command hides the arguments it was given