Wrapper Environments Can Forge False Preferences
Jul 02, 2026
When a package manager or launcher injects environment variables that look like user preferences, your CLI may honour a choice the operator never made. Treat wrapper provenance as part of the product path—and test through the wrapper that broke you.
The problem
The symptom was familiar: an operator ran a command through a package script, saw a banner announcing the system default terminal editor, and landed in an editor they did not want. The first instinct is to improve fallback discovery—check for a friendlier editor when the configured one is unavailable or undesirable.
That instinct was wrong. A fallback already existed. The resolver honoured EDITOR before it ever reached the fallback chain. And EDITOR was set—to the system default terminal editor—by the package manager's lifecycle environment, not by the operator.
The hard part is that a user who genuinely exports the same value and a tool that injects the same value are indistinguishable inside the child process. From the application's perspective, both look like EDITOR=vi with no other signal.
What actually happened
The fix was not a bigger fallback list or a change at the command entry point. It was a policy decision inside the central editor resolver: recognise when a bare default editor value is likely injected by a trustworthy package-manager run lifecycle, treat it as no preference, and let the existing fallback chain proceed.
That sounds narrow. It was—but the boundary contained more environmental complexity than the line count suggested:
- Package managers can inject lifecycle variables that describe how a script was launched.
- Different package-manager versions represent the same
npm runlifecycle with different marker values. - Interactive commands need real terminal behaviour; a non-interactive test path can exit before the resolver ever runs.
- Continuous integration exposed a portability gap that local testing missed: the same lifecycle appeared as
runin one environment andrun-scriptin another.
The production change stayed inside the resolver so the banner path and the actual editor-launch path could not diverge. Unit tests covered the predicate; a wrapper-level regression test ran through the package manager, reached the real banner output, asserted the preferred editor, rejected the old one, and stopped before opening a real editor or creating side effects.
The manual acceptance surface was the local terminal: run the real command in a host pseudo-terminal, observe the banner, abort safely. There was no web preview—this is a CLI product path.
One tradeoff was explicit and documented: an operator who genuinely wants the bare default editor, leaves the visual-editor variable unset, and invokes through the package manager will also be normalised to the friendlier fallback. The supported escape hatches are setting the visual-editor variable, passing a command with arguments, or running outside the package-manager wrapper.
The lesson
Wrapper environments are part of the product path, not incidental packaging detail. Shell state, package scripts, lifecycle injection, runtime launchers, and pseudo-terminal behaviour can materially change what production code sees.
When a defect appears behind a wrapper, three habits matter:
- Reproduce through the exact operator command before designing the fix.
- Record the runtime environment inside the wrapper process, not only in the parent shell.
- Put policy in one shared resolver so display and execution cannot drift apart.
If you cannot recover provenance from the environment surface alone, make the product policy explicit rather than hiding an arbitrary guess.
The broader principle
A value in the environment is not automatically a preference. It may be a default injected by a tool the operator did not configure. That is a runtime-provenance problem disguised as a simple configuration bug.
Verification must follow the same composition. Unit proof of a predicate is insufficient when the original defect appeared on the composed path—package manager, script, resolver, banner, pseudo-terminal. A real-path test should assert both the desired new behaviour and the absence of the old behaviour.
Treat CI variance as product evidence. When a check fails in automation but passes locally, the difference in lifecycle markers or environment shape is often telling you about a contract you have not enumerated yet.
How to apply it
Before freezing an environment-handling contract, specify a matrix covering:
- normalisation cases (injected defaults treated as no preference),
- preserved genuine preferences (visual-editor variable, commands with arguments, paths),
- incomplete or untrusted lifecycle markers,
- adjacent modes such as one-off execution versus scripted runs,
- desired operator-visible output,
- absence of the old output.
Include a wrapper-level regression test in addition to unit tests. Define a safe manual smoke that stops before launching external tools or creating durable side effects. Document unavoidable provenance tradeoffs and the supported escape hatch.
For operator-facing commands with no preview deployment, declare the human acceptance surface during design—not at the end. Local CLI smoke, with branch, expected revision, working directory, command, environment, inputs, expected result, and blocking result, is a first-class acceptance method.