Skip to content

gstack-evidence reports false test results in any repo with a .env.local: Bun auto-loads dotenv and Bun.spawn inherits it #2624

Closed

Description

@v639dragoon

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

  1. added a commit that references this issue on Aug 22, 2026
    a0fd8ba
  2. added a commit that references this issue on Aug 24, 2026
    ad84005
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions