Problem — .env.example is the only discovery surface a user or a fresh install has for "what can I configure," and it has drifted: at least twelve variables the server reads at runtime are undocumented, including two whose absence is actively confusing. SIGNAL_CONFIG_PATH is documented while its two siblings SIGNAL_DIR and SIGNAL_DB_PATH — read in the same three-line block of signalSync.js — are not, so a user relocating a Signal install documents-their-way into half a configuration. SIGNAL_KEYCHAIN_PASSWORD is read as a credential and is undocumented entirely. CODEX_TIMEOUT_MS / GROK_TIMEOUT_MS / AGY_IMAGEGEN_TIMEOUT_MS are documented but the parallel GROK_VIDEO_TIMEOUT_MS is not. Because these are all read lazily at first use with a silent fallback, a typo'd or unset variable produces no signal at all — the feature just quietly uses the default. There is no guard test, so the drift will recur.
Evidence — the half-documented Signal block, server/services/signalSync.js:84-92:
return expandHome(process.env.SIGNAL_DIR || DEFAULT_SIGNAL_DIR);
}
…
return process.env.SIGNAL_CONFIG_PATH || join(signalDir(), CONFIG_REL);
}
…
return process.env.SIGNAL_DB_PATH || join(signalDir(), DB_REL);
against .env.example, which carries only:
# Signal sync config file path (default: SIGNAL_DIR/config.json)
# SIGNAL_CONFIG_PATH=/path/to/signal/config.json
The full undocumented set found by diffing process.env.<NAME> reads under server/ (excluding node_modules and tests) against the .env.example keys: SIGNAL_DIR, SIGNAL_DB_PATH, SIGNAL_KEYCHAIN_PASSWORD (signalSync.js:85,91,246), IMESSAGE_CHAT_DB (imessageSync.js:262), CONTACTS_AB_ROOT (contactsSync.js:191), PORTOS_VNC_PORT (remoteDesktop.js:22), EIDOVERSE_WS_URL / EIDOVERSE_HTTP_URL (eidoverseWorld.js:588,593), GROK_VIDEO_TIMEOUT_MS (videoGen/grok.js:49), PORTOS_REGEN_MAX_MP (imageGen/regen.js:100), SPEC_DECODE_IDLE_STALL_MS (specDecodeModels.js:43), LORA_TRAIN_MAX_QUANT_BITS (loraTraining/runtimes.js:137), FFLF_LTX2_PIXEL_BUDGET (videoGen/renderArgs.js:104).
Plan
- Add a commented block to
.env.example for each of the twelve, next to its topical neighbours (Signal three under the existing Signal comment; GROK_VIDEO_TIMEOUT_MS beside GROK_TIMEOUT_MS; the LoRA one beside the LORA_TRAIN_STALL_* group; etc.). Each gets one comment line naming the reading module and the default, matching the file's existing style. Use placeholder values only (/path/to/..., host-example.ts.net) per the Sensitive Data rules — never a real path.
- Add
server/envExampleDrift.test.js (node env): read every git-tracked non-test .js under server/, collect process.env.<NAME> identifiers, subtract a documented INHERITED_ENV allowlist (OS/toolchain/inherited-only names that a user never sets in .env: NODE_ENV, PATH, HOME, PWD, USER, TMP, TEMP, TMPDIR, LOCALAPPDATA, XDG_CACHE_HOME, DYLD_LIBRARY_PATH, LD_LIBRARY_PATH, TZ, M, P, S, PM2_HOME, PM2_ID, CLAUDECODE, VITEST, VITEST_FAST, PORTOS_PROBE, PORTOS_TEST_*, FAKE_*, TEST_DB_OK, PGTESTDATABASE, SPRITE_WALK_PARITY_RUN, FEATURE_FLAG, PORTOS_PROCESS_ENV_TEST, CERT_DIR, HF_HOME, HF_HUB_CACHE, BUN_INSTALL, GH_TOKEN, GITHUB_TOKEN, CLAUDE_CODE_USE_BEDROCK, PORTOS_WORKSPACE_ROOTS if already documented), and assert every remaining name appears as a NAME= token in .env.example (commented or not). Each allowlist entry carries a one-line reason comment. Decision: allowlist by category with reasons rather than snapshotting today's diff, so the test stays meaningful as the tree grows.
- Add the reverse check in the same file: every
NAME= in .env.example is read somewhere under server/, scripts/, ecosystem.config.cjs or docker-compose.yml — this catches a documented-but-dead variable, the other drift direction.
Tests
server/envExampleDrift.test.js is the test. Two cases (forward and reverse drift) plus fixture strings proving the extractor sees process.env.FOO, process.env['FOO'] is NOT matched (document that limitation in the header), and that a commented # FOO=bar line in .env.example counts as documented. Pins the next undocumented variable.
Acceptance criteria
Out of scope — startup validation / fail-fast for these variables (they are all optional overrides with defaults), and the client's VITE_* variables.
Filed by a /do:better --scan-only --issues audit (2026-09-01). Category: stack-specific · Severity: medium · Files: server/services/signalSync.js:85, server/services/signalSync.js:91, server/services/signalSync.js:246, server/services/imessageSync.js:262, server/services/contactsSync.js:191, server/services/remoteDesktop.js:22, server/services/eidoverseWorld.js:588, server/services/videoGen/grok.js:49, server/services/imageGen/regen.js:100, server/services/specDecodeModels.js:43, server/services/loraTraining/runtimes.js:137, server/services/videoGen/renderArgs.js:104
All labels already exist in the repo; do NOT create labels. Never add planner:* labels.
Problem —
.env.exampleis the only discovery surface a user or a fresh install has for "what can I configure," and it has drifted: at least twelve variables the server reads at runtime are undocumented, including two whose absence is actively confusing.SIGNAL_CONFIG_PATHis documented while its two siblingsSIGNAL_DIRandSIGNAL_DB_PATH— read in the same three-line block ofsignalSync.js— are not, so a user relocating a Signal install documents-their-way into half a configuration.SIGNAL_KEYCHAIN_PASSWORDis read as a credential and is undocumented entirely.CODEX_TIMEOUT_MS/GROK_TIMEOUT_MS/AGY_IMAGEGEN_TIMEOUT_MSare documented but the parallelGROK_VIDEO_TIMEOUT_MSis not. Because these are all read lazily at first use with a silent fallback, a typo'd or unset variable produces no signal at all — the feature just quietly uses the default. There is no guard test, so the drift will recur.Evidence — the half-documented Signal block,
server/services/signalSync.js:84-92:against
.env.example, which carries only:The full undocumented set found by diffing
process.env.<NAME>reads underserver/(excluding node_modules and tests) against the.env.examplekeys:SIGNAL_DIR,SIGNAL_DB_PATH,SIGNAL_KEYCHAIN_PASSWORD(signalSync.js:85,91,246),IMESSAGE_CHAT_DB(imessageSync.js:262),CONTACTS_AB_ROOT(contactsSync.js:191),PORTOS_VNC_PORT(remoteDesktop.js:22),EIDOVERSE_WS_URL/EIDOVERSE_HTTP_URL(eidoverseWorld.js:588,593),GROK_VIDEO_TIMEOUT_MS(videoGen/grok.js:49),PORTOS_REGEN_MAX_MP(imageGen/regen.js:100),SPEC_DECODE_IDLE_STALL_MS(specDecodeModels.js:43),LORA_TRAIN_MAX_QUANT_BITS(loraTraining/runtimes.js:137),FFLF_LTX2_PIXEL_BUDGET(videoGen/renderArgs.js:104).Plan
.env.examplefor each of the twelve, next to its topical neighbours (Signal three under the existing Signal comment;GROK_VIDEO_TIMEOUT_MSbesideGROK_TIMEOUT_MS; the LoRA one beside theLORA_TRAIN_STALL_*group; etc.). Each gets one comment line naming the reading module and the default, matching the file's existing style. Use placeholder values only (/path/to/...,host-example.ts.net) per the Sensitive Data rules — never a real path.server/envExampleDrift.test.js(node env): read every git-tracked non-test.jsunderserver/, collectprocess.env.<NAME>identifiers, subtract a documentedINHERITED_ENVallowlist (OS/toolchain/inherited-only names that a user never sets in.env:NODE_ENV,PATH,HOME,PWD,USER,TMP,TEMP,TMPDIR,LOCALAPPDATA,XDG_CACHE_HOME,DYLD_LIBRARY_PATH,LD_LIBRARY_PATH,TZ,M,P,S,PM2_HOME,PM2_ID,CLAUDECODE,VITEST,VITEST_FAST,PORTOS_PROBE,PORTOS_TEST_*,FAKE_*,TEST_DB_OK,PGTESTDATABASE,SPRITE_WALK_PARITY_RUN,FEATURE_FLAG,PORTOS_PROCESS_ENV_TEST,CERT_DIR,HF_HOME,HF_HUB_CACHE,BUN_INSTALL,GH_TOKEN,GITHUB_TOKEN,CLAUDE_CODE_USE_BEDROCK,PORTOS_WORKSPACE_ROOTSif already documented), and assert every remaining name appears as aNAME=token in.env.example(commented or not). Each allowlist entry carries a one-line reason comment. Decision: allowlist by category with reasons rather than snapshotting today's diff, so the test stays meaningful as the tree grows.NAME=in.env.exampleis read somewhere underserver/,scripts/,ecosystem.config.cjsordocker-compose.yml— this catches a documented-but-dead variable, the other drift direction.Tests
server/envExampleDrift.test.jsis the test. Two cases (forward and reverse drift) plus fixture strings proving the extractor seesprocess.env.FOO,process.env['FOO']is NOT matched (document that limitation in the header), and that a commented# FOO=barline in.env.examplecounts as documented. Pins the next undocumented variable.Acceptance criteria
.env.examplewith a one-line comment naming module and default.server/envExampleDrift.test.jspasses in both directions with the allowlist as written..env.examplelines.cd server && npm testpasses.Out of scope — startup validation / fail-fast for these variables (they are all optional overrides with defaults), and the client's
VITE_*variables.Filed by a
/do:better --scan-only --issuesaudit (2026-09-01). Category:stack-specific· Severity:medium· Files:server/services/signalSync.js:85, server/services/signalSync.js:91, server/services/signalSync.js:246, server/services/imessageSync.js:262, server/services/contactsSync.js:191, server/services/remoteDesktop.js:22, server/services/eidoverseWorld.js:588, server/services/videoGen/grok.js:49, server/services/imageGen/regen.js:100, server/services/specDecodeModels.js:43, server/services/loraTraining/runtimes.js:137, server/services/videoGen/renderArgs.js:104All labels already exist in the repo; do NOT create labels. Never add
planner:*labels.