Repository navigation
gstack-evidence reports false test results in any repo with a .env.local: Bun auto-loads dotenv and Bun.spawn inherits it #2624
Description
Summary
gstack-evidence run reports false test failures in any repo that has a
.env.local. It is a Bun script, Bun auto-loads dotenv files from the cwd into
process.env, and Bun.spawn inherits that polluted environment into the
child test runner. The tool whose entire job is trustworthy verification
silently returns the wrong verdict.
Mechanism
bin/gstack-evidence:1 is #!/usr/bin/env bun. Bun's runtime loads .env,
.env.local and .env.<NODE_ENV> from the cwd into process.env before user
code runs. Then at bin/gstack-evidence:192:
proc = Bun.spawn(spawnArgv, { stdin: "inherit", stdout: "pipe", stderr: "pipe" });No env key is passed, so the child inherits process.env — already carrying
the repo's secrets and config. For the common single-token form the argv is
["bash", "-c", cmd] (:170), and it flows straight through to npm and the
test runner.
There is no env sanitisation anywhere in the file; the only process.env read
is GSTACK_HOME at :95.
Observed
On a Next.js repo whose .env.local defines PHOTO_RESERVATION_SECRET and
NEXT_PUBLIC_BASE_URL, two unrelated suites went red under the wrapper and
green everywhere else:
gstack-evidence run --label vitest -- 'npm run test:once' -> 2 files RED
npm run test:once -> green
CI=true npm run test:once -> green
Both suites assert behaviour that only holds when those vars are unset (a
capability degrades to null; a URL falls back to http://localhost). The
injected production values flipped both. Nothing in the diff was wrong.
Two-line confirmation that the runtime, not the repo, is responsible:
$ bun -e 'console.log(process.env.PHOTO_RESERVATION_SECRET, process.env.NEXT_PUBLIC_BASE_URL)'
<secret> https://app.example.com
$ node -e 'console.log(process.env.PHOTO_RESERVATION_SECRET, process.env.NEXT_PUBLIC_BASE_URL)'
undefined undefined
Why this one matters more than the usual env-leak bug
- It is silent. The run completes and records a verdict; it just records the
wrong one. Both directions are possible — a false red as above, or a false
green where an injected value masks a real failure. - It affects every repo with a
.env.local, which is most JS repos. - It lands specifically on the evidence ledger, which exists to be believed.
gstack-evidence is the only script under bin/ that runs user-supplied repo
commands, so it is the whole blast radius — the other Bun.spawn call sites in
bin/ are fixed-argv tool invocations.
Suggested fix
Subtract the keys Bun parsed from .env* in the cwd before spawning, and pass
an explicit env to Bun.spawn. That keeps the developer's real environment
(which the child legitimately needs) while removing exactly what the runtime
injected behind the user's back.
Worth noting test/helpers/hermetic-env.ts already implements a full allowlist
(buildHermeticEnv, hermeticChildEnv), but it is drop-everything-else and
lives under test/. That is too aggressive here: gstack-evidence run executes
arbitrary developer commands that may need PATH extensions, credentials, or
tool config. The narrow subtraction is the right shape.
A --clean-env opt-out flag would be a reasonable escape hatch for anyone who
genuinely wants dotenv values passed through.
Activity
fix(evidence): stop bun's dotenv autoload from reaching the spawned c…
v1.69.0.0 fix: the silent-failure wave — 6 fixes, 5 community PRs abs…