buildGbrainEnv leaves PGLite users exposed to .env poisoning → broken-db false-positive
Summary
PR #1583 (CHANGELOG line 1359, 1410) fixed lib/gbrain-local-status.ts:freshClassify so the probe's env is routed through buildGbrainEnv. The fix is complete for Postgres/Supabase brains but incomplete for PGLite brains, because buildGbrainEnv works by seeding DATABASE_URL from ~/.gbrain/config.json — and a PGLite config has no database_url field.
For PGLite users, line 111 of lib/gbrain-exec.ts fires if (!cfg.database_url) return out; and returns the caller env unmodified. Bun's auto-load of the cwd's .env then flows straight through to the gbrain sources list --json probe, gbrain treats the leaked DATABASE_URL as a Postgres override, connection fails, classifier reads "Cannot connect to database" from stderr, and returns broken-db. The 60s cache then propagates that poisoned negative to clean directories.
Net effect: every PGLite user with a DATABASE_URL in any project's .env (sqlite, mysql, app DB on a different port, etc.) sees broken-db from gstack — and brain-aware blocks get silently suppressed in their planning-skill SKILL.md files.
Reproduction
Setup: gbrain 0.42.x with engine: "pglite" (e.g. ~/.gbrain/config.json lacks database_url).
# Working: $HOME, no .env in scope, no DATABASE_URL leaked
rm -f ~/.gstack/.gbrain-local-status-cache.json
( cd "$HOME" && env -u DATABASE_URL -u GBRAIN_DATABASE_URL \
~/.claude/skills/gstack/bin/gstack-gbrain-detect ) | jq .gbrain_local_status
# → "ok"
# Broken: any project repo with DATABASE_URL in .env
cd /path/to/some-project # contains .env with DATABASE_URL=sqlite:///app.db
rm -f ~/.gstack/.gbrain-local-status-cache.json
~/.claude/skills/gstack/bin/gstack-gbrain-detect | jq .gbrain_local_status
# → "broken-db"
# Direct gbrain works fine — proving the brain isn't actually broken:
gbrain sources list --json # exit 0, valid JSON, empty stderr
The broken-db classification then gets cached for 60s and propagates to subsequent probes even from clean directories.
Why PR #1583 didn't cover this
buildGbrainEnv in lib/gbrain-exec.ts:95-127 is built around seeding DATABASE_URL:
export function buildGbrainEnv(opts: BuildGbrainEnvOptions = {}): NodeJS.ProcessEnv {
const baseEnv = opts.baseEnv || process.env;
const out: NodeJS.ProcessEnv = { ...baseEnv };
if (baseEnv.GSTACK_RESPECT_ENV_DATABASE_URL === "1") return out;
...
if (!cfg.database_url) return out; // ← PGLite escape hatch
...
out.DATABASE_URL = cfg.database_url;
}
For PGLite (file-engine, no URL), cfg.database_url is undefined, line 111 returns out (which is { ...baseEnv }), and the .env-poisoned DATABASE_URL is preserved unchanged. The probe sees it, gbrain dutifully tries to connect to Postgres, fails, and the classifier locks in broken-db.
Suggested fix
When the resolved config engine is PGLite, strip DATABASE_URL / GBRAIN_DATABASE_URL from the returned env instead of passing through. The config schema already carries enough signal — either engine === "pglite" or database_path present with no database_url. Sketch:
// lib/gbrain-exec.ts
if (!cfg.database_url) {
// PGLite engine has no DATABASE_URL to seed. Strip any caller-supplied
// value (e.g. Bun-autoloaded project .env) so the file-backed brain
// isn't probed as Postgres → "Cannot connect to database" → broken-db.
if (cfg.engine === "pglite" || cfg.database_path) {
delete out.DATABASE_URL;
delete out.GBRAIN_DATABASE_URL;
}
return out;
}
GSTACK_RESPECT_ENV_DATABASE_URL=1 keeps its escape hatch (handled before this branch).
Same one-liner pattern should probably be mirrored in lib/gbrain-local-status.ts:223-265 freshClassify if buildGbrainEnv shouldn't take on the engine-aware behavior directly.
Test coverage to add
A freshClassify test case with:
~/.gbrain/config.json set to {"engine":"pglite","database_path":"..."} (no database_url)
- caller env supplies
DATABASE_URL=sqlite:///x.db (simulating the Bun .env autoload)
- stub
gbrain exits 0 with valid JSON when called with no DATABASE_URL env
- assert
localEngineStatus() returns "ok", not "broken-db"
This would have caught the regression and would prevent it from re-emerging.
Environment
gstack: 1.57.6.0 (current main tip, commit 9cc41b7)
gbrain: 0.42.29.0
- Engine: PGLite (
~/.gbrain/config.json has engine: "pglite", database_path: ..., no database_url)
- Host: macOS / zsh
- Triggering
.env (in an unrelated project repo, cwd at probe time): DATABASE_URL=sqlite:///node0.db
References
buildGbrainEnvleaves PGLite users exposed to.envpoisoning →broken-dbfalse-positiveSummary
PR #1583 (CHANGELOG line 1359, 1410) fixed
lib/gbrain-local-status.ts:freshClassifyso the probe's env is routed throughbuildGbrainEnv. The fix is complete for Postgres/Supabase brains but incomplete for PGLite brains, becausebuildGbrainEnvworks by seedingDATABASE_URLfrom~/.gbrain/config.json— and a PGLite config has nodatabase_urlfield.For PGLite users, line 111 of
lib/gbrain-exec.tsfiresif (!cfg.database_url) return out;and returns the caller env unmodified. Bun's auto-load of the cwd's.envthen flows straight through to thegbrain sources list --jsonprobe, gbrain treats the leakedDATABASE_URLas a Postgres override, connection fails, classifier reads"Cannot connect to database"from stderr, and returnsbroken-db. The 60s cache then propagates that poisoned negative to clean directories.Net effect: every PGLite user with a
DATABASE_URLin any project's.env(sqlite, mysql, app DB on a different port, etc.) seesbroken-dbfrom gstack — and brain-aware blocks get silently suppressed in their planning-skillSKILL.mdfiles.Reproduction
Setup: gbrain 0.42.x with
engine: "pglite"(e.g.~/.gbrain/config.jsonlacksdatabase_url).The
broken-dbclassification then gets cached for 60s and propagates to subsequent probes even from clean directories.Why PR #1583 didn't cover this
buildGbrainEnvinlib/gbrain-exec.ts:95-127is built around seedingDATABASE_URL:For PGLite (file-engine, no URL),
cfg.database_urlis undefined, line 111 returnsout(which is{ ...baseEnv }), and the.env-poisonedDATABASE_URLis preserved unchanged. The probe sees it, gbrain dutifully tries to connect to Postgres, fails, and the classifier locks inbroken-db.Suggested fix
When the resolved config engine is PGLite, strip
DATABASE_URL/GBRAIN_DATABASE_URLfrom the returned env instead of passing through. The config schema already carries enough signal — eitherengine === "pglite"ordatabase_pathpresent with nodatabase_url. Sketch:GSTACK_RESPECT_ENV_DATABASE_URL=1keeps its escape hatch (handled before this branch).Same one-liner pattern should probably be mirrored in
lib/gbrain-local-status.ts:223-265 freshClassifyifbuildGbrainEnvshouldn't take on the engine-aware behavior directly.Test coverage to add
A
freshClassifytest case with:~/.gbrain/config.jsonset to{"engine":"pglite","database_path":"..."}(nodatabase_url)DATABASE_URL=sqlite:///x.db(simulating the Bun.envautoload)gbrainexits 0 with valid JSON when called with noDATABASE_URLenvlocalEngineStatus()returns"ok", not"broken-db"This would have caught the regression and would prevent it from re-emerging.
Environment
gstack: 1.57.6.0 (currentmaintip, commit9cc41b7)gbrain: 0.42.29.0~/.gbrain/config.jsonhasengine: "pglite",database_path: ..., nodatabase_url).env(in an unrelated project repo, cwd at probe time):DATABASE_URL=sqlite:///node0.dbReferences
freshClassifyfix (Postgres/Supabase complete, PGLite incomplete)lib/gbrain-exec.ts:95-127—buildGbrainEnvdefinitionlib/gbrain-local-status.ts:223-265—freshClassify(the probe call site)