skip to content

In a Postman run, why can a script read an environment variable as undefined while the request still sends its value?

level: seniorimportance: should knowfreq 26%

answer

  1. Two consumers, two different copies
  2. The flag selects; something else decides
  3. The host answers per secret, not per run
  4. A clone is masked, the original is not
  5. No asterisks are written anywhere

basics

~20 s

Postman's runtime clones the environment, globals and collection scopes for a script and blanks any flagged secret the host declined to expose, while the request keeps substituting from the untouched originals. Script and request are deliberately given different views.

solid answer

~40 s

Before a request's scripts run, the runtime collects every variable whose `secret` property is `true` from **`environment`, `globals` and `collectionVariables`** and hands them to the host's secret resolver along with the request URL. The resolver answers per secret with a resolved value, an error, and `allowedInScript`. Anything other than `allowedInScript: true` puts a `scopeName:variableKey` entry into a forbidden-key set. When the script is about to execute, the runtime builds a **clone of each of those three scopes** and sets every forbidden key's value to `undefined`; the script runs against the clone. The payload's original scopes are untouched, so brace-token substitution into the URL, headers and body still gets the real value. `undefined` in the script and a correct value on the wire are therefore the designed behaviour, not a contradiction.

code

javascript · 2 lines
javascript
const token = pm.environment.get('apiKey');
pm.variables.set('seen', typeof token);

go deeper

for a junior

Recall that a Postman script and the request it precedes do not always see the same value for a variable, and that reading undefined in a script is not proof the request is broken.

for a middle

Explain the mechanism: three scopes are scanned for the flag, the host answers per secret, and a clone of each scope is handed to the script with the forbidden values unset.

for a senior

Show the diagnosis path — separate masking from a failed resolution by which of the request and the script actually failed — and refuse the obvious workaround of copying the value into an unflagged variable.

for a principal

Own the boundary this draws: what a script is trusted with versus what only the transport needs, and how you would decide which secrets a team's scripts may ever read.

## Two consumers, two different views of the same variable A Postman run has two consumers for a variable's value: the **request**, which needs the real thing to put on the wire, and the **script**, which usually does not. The runtime is built so those two can be given different answers for the same key, and that is exactly what produces the symptom in the question — a script that reads `undefined` while the call it precedes succeeds. Nothing about this is a bug, and nothing about it is the `secret` flag acting alone. The flag only selects which variables are *considered*. ## What the runtime asks, and what it is told Before a request's scripts run, the runtime performs a secret-resolution pass. It walks three scopes — **`environment`, `globals` and `collectionVariables`** — and collects every variable whose `secret` property is `true`. Iteration data is not walked, and vault secrets are a separate scope with their own rules. If the host supplied a secret-resolver function, the runtime hands it that collection together with the request's URL string, and the resolver answers **per secret** with up to three things: - a **resolved value**, which the runtime writes back into the variable, so a placeholder can be fetched at run time rather than stored in the file; - an **error**, recorded as a resolution failure; - **`allowedInScript`**, a Boolean. `allowedInScript: true` means the script may see the value. Anything else — `false`, or simply absent — means it may not. For each of those, the runtime adds a `scopeName:variableKey` entry to a set of **forbidden secret keys** and carries that set on the execution context. ## How the script's view is built 1. The runtime picks the safe subset of the context that a script is allowed to receive. 2. If forbidden keys are present, it calls its masking helper on that subset. 3. The helper builds a **brand-new variable scope** from each of `environment`, `globals` and `collectionVariables` — a clone, from the scope's own serialised form. 4. It walks the clone's variables and, for every key in the forbidden set, **sets the value to `undefined`**. 5. The clone is what the script executes against. The original scopes on the payload are untouched. There is no placeholder, no row of asterisks, no thrown error. The key is still there; its value is simply gone. ## What the request still sees | consumer | scope it is given | value of a forbidden secret | |---|---|---| | pre-request / post-response script | a clone of the scope | `undefined` | | the request being sent | the original scope on the payload | the real, resolved value | That is why the same run shows a script logging nothing useful and a server accepting the call. The substitution that fills a brace token in a URL, header or body runs against the untouched scopes, not against the script's copy. ## Diagnosing it in practice - **Check whether the variable is flagged `secret`.** An ordinary variable never enters this path, so if a plain value reads `undefined`, look for a typo or a disabled entry instead. - **Check what the host answered.** The blanking is driven by `allowedInScript`, not by the flag — a secret whose resolver said `true` is perfectly readable from a script. - **Distinguish it from a failed resolution.** A resolver error is reported as a resolution failure and, when fatal, stops the item before the request is ever sent. A masked value is silent by design and does not affect the send. - **Do not "fix" it by copying the value elsewhere.** Reading the value in a script and writing it into an unflagged variable defeats the whole arrangement, and it will read as a red flag to whoever asks you this. - **Re-check after a script edits the URL.** If a pre-request script rewrites the URL, the runtime resolves secrets again for the new URL and merges any newly forbidden keys into the same set. ## The one-sentence answer The runtime resolves flagged secrets before scripts run, and any secret the host declined to expose is masked by handing the script a **cloned scope with that value unset**, while the request continues to substitute from the original — so `undefined` in a script and a correct value on the wire are the designed behaviour, not a contradiction.

  • How does this differ from a secret whose resolution actually failed?
    A resolution failure is reported as an error rather than silently masked, and a fatal one stops the item before the request is sent at all. Masking is silent by design and has no effect on the send. So a failed request plus a script error points at resolution; a successful request plus `undefined` in the script points at masking.
  • A pre-request script rewrites the request URL. Does the masking decision change?
    It can. The runtime re-resolves secrets for the changed URL and merges any newly forbidden keys into the same set, because the resolver is given the URL and may answer differently for a different target. A value readable while the request pointed at one host can therefore be masked once the script repoints it.

The script is handed a photocopy with one line blacked out; the original, unredacted page is what goes to the printer.

saying these in an interview costs you the question

  • Believes the secret flag alone blanks it for scripts
  • Expects an asterisk placeholder instead of undefined
  • Thinks masking removes the value from the run entirely
  • Assumes iteration data is scanned for the flag
  • Copies the value into an unflagged variable to work around it