Surfaced while implementing #11341 (the childEnv() enforcement gate). Filed rather than folded in: #11341's declared surface is the check script, its self-test, its wiring and its baseline, and the fix here would be edits to eight test files — including two in a neighbourhood another card held in flight at the time.
The gap
#11341's gate refuses a bulk process.env reference in a spawner file under packages/cli/test/** — a spread, an Object.assign source, an alias. It says nothing about a spawn that passes no env option at all, and that child gets process.env verbatim: TEST=true, VITEST=true, VITEST_WORKER_ID, the lot.
That is the same leak in a purer form. { ...process.env, NO_COLOR: '1' } at least lets the author see the inheritance they are asking for; omitting env inherits everything with nothing on the page to read.
Measured on origin/main @ 2dc0a770b
Eight call sites, derived by walking every spawner file's AST for calls to a node:child_process binding (or a promisify() wrapper of one) whose options object carries no env key:
packages/cli/test/authoring-rule-command-parity.test.ts:192 execFileSync
packages/cli/test/cloud-login-json-ndjson.e2e.test.ts:310 execFileSync
packages/cli/test/emit-json-pipe.test.ts:62 execFileP (promisify(execFile))
packages/cli/test/emit-json-pipe.test.ts:144 spawn
packages/cli/test/emit-json-pipe.test.ts:194 execFileP
packages/cli/test/format-zod-union.test.ts:176 execFileSync
packages/cli/test/login-json-ndjson.e2e.test.ts:298 execFileSync
packages/cli/test/metadata-type-schema-gate.test.ts:185 execFileSync
None of these is an active false green today. Every one spawns tsx on a probe script or a non-serve CLI command, so nothing in the child reads TEST or VITEST — the same reading #11341 recorded for its own fifteen. The gap is latent and structural, exactly as #11267 was before someone tried to write a security-posture assertion against it.
Why #11341 left it out rather than widening
Stated in that gate's header as a named false negative, not discovered afterwards. The reason is that closing it is a decision about the convention's reach, not about the defect:
childEnv() was written for spawned CLI children whose environment matters. Requiring it at execFileSync(TSX, [probe], { cwd }) in an in-process-flavoured unit test asserts that every child in this directory owes the convention — which may well be right, but it is a widening of childEnv()'s contract and it belongs to whoever owns that contract. A gate that reaches past its own convention is how a gate gets carved out.
What closing it would look like
Two halves, and the second is the one that needs the ruling:
- Extend
scripts/check-cli-test-child-env.mjs to treat a spawn call with no env option as a finding. Mechanically small — the gate already resolves spawn bindings and their promisify() wrappers; the call-anchored pass exists in its measurement history and was set aside for the site-anchoring reason its header gives. - Decide whether the eight above are repairs or carve-outs. A probe script that reads nothing from the environment arguably wants
childEnv() anyway (it costs nothing and removes the question); a format-zod-union fixture spawning tsx may be better declared than changed.
Either outcome needs the eight baselined first if the gate lands before the repairs, for the same reason #11341's did.
Explicitly not this issue
Surfaced while implementing #11341 (the
childEnv()enforcement gate). Filed rather than folded in: #11341's declared surface is the check script, its self-test, its wiring and its baseline, and the fix here would be edits to eight test files — including two in a neighbourhood another card held in flight at the time.The gap
#11341's gate refuses a bulk
process.envreference in a spawner file underpackages/cli/test/**— a spread, anObject.assignsource, an alias. It says nothing about a spawn that passes noenvoption at all, and that child getsprocess.envverbatim:TEST=true,VITEST=true,VITEST_WORKER_ID, the lot.That is the same leak in a purer form.
{ ...process.env, NO_COLOR: '1' }at least lets the author see the inheritance they are asking for; omittingenvinherits everything with nothing on the page to read.Measured on
origin/main@2dc0a770bEight call sites, derived by walking every spawner file's AST for calls to a
node:child_processbinding (or apromisify()wrapper of one) whose options object carries noenvkey:None of these is an active false green today. Every one spawns
tsxon a probe script or a non-serveCLI command, so nothing in the child readsTESTorVITEST— the same reading #11341 recorded for its own fifteen. The gap is latent and structural, exactly as #11267 was before someone tried to write a security-posture assertion against it.Why #11341 left it out rather than widening
Stated in that gate's header as a named false negative, not discovered afterwards. The reason is that closing it is a decision about the convention's reach, not about the defect:
childEnv()was written for spawned CLI children whose environment matters. Requiring it atexecFileSync(TSX, [probe], { cwd })in an in-process-flavoured unit test asserts that every child in this directory owes the convention — which may well be right, but it is a widening ofchildEnv()'s contract and it belongs to whoever owns that contract. A gate that reaches past its own convention is how a gate gets carved out.What closing it would look like
Two halves, and the second is the one that needs the ruling:
scripts/check-cli-test-child-env.mjsto treat a spawn call with noenvoption as a finding. Mechanically small — the gate already resolves spawn bindings and theirpromisify()wrappers; the call-anchored pass exists in its measurement history and was set aside for the site-anchoring reason its header gives.childEnv()anyway (it costs nothing and removes the question); aformat-zod-unionfixture spawningtsxmay be better declared than changed.Either outcome needs the eight baselined first if the gate lands before the repairs, for the same reason #11341's did.
Explicitly not this issue
packages/cli/testspawner from re-introducing the bare...process.envchild env #11341 — the parent defect and the gate. Its baseline holds the eighteen bulk-reference leaks; nothing here is in it.bin/run.jsplus a ts-path-enablingNODE_ENV, a different defect that carries its own upstream block.