[Bug]: WSL launch drops inherited entries when the Windows environment key is spelled wslenv #35957
Send to my agentSummary#
The desktop can discard a user's existing environment-sharing configuration when Windows stores its variable name with a different letter case. Appending a T3 setting then replaces the inherited entries, so their variables and path-translation or direction flags no longer reach the Linux backend. A reduced probe executing the checked-in environment builder demonstrates the conflicting keys; the Windows selection consequence follows Node's documented subprocess behavior.
Before submitting#
- Related issues: searched open and closed issues and PRs; the related telemetry PR is listed under Additional context.
- Removed credentials, tokens, private paths, and sensitive data.
Area#
apps/desktop
Observed behavior#
With an inherited Windows key spelled wslenv=GOPATH/plu:KEEP/w:Mixed/p:EMPTY::KEEP/w and T3CODE_TELEMETRY_ENABLED=false, the WSL configuration builder produces both the inherited wslenv property and a new WSLENV=T3CODE_TELEMETRY_ENABLED property. Node's documented Windows spawn rule selects the lexicographically first case-insensitive match, which is the new uppercase key. All inherited entries therefore disappear from the WSLENV value handed to wsl.exe. This was reproduced at the source-derived builder boundary on macOS; the Windows spawn selection was modeled from the official Node contract, not observed on Windows.
Reproduction#
- Use a clean checkout of the commit below and save the script below as
repro-wslenv.mjsoutside the checkout. - Run
node repro-wslenv.mjs /path/to/checkout. No dependency installation, credentials, Electron instance, Windows host, or WSL distro is required. - The uppercase-key control retains the original flagged entries and appends the T3 variable. The lowercase-key case constructs conflicting Windows environment keys and loses the inherited value under the documented Windows spawn ordering rule.
The reduced configuration case reproduced deterministically once; an earlier combined exploration showed the same result. This does not run the full Effect service, a Windows process, or an actual WSL launch. The proxy models Windows case-insensitive process.env lookup while keeping the input's enumerated property spelling; the copied ordinary object then follows normal JavaScript property semantics.
The script reads the production constants, merge helper, environment-building statements, and final environment expression directly from the checkout, strips their TypeScript annotations, and evaluates them without importing Electron. It prints its Node and Git versions, uses a temporary directory for all home/state variables, removes that directory, and leaves the checkout untouched. Exit 1 means the configuration defect reproduced, 0 means the expected merged value was retained, and 2 means the setup or a control failed. Its source extraction intentionally returns inconclusive if the code layout changes.
Reproduction script: repro-wslenv.mjs
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import vm from 'node:vm';
import { stripTypeScriptTypes } from 'node:module';
import { execFileSync } from 'node:child_process';
console.log(`node ${process.version}; ${process.platform} ${process.arch}`);
const checkout = path.resolve(process.argv[2]);
console.log(execFileSync('git',['--version'],{encoding:'utf8'}).trim());
console.log(`commit ${execFileSync('git', ['-C', checkout, 'rev-parse', 'HEAD'], {encoding:'utf8'}).trim()}`);
const source = fs.readFileSync(path.join(checkout, 'apps/desktop/src/backend/DesktopBackendConfiguration.ts'), 'utf8');
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'wsl-env-probe-'));
try {
for (const key of ['HOME','USERPROFILE','APPDATA','LOCALAPPDATA','XDG_CONFIG_HOME','XDG_DATA_HOME','XDG_STATE_HOME','XDG_CACHE_HOME','T3CODE_HOME','TMPDIR']) {
const dir=path.join(scratch,key); fs.mkdirSync(dir); process.env[key]=dir;
}
const cut = (start,end) => {
const i=source.indexOf(start), j=source.indexOf(end,i);
if(i<0||j<0) throw Error('source layout changed');
return source.slice(i,j);
};
const helpers=cut('const DESKTOP_BACKEND_ENV_NAMES =','const logBackendObservabilitySettingsReadFailure =');
const builder=cut(' const forwardedEnv: Record<string, string> = {};',' const baseConfig = {');
const envExpression=cut(' env: {\n ...parentEnvWithoutT3Home,',' // env already excludes T3CODE_HOME;').replace(/^ env: /,'').trim().replace(/,$/,'');
const api=vm.runInNewContext(stripTypeScriptTypes(`${helpers}\n({mergeWslEnv, build(env: Record<string,string|undefined>) {${builder}\nreturn ${envExpression};}})`,{mode:'strip'}));
const lookup = (object,key) => object[Object.keys(object).find(k=>k.toUpperCase()===key.toUpperCase())];
// Models Windows process.env lookup while preserving its enumerated key spelling.
const windowsEnv = values=>new Proxy(values,{get:(object,key)=> typeof key==='string'?lookup(object,key):object[key]});
const inherited='GOPATH/plu:KEEP/w:Mixed/p:EMPTY::KEEP/w';
const control=api.build(windowsEnv({WSLENV:inherited,T3CODE_TELEMETRY_ENABLED:'false',GOPATH:'C:\\sdk'}));
if(control.WSLENV!==`${inherited}:T3CODE_TELEMETRY_ENABLED`) throw Error('uppercase WSLENV control failed');
if(api.mergeWslEnv('Mixed/p:mixed/w::DUP/u:DUP/w',['Mixed','MIXED'])!=='Mixed/p:mixed/w::DUP/u:DUP/w:MIXED') throw Error('case-sensitive entries control failed');
console.log(`CONTROL WSLENV=${control.WSLENV}`);
const lowercase=api.build(windowsEnv({wslenv:inherited,T3CODE_TELEMETRY_ENABLED:'false',GOPATH:'C:\\sdk'}));
// Apply the documented Node Windows env-key ordering rule; no Windows process is spawned.
const selected=Object.fromEntries(Object.keys(lowercase).sort().filter((key,index,keys)=>keys.findIndex(k=>k.toUpperCase()===key.toUpperCase())===index).map(key=>[key,lowercase[key]]));
console.log(`CASE wslenv: config keys=${Object.keys(lowercase).filter(k=>k.toUpperCase()==='WSLENV').join(',')}`);
console.log(`CASE wslenv: documented Windows spawn selection=${selected.WSLENV}`);
const failed=selected.WSLENV!==`${inherited}:T3CODE_TELEMETRY_ENABLED`;
console.log(failed?'RESULT: defect reproduced':'RESULT: expected behavior');
process.exitCode=failed?1:0;
} catch(error) {
console.log(`setup/control failure: ${error.message}`);
console.log('RESULT: inconclusive'); process.exitCode=2;
} finally {fs.rmSync(scratch,{recursive:true,force:true});}
Expected behavior#
The WSL launch must preserve the inherited list and its case-sensitive entry names, duplicate/empty segments, and /p, /l, /u, /w flags while appending T3's own variables. The Windows environment variable's key spelling must not cause that list to be replaced. Microsoft documents the entry semantics in WSLENV flags; Node documents that Windows environment variable lookup is case-insensitive in process.env and that conflicting subprocess keys are resolved by lexicographic order in child_process. T3 itself says it preserves the user's existing entries in apps/desktop/src/backend/DesktopBackendConfiguration.ts#L130-L152.
Impact and scope#
Impact: P2 - The launch configuration can remove user-defined Windows-to-WSL environment forwarding, including path translation and direction flags, whenever a non-uppercase Windows WSLENV key coincides with at least one nonempty T3-forwarded variable.
The affected input and corrupted builder output are demonstrated. The uppercase control establishes a bounded configuration workaround at this boundary: recreate the host variable with the uppercase spelling WSLENV before starting T3. NOT_ENOUGH_INFO: an actual Windows/WSL launch was unavailable, so the final Linux process environment and the failure of a particular provider or SDK were not measured; running the desktop on Windows with the shown synthetic environment would establish that scope.
Version#
T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93. The repository's Electron pin is 44.10.1; no dependency pins were changed.
Runtime and platform#
Probe: Node v26.12.1, Git 2.56.0, macOS (darwin), arm64. The affected target is the Windows desktop WSL backend. NOT_ENOUGH_INFO: no Windows, installed WSL, or Linux distribution runtime version was available because this is a reduced source-derived configuration probe.
Interface#
Other: desktop WSL backend process environment construction.
Earliest failure stage#
Setup or configuration.
Relevant output or evidence#
node v26.12.1; darwin arm64
git version 2.56.0
commit 59e64eef881632a23f89c4138bcf37f23d92bf93
CONTROL WSLENV=GOPATH/plu:KEEP/w:Mixed/p:EMPTY::KEEP/w:T3CODE_TELEMETRY_ENABLED
CASE wslenv: config keys=wslenv,WSLENV
CASE wslenv: documented Windows spawn selection=T3CODE_TELEMETRY_ENABLED
RESULT: defect reproduced
Node also prints its standard experimental warning for stripTypeScriptTypes; it does not affect the result. All values above are synthetic.
The inherited process.env service is returned without key normalization in packages/shared/src/HostProcess.ts#L61-L66. The builder copies its entries to an ordinary record, then reads only .WSLENV and inserts a new uppercase key in apps/desktop/src/backend/DesktopBackendConfiguration.ts#L638-L670. The configuration environment is passed to the process spawner in apps/desktop/src/backend/DesktopBackendManager.ts#L460-L471.
Additional context#
- https://github.com/pingdotgg/t3code/pull/13641 — adds standard OTLP forwarding and discusses pre-existing flagged entries. This report concerns the case of the Windows WSLENV key itself, including unrelated inherited entries, rather than changing the semantics of any flagged OTLP entry.
tanstack-source added labelsbug
juliusmarminge closed this
Sign in to join this conversation.
I tried to reproduce this on the current build and could not.
On origin/main 0c4b1d8f27 I changed the existing DesktopBackendConfiguration.test.ts case 'resolveWsl preserves existing WSLENV entries when forwarding backend secrets' into it.effect.each(['WSLENV','wslenv']). It now asserts that exactly one WSLENV key (any case) comes out with the merged value. Ran
vp test run src/backend/DesktopBackendConfiguration.test.ts -t "entries when forwarding"in apps/desktop. The uppercase case passes. The lowercase case fails: config.env has two keys,wslenv= 'GOPATH/p:OPENAI_API_KEY/u:EMPTY::AZURE_DEVOPS_EXT_PAT/u' (the inherited value, untouched) andWSLENV= 'OPENAI_API_KEY:ANTHROPIC_API_KEY:T3CODE_OTLP_HEADERS:T3CODE_OTLP_PROTOCOL' (only T3's names). Not observed in a real Windows desktop launch of wsl.exe, because this VM has no Windows host or WSL. That last step (Windows spawn keeping the key that sorts first,WSLENV) comes from Node's documented behaviour.Closing as not planned. If it still happens for you, reply with the exact steps and the version you are on and it will be looked at again.