Skip to content

.env.example has drifted from the environment variables the server actually reads #5706

Description

@atomantic

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

  1. 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.
  2. 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.
  3. 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

  • All twelve variables appear in .env.example with a one-line comment naming module and default.
  • server/envExampleDrift.test.js passes in both directions with the allowlist as written.
  • No real host name, path, or credential appears in the added .env.example lines.
  • cd server && npm test passes.

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions